From 5cd6cb166ab51f0d07bab1367d0549abd8aaa6af Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 11:53:01 +0000 Subject: [PATCH 01/53] Configurable test log levels and common error handling --- .../src/middleware/errorHandling.ts | 28 +++++++++++++++ packages/backend-core/src/middleware/index.ts | 5 +-- packages/backend-core/tests/jestEnv.ts | 28 +++------------ packages/backend-core/tests/jestSetup.ts | 21 +++++++++++- packages/backend-core/tests/logging.ts | 34 +++++++++++++++++++ packages/server/src/api/index.ts | 25 ++------------ packages/server/src/tests/jestEnv.ts | 12 +++---- packages/server/src/tests/jestSetup.ts | 1 + packages/server/src/tests/logging.ts | 34 +++++++++++++++++++ packages/types/src/api/web/errors.ts | 1 + packages/worker/src/api/index.ts | 24 +++---------- .../routes/system/tests/migrations.spec.ts | 4 +-- .../src/middleware/tests/tenancy.spec.ts | 8 ++--- packages/worker/src/tests/jestEnv.ts | 2 +- packages/worker/src/tests/jestSetup.ts | 7 ++-- packages/worker/src/tests/logging.ts | 34 +++++++++++++++++++ 16 files changed, 182 insertions(+), 86 deletions(-) create mode 100644 packages/backend-core/src/middleware/errorHandling.ts create mode 100644 packages/backend-core/tests/logging.ts create mode 100644 packages/server/src/tests/logging.ts create mode 100644 packages/worker/src/tests/logging.ts diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts new file mode 100644 index 0000000000..1baaa92501 --- /dev/null +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -0,0 +1,28 @@ +import { APIError } from "@budibase/types" +import * as errors from "../errors" +import env from "../environment" + +export async function errorHandling(ctx: any, next: any) { + try { + await next() + } catch (err: any) { + const status = err.status || err.statusCode || 500 + ctx.status = status + + if (status > 499 || env.LOG_4XX) { + ctx.log.error(err) + } + + const error = errors.getPublicError(err) + const body: APIError = { + message: err.message, + status: status, + validationErrors: err.validation, + error, + } + + ctx.body = body + } +} + +export default errorHandling diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index 4986cde64b..de609f9a3e 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -1,7 +1,7 @@ export * as jwt from "./passport/jwt" export * as local from "./passport/local" -export * as google from "./passport/google" -export * as oidc from "./passport/oidc" +export * as google from "./passport/sso/google" +export * as oidc from "./passport/sso/oidc" import * as datasourceGoogle from "./passport/datasource/google" export const datasource = { google: datasourceGoogle, @@ -16,4 +16,5 @@ export { default as adminOnly } from "./adminOnly" export { default as builderOrAdmin } from "./builderOrAdmin" export { default as builderOnly } from "./builderOnly" export { default as logging } from "./logging" +export { default as errorHandling } from "./errorHandling" export * as joiValidator from "./joi-validator" diff --git a/packages/backend-core/tests/jestEnv.ts b/packages/backend-core/tests/jestEnv.ts index 1190eb3bb7..71cf865737 100644 --- a/packages/backend-core/tests/jestEnv.ts +++ b/packages/backend-core/tests/jestEnv.ts @@ -1,23 +1,5 @@ -import env from "../src/environment" -import { mocks } from "./utilities" - -// must explicitly enable fetch mock -mocks.fetch.enable() - -// mock all dates to 2020-01-01T00:00:00.000Z -// use tk.reset() to use real dates in individual tests -import tk from "timekeeper" -tk.freeze(mocks.date.MOCK_DATE) - -env._set("SELF_HOSTED", "1") -env._set("NODE_ENV", "jest") - -if (!process.env.DEBUG) { - global.console.log = jest.fn() // console.log are ignored in tests -} - -if (!process.env.CI) { - // set a longer timeout in dev for debugging - // 100 seconds - jest.setTimeout(100000) -} +process.env.SELF_HOSTED = "1" +process.env.MULTI_TENANCY = "1" +process.env.NODE_ENV = "jest" +process.env.MOCK_REDIS = "1" +process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" diff --git a/packages/backend-core/tests/jestSetup.ts b/packages/backend-core/tests/jestSetup.ts index f7887ec824..e786086de6 100644 --- a/packages/backend-core/tests/jestSetup.ts +++ b/packages/backend-core/tests/jestSetup.ts @@ -1,4 +1,23 @@ +import "./logging" import env from "../src/environment" -import { testContainerUtils } from "./utilities" +import { mocks, testContainerUtils } from "./utilities" + +// must explicitly enable fetch mock +mocks.fetch.enable() + +// mock all dates to 2020-01-01T00:00:00.000Z +// use tk.reset() to use real dates in individual tests +import tk from "timekeeper" +tk.freeze(mocks.date.MOCK_DATE) + +if (!process.env.DEBUG) { + console.log = jest.fn() // console.log are ignored in tests +} + +if (!process.env.CI) { + // set a longer timeout in dev for debugging + // 100 seconds + jest.setTimeout(100000) +} testContainerUtils.setupEnv(env) diff --git a/packages/backend-core/tests/logging.ts b/packages/backend-core/tests/logging.ts new file mode 100644 index 0000000000..271f4d62ff --- /dev/null +++ b/packages/backend-core/tests/logging.ts @@ -0,0 +1,34 @@ +export enum LogLevel { + TRACE = "trace", + DEBUG = "debug", + INFO = "info", + WARN = "warn", + ERROR = "error", +} + +const LOG_INDEX: { [key in LogLevel]: number } = { + [LogLevel.TRACE]: 1, + [LogLevel.DEBUG]: 2, + [LogLevel.INFO]: 3, + [LogLevel.WARN]: 4, + [LogLevel.ERROR]: 5, +} + +const setIndex = LOG_INDEX[process.env.LOG_LEVEL as LogLevel] + +if (setIndex > LOG_INDEX.trace) { + global.console.trace = jest.fn() +} + +if (setIndex > LOG_INDEX.debug) { + global.console.debug = jest.fn() +} + +if (setIndex > LOG_INDEX.info) { + global.console.info = jest.fn() + global.console.log = jest.fn() +} + +if (setIndex > LOG_INDEX.warn) { + global.console.warn = jest.fn() +} diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts index 3375161dd8..78a6056366 100644 --- a/packages/server/src/api/index.ts +++ b/packages/server/src/api/index.ts @@ -1,5 +1,5 @@ import Router from "@koa/router" -import { errors, auth } from "@budibase/backend-core" +import { auth, middleware } from "@budibase/backend-core" import currentApp from "../middleware/currentapp" import zlib from "zlib" import { mainRoutes, staticRoutes, publicRoutes } from "./routes" @@ -14,6 +14,8 @@ export const router: Router = new Router() router.get("/health", ctx => (ctx.status = 200)) router.get("/version", ctx => (ctx.body = pkg.version)) +router.use(middleware.errorHandling) + router .use( compress({ @@ -54,27 +56,6 @@ router .use(currentApp) .use(auth.auditLog) -// error handling middleware -router.use(async (ctx, next) => { - try { - await next() - } catch (err: any) { - ctx.status = err.status || err.statusCode || 500 - const error = errors.getPublicError(err) - ctx.body = { - message: err.message, - status: ctx.status, - validationErrors: err.validation, - error, - } - ctx.log.error(err) - // unauthorised errors don't provide a useful trace - if (!env.isTest()) { - console.trace(err) - } - } -}) - // authenticated routes for (let route of mainRoutes) { router.use(route.routes()) diff --git a/packages/server/src/tests/jestEnv.ts b/packages/server/src/tests/jestEnv.ts index 9707893bd9..b1ef038c1b 100644 --- a/packages/server/src/tests/jestEnv.ts +++ b/packages/server/src/tests/jestEnv.ts @@ -1,9 +1,9 @@ -import env from "../environment" import { tmpdir } from "os" -env._set("SELF_HOSTED", "1") -env._set("NODE_ENV", "jest") -env._set("MULTI_TENANCY", "1") +process.env.SELF_HOSTED = "1" +process.env.NODE_ENV = "jest" +process.env.MULTI_TENANCY = "1" // @ts-ignore -env._set("BUDIBASE_DIR", tmpdir("budibase-unittests")) -env._set("LOG_LEVEL", "silent") +process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") +process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.MOCK_REDIS = "1" diff --git a/packages/server/src/tests/jestSetup.ts b/packages/server/src/tests/jestSetup.ts index d87ccbfe7c..b052d9941b 100644 --- a/packages/server/src/tests/jestSetup.ts +++ b/packages/server/src/tests/jestSetup.ts @@ -1,3 +1,4 @@ +import "./logging" import env from "../environment" import { env as coreEnv } from "@budibase/backend-core" import { testContainerUtils } from "@budibase/backend-core/tests" diff --git a/packages/server/src/tests/logging.ts b/packages/server/src/tests/logging.ts new file mode 100644 index 0000000000..271f4d62ff --- /dev/null +++ b/packages/server/src/tests/logging.ts @@ -0,0 +1,34 @@ +export enum LogLevel { + TRACE = "trace", + DEBUG = "debug", + INFO = "info", + WARN = "warn", + ERROR = "error", +} + +const LOG_INDEX: { [key in LogLevel]: number } = { + [LogLevel.TRACE]: 1, + [LogLevel.DEBUG]: 2, + [LogLevel.INFO]: 3, + [LogLevel.WARN]: 4, + [LogLevel.ERROR]: 5, +} + +const setIndex = LOG_INDEX[process.env.LOG_LEVEL as LogLevel] + +if (setIndex > LOG_INDEX.trace) { + global.console.trace = jest.fn() +} + +if (setIndex > LOG_INDEX.debug) { + global.console.debug = jest.fn() +} + +if (setIndex > LOG_INDEX.info) { + global.console.info = jest.fn() + global.console.log = jest.fn() +} + +if (setIndex > LOG_INDEX.warn) { + global.console.warn = jest.fn() +} diff --git a/packages/types/src/api/web/errors.ts b/packages/types/src/api/web/errors.ts index 65870d6a29..996c0ba34c 100644 --- a/packages/types/src/api/web/errors.ts +++ b/packages/types/src/api/web/errors.ts @@ -2,4 +2,5 @@ export interface APIError { message: string status: number error?: any + validationErrors?: any } diff --git a/packages/worker/src/api/index.ts b/packages/worker/src/api/index.ts index d8df62f532..b390d36bb8 100644 --- a/packages/worker/src/api/index.ts +++ b/packages/worker/src/api/index.ts @@ -3,8 +3,7 @@ const compress = require("koa-compress") const zlib = require("zlib") import { routes } from "./routes" import { middleware as pro } from "@budibase/pro" -import { errors, auth, middleware } from "@budibase/backend-core" -import { APIError } from "@budibase/types" +import { auth, middleware } from "@budibase/backend-core" const PUBLIC_ENDPOINTS = [ // deprecated single tenant sso callback @@ -109,7 +108,9 @@ const NO_TENANCY_ENDPOINTS = [ const NO_CSRF_ENDPOINTS = [...PUBLIC_ENDPOINTS] const router: Router = new Router() + router + .use(middleware.errorHandling) .use( compress({ threshold: 2048, @@ -136,29 +137,12 @@ router (!ctx.isAuthenticated || (ctx.user && !ctx.user.budibaseAccess)) && !ctx.internal ) { - ctx.throw(403, "Unauthorized - no public worker access") + ctx.throw(403, "Unauthorized") } return next() }) .use(middleware.auditLog) -// error handling middleware - TODO: This could be moved to backend-core -router.use(async (ctx, next) => { - try { - await next() - } catch (err: any) { - ctx.log.error(err) - ctx.status = err.status || err.statusCode || 500 - const error = errors.getPublicError(err) - const body: APIError = { - message: err.message, - status: ctx.status, - error, - } - ctx.body = body - } -}) - router.get("/health", ctx => (ctx.status = 200)) // authenticated routes diff --git a/packages/worker/src/api/routes/system/tests/migrations.spec.ts b/packages/worker/src/api/routes/system/tests/migrations.spec.ts index 304a64761e..950f5c2153 100644 --- a/packages/worker/src/api/routes/system/tests/migrations.spec.ts +++ b/packages/worker/src/api/routes/system/tests/migrations.spec.ts @@ -30,7 +30,7 @@ describe("/api/system/migrations", () => { headers: {}, status: 403, }) - expect(res.text).toBe("Unauthorized - no public worker access") + expect(res.body).toEqual({ message: "Unauthorized", status: 403 }) expect(migrateFn).toBeCalledTimes(0) }) @@ -47,7 +47,7 @@ describe("/api/system/migrations", () => { headers: {}, status: 403, }) - expect(res.text).toBe("Unauthorized - no public worker access") + expect(res.body).toEqual({ message: "Unauthorized", status: 403 }) }) it("returns definitions", async () => { diff --git a/packages/worker/src/middleware/tests/tenancy.spec.ts b/packages/worker/src/middleware/tests/tenancy.spec.ts index a8b7a50e55..8853291634 100644 --- a/packages/worker/src/middleware/tests/tenancy.spec.ts +++ b/packages/worker/src/middleware/tests/tenancy.spec.ts @@ -24,7 +24,7 @@ describe("tenancy middleware", () => { }) it("should get tenant id from header", async () => { - const tenantId = structures.uuid() + const tenantId = structures.tenant.id() const headers = { [constants.Header.TENANT_ID]: tenantId, } @@ -35,7 +35,7 @@ describe("tenancy middleware", () => { }) it("should get tenant id from query param", async () => { - const tenantId = structures.uuid() + const tenantId = structures.tenant.id() const res = await config.request.get( `/api/global/configs/checklist?tenantId=${tenantId}` ) @@ -43,7 +43,7 @@ describe("tenancy middleware", () => { }) it("should get tenant id from subdomain", async () => { - const tenantId = structures.uuid() + const tenantId = structures.tenant.id() const headers = { host: `${tenantId}.localhost:10000`, } @@ -67,7 +67,7 @@ describe("tenancy middleware", () => { it("should throw when no tenant id is found", async () => { const res = await config.request.get(`/api/global/configs/checklist`) expect(res.status).toBe(403) - expect(res.text).toBe("Tenant id not set") + expect(res.body).toEqual({ message: "Tenant id not set", status: 403 }) expect(res.headers[constants.Header.TENANT_ID]).toBe(undefined) }) }) diff --git a/packages/worker/src/tests/jestEnv.ts b/packages/worker/src/tests/jestEnv.ts index 0b27cf52aa..602a505c1b 100644 --- a/packages/worker/src/tests/jestEnv.ts +++ b/packages/worker/src/tests/jestEnv.ts @@ -1,7 +1,7 @@ process.env.SELF_HOSTED = "0" process.env.NODE_ENV = "jest" process.env.JWT_SECRET = "test-jwtsecret" -process.env.LOG_LEVEL = "silent" +process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" process.env.MULTI_TENANCY = "1" process.env.MINIO_URL = "http://localhost" process.env.MINIO_ACCESS_KEY = "test" diff --git a/packages/worker/src/tests/jestSetup.ts b/packages/worker/src/tests/jestSetup.ts index 2deef96176..31d36f00ae 100644 --- a/packages/worker/src/tests/jestSetup.ts +++ b/packages/worker/src/tests/jestSetup.ts @@ -1,5 +1,6 @@ -import { mocks, testContainerUtils } from "@budibase/backend-core/tests" +import "./logging" +import { mocks, testContainerUtils } from "@budibase/backend-core/tests" import env from "../environment" import { env as coreEnv } from "@budibase/backend-core" @@ -11,10 +12,6 @@ mocks.fetch.enable() const tk = require("timekeeper") tk.freeze(mocks.date.MOCK_DATE) -if (!process.env.DEBUG) { - global.console.log = jest.fn() // console.log are ignored in tests -} - if (!process.env.CI) { // set a longer timeout in dev for debugging // 100 seconds diff --git a/packages/worker/src/tests/logging.ts b/packages/worker/src/tests/logging.ts new file mode 100644 index 0000000000..271f4d62ff --- /dev/null +++ b/packages/worker/src/tests/logging.ts @@ -0,0 +1,34 @@ +export enum LogLevel { + TRACE = "trace", + DEBUG = "debug", + INFO = "info", + WARN = "warn", + ERROR = "error", +} + +const LOG_INDEX: { [key in LogLevel]: number } = { + [LogLevel.TRACE]: 1, + [LogLevel.DEBUG]: 2, + [LogLevel.INFO]: 3, + [LogLevel.WARN]: 4, + [LogLevel.ERROR]: 5, +} + +const setIndex = LOG_INDEX[process.env.LOG_LEVEL as LogLevel] + +if (setIndex > LOG_INDEX.trace) { + global.console.trace = jest.fn() +} + +if (setIndex > LOG_INDEX.debug) { + global.console.debug = jest.fn() +} + +if (setIndex > LOG_INDEX.info) { + global.console.info = jest.fn() + global.console.log = jest.fn() +} + +if (setIndex > LOG_INDEX.warn) { + global.console.warn = jest.fn() +} From ae9979929ad92b8be257422e8cd6c6339892e8c4 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 11:57:30 +0000 Subject: [PATCH 02/53] Enable higher concurrency and resiliency in worker tests --- packages/backend-core/package.json | 4 +- .../src/cache/tests/writethrough.spec.js | 61 ------ .../src/cache/tests/writethrough.spec.ts | 73 +++++++ packages/backend-core/src/cache/user.ts | 14 +- .../backend-core/src/context/deprovision.ts | 108 ---------- .../src/context/tests/index.spec.ts | 11 +- packages/backend-core/src/db/db.ts | 3 +- .../src/db/tests/utils.seq.spec.ts | 190 ----------------- .../backend-core/src/db/tests/utils.spec.ts | 192 +++++++++++++++++ packages/backend-core/src/db/views.ts | 64 +++--- packages/backend-core/src/events/analytics.ts | 4 +- .../posthog/tests/PosthogProcessor.spec.ts | 6 +- .../backend-core/src/featureFlags/index.ts | 4 +- packages/backend-core/src/index.ts | 20 +- .../src/middleware/authenticated.ts | 2 +- .../src/middleware/passport/utils.ts | 4 +- .../backend-core/src/middleware/tenancy.ts | 3 +- .../backend-core/src/migrations/migrations.ts | 8 +- .../src/objectStore/buckets/global.ts | 4 +- .../src/objectStore/buckets/plugins.ts | 4 +- packages/backend-core/src/platform/index.ts | 3 + .../backend-core/src/platform/platformDb.ts | 6 + packages/backend-core/src/platform/tenants.ts | 101 +++++++++ .../src/platform/tests/tenants.spec.ts | 25 +++ packages/backend-core/src/platform/users.ts | 90 ++++++++ packages/backend-core/src/redis/index.ts | 2 +- packages/backend-core/src/redis/redlock.ts | 39 ++-- packages/backend-core/src/tenancy/db.ts | 6 + packages/backend-core/src/tenancy/index.ts | 2 +- packages/backend-core/src/tenancy/tenancy.ts | 102 +-------- .../src/utils/tests/utils.spec.ts | 46 ++-- .../tests/utilities/DBTestConfiguration.ts | 32 +++ packages/backend-core/tests/utilities/db.ts | 9 - .../backend-core/tests/utilities/index.ts | 3 +- .../backend-core/tests/utilities/testEnv.ts | 16 +- packages/backend-core/yarn.lock | 8 +- packages/server/package.json | 3 +- .../src/api/routes/tests/backup.spec.ts | 1 - .../src/tests/utilities/TestConfiguration.ts | 96 +++++---- packages/server/yarn.lock | 109 ++++++---- packages/types/src/sdk/locks.ts | 1 + packages/worker/package.json | 5 +- .../src/api/controllers/global/users.ts | 49 +++-- .../src/api/controllers/system/tenants.ts | 12 +- .../api/routes/global/tests/configs.spec.ts | 198 ++++++++---------- .../src/api/routes/global/tests/roles.spec.ts | 5 +- .../src/api/routes/global/tests/users.spec.ts | 84 ++++---- .../worker/src/api/routes/system/tenants.ts | 2 +- .../api/routes/system/tests/restore.spec.ts | 4 +- .../api/routes/system/tests/status.spec.ts | 10 +- .../functions/globalInfoSyncUsers.ts | 7 +- packages/worker/src/sdk/tenants/index.ts | 1 + packages/worker/src/sdk/tenants/tenants.ts | 76 +++++++ packages/worker/src/sdk/users/users.ts | 2 +- .../worker/src/tests/TestConfiguration.ts | 121 ++++------- packages/worker/src/tests/api/base.ts | 3 +- packages/worker/src/tests/api/restore.ts | 1 + packages/worker/yarn.lock | 110 +++++----- 58 files changed, 1165 insertions(+), 1004 deletions(-) delete mode 100644 packages/backend-core/src/cache/tests/writethrough.spec.js create mode 100644 packages/backend-core/src/cache/tests/writethrough.spec.ts delete mode 100644 packages/backend-core/src/context/deprovision.ts delete mode 100644 packages/backend-core/src/db/tests/utils.seq.spec.ts create mode 100644 packages/backend-core/src/db/tests/utils.spec.ts create mode 100644 packages/backend-core/src/platform/index.ts create mode 100644 packages/backend-core/src/platform/platformDb.ts create mode 100644 packages/backend-core/src/platform/tenants.ts create mode 100644 packages/backend-core/src/platform/tests/tenants.spec.ts create mode 100644 packages/backend-core/src/platform/users.ts create mode 100644 packages/backend-core/src/tenancy/db.ts create mode 100644 packages/backend-core/tests/utilities/DBTestConfiguration.ts delete mode 100644 packages/backend-core/tests/utilities/db.ts create mode 100644 packages/worker/src/sdk/tenants/index.ts create mode 100644 packages/worker/src/sdk/tenants/tenants.ts diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index e20798f3ac..ac87025303 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -18,7 +18,7 @@ "build:pro": "../../scripts/pro/build.sh", "postbuild": "yarn run build:pro", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", - "test": "jest --coverage --maxWorkers=2", + "test": "jest --coverage", "test:watch": "jest --watchAll" }, "dependencies": { @@ -62,7 +62,7 @@ "@trendyol/jest-testcontainers": "^2.1.1", "@types/chance": "1.1.3", "@types/ioredis": "4.28.0", - "@types/jest": "27.5.1", + "@types/jest": "28.1.1", "@types/koa": "2.13.4", "@types/koa-pino-logger": "3.0.0", "@types/lodash": "4.14.180", diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.js b/packages/backend-core/src/cache/tests/writethrough.spec.js deleted file mode 100644 index fefca30c18..0000000000 --- a/packages/backend-core/src/cache/tests/writethrough.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -require("../../../tests") -const { Writethrough } = require("../writethrough") -const { getDB } = require("../../db") -const tk = require("timekeeper") -const { structures } = require("../../../tests") - -const START_DATE = Date.now() -tk.freeze(START_DATE) - - -const DELAY = 5000 - -const db = getDB(structures.db.id()) -const db2 = getDB(structures.db.id()) -const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY) - -describe("writethrough", () => { - describe("put", () => { - let first - it("should be able to store, will go to DB", async () => { - const response = await writethrough.put({ _id: "test", value: 1 }) - const output = await db.get(response.id) - first = output - expect(output.value).toBe(1) - }) - - it("second put shouldn't update DB", async () => { - const response = await writethrough.put({ ...first, value: 2 }) - const output = await db.get(response.id) - expect(first._rev).toBe(output._rev) - expect(output.value).toBe(1) - }) - - it("should put it again after delay period", async () => { - tk.freeze(START_DATE + DELAY + 1) - const response = await writethrough.put({ ...first, value: 3 }) - const output = await db.get(response.id) - expect(response.rev).not.toBe(first._rev) - expect(output.value).toBe(3) - }) - }) - - describe("get", () => { - it("should be able to retrieve", async () => { - const response = await writethrough.get("test") - expect(response.value).toBe(3) - }) - }) - - describe("same doc, different databases (tenancy)", () => { - it("should be able to two different databases", async () => { - const resp1 = await writethrough.put({ _id: "db1", value: "first" }) - const resp2 = await writethrough2.put({ _id: "db1", value: "second" }) - expect(resp1.rev).toBeDefined() - expect(resp2.rev).toBeDefined() - expect((await db.get("db1")).value).toBe("first") - expect((await db2.get("db1")).value).toBe("second") - }) - }) -}) - diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.ts b/packages/backend-core/src/cache/tests/writethrough.spec.ts new file mode 100644 index 0000000000..d346788121 --- /dev/null +++ b/packages/backend-core/src/cache/tests/writethrough.spec.ts @@ -0,0 +1,73 @@ +import { structures, DBTestConfiguration } from "../../../tests" +import { Writethrough } from "../writethrough" +import { getDB } from "../../db" +import tk from "timekeeper" + +const START_DATE = Date.now() +tk.freeze(START_DATE) + +const DELAY = 5000 + +describe("writethrough", () => { + const config = new DBTestConfiguration() + + const db = getDB(structures.db.id()) + const db2 = getDB(structures.db.id()) + + const writethrough = new Writethrough(db, DELAY) + const writethrough2 = new Writethrough(db2, DELAY) + + describe("put", () => { + let first: any + + it("should be able to store, will go to DB", async () => { + await config.doInTenant(async () => { + const response = await writethrough.put({ _id: "test", value: 1 }) + const output = await db.get(response.id) + first = output + expect(output.value).toBe(1) + }) + }) + + it("second put shouldn't update DB", async () => { + await config.doInTenant(async () => { + const response = await writethrough.put({ ...first, value: 2 }) + const output = await db.get(response.id) + expect(first._rev).toBe(output._rev) + expect(output.value).toBe(1) + }) + }) + + it("should put it again after delay period", async () => { + await config.doInTenant(async () => { + tk.freeze(START_DATE + DELAY + 1) + const response = await writethrough.put({ ...first, value: 3 }) + const output = await db.get(response.id) + expect(response.rev).not.toBe(first._rev) + expect(output.value).toBe(3) + }) + }) + }) + + describe("get", () => { + it("should be able to retrieve", async () => { + await config.doInTenant(async () => { + const response = await writethrough.get("test") + expect(response.value).toBe(3) + }) + }) + }) + + describe("same doc, different databases (tenancy)", () => { + it("should be able to two different databases", async () => { + await config.doInTenant(async () => { + const resp1 = await writethrough.put({ _id: "db1", value: "first" }) + const resp2 = await writethrough2.put({ _id: "db1", value: "second" }) + expect(resp1.rev).toBeDefined() + expect(resp2.rev).toBeDefined() + expect((await db.get("db1")).value).toBe("first") + expect((await db2.get("db1")).value).toBe("second") + }) + }) + }) +}) diff --git a/packages/backend-core/src/cache/user.ts b/packages/backend-core/src/cache/user.ts index a128465cd6..b514c3af9b 100644 --- a/packages/backend-core/src/cache/user.ts +++ b/packages/backend-core/src/cache/user.ts @@ -1,8 +1,9 @@ import * as redis from "../redis/init" -import { getTenantId, lookupTenantId, doWithGlobalDB } from "../tenancy" +import * as tenancy from "../tenancy" +import * as context from "../context" +import * as platform from "../platform" import env from "../environment" -import * as accounts from "../cloud/accounts" -import { Database } from "@budibase/types" +import * as accounts from "../accounts" const EXPIRY_SECONDS = 3600 @@ -10,7 +11,8 @@ const EXPIRY_SECONDS = 3600 * The default populate user function */ async function populateFromDB(userId: string, tenantId: string) { - const user = await doWithGlobalDB(tenantId, (db: Database) => db.get(userId)) + const db = tenancy.getTenantDB(tenantId) + const user = await db.get(userId) user.budibaseAccess = true if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const account = await accounts.getAccount(user.email) @@ -42,9 +44,9 @@ export async function getUser( } if (!tenantId) { try { - tenantId = getTenantId() + tenantId = context.getTenantId() } catch (err) { - tenantId = await lookupTenantId(userId) + tenantId = await platform.users.lookupTenantId(userId) } } const client = await redis.getUserClient() diff --git a/packages/backend-core/src/context/deprovision.ts b/packages/backend-core/src/context/deprovision.ts deleted file mode 100644 index 81f03096dc..0000000000 --- a/packages/backend-core/src/context/deprovision.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - getGlobalUserParams, - getAllApps, - doWithDB, - StaticDatabases, -} from "../db" -import { doWithGlobalDB } from "../tenancy" -import { App, Tenants, User, Database } from "@budibase/types" - -const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants -const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name - -async function removeTenantFromInfoDB(tenantId: string) { - try { - await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => { - const tenants = (await infoDb.get(TENANT_DOC)) as Tenants - tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) - - await infoDb.put(tenants) - }) - } catch (err) { - console.error(`Error removing tenant ${tenantId} from info db`, err) - throw err - } -} - -export async function removeUserFromInfoDB(dbUser: User) { - await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => { - const keys = [dbUser._id!, dbUser.email] - const userDocs = await infoDb.allDocs({ - keys, - include_docs: true, - }) - const toDelete = userDocs.rows.map((row: any) => { - return { - ...row.doc, - _deleted: true, - } - }) - await infoDb.bulkDocs(toDelete) - }) -} - -async function removeUsersFromInfoDB(tenantId: string) { - return doWithGlobalDB(tenantId, async (db: any) => { - try { - const allUsers = await db.allDocs( - getGlobalUserParams(null, { - include_docs: true, - }) - ) - await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { - const allEmails = allUsers.rows.map((row: any) => row.doc.email) - // get the id docs - let keys = allUsers.rows.map((row: any) => row.id) - // and the email docs - keys = keys.concat(allEmails) - // retrieve the docs and delete them - const userDocs = await infoDb.allDocs({ - keys, - include_docs: true, - }) - const toDelete = userDocs.rows.map((row: any) => { - return { - ...row.doc, - _deleted: true, - } - }) - await infoDb.bulkDocs(toDelete) - }) - } catch (err) { - console.error(`Error removing tenant ${tenantId} users from info db`, err) - throw err - } - }) -} - -async function removeGlobalDB(tenantId: string) { - return doWithGlobalDB(tenantId, async (db: Database) => { - try { - await db.destroy() - } catch (err) { - console.error(`Error removing tenant ${tenantId} users from info db`, err) - throw err - } - }) -} - -async function removeTenantApps(tenantId: string) { - try { - const apps = (await getAllApps({ all: true })) as App[] - const destroyPromises = apps.map(app => - doWithDB(app.appId, (db: Database) => db.destroy()) - ) - await Promise.allSettled(destroyPromises) - } catch (err) { - console.error(`Error removing tenant ${tenantId} apps`, err) - throw err - } -} - -// can't live in tenancy package due to circular dependency on db/utils -export async function deleteTenant(tenantId: string) { - await removeTenantFromInfoDB(tenantId) - await removeUsersFromInfoDB(tenantId) - await removeGlobalDB(tenantId) - await removeTenantApps(tenantId) -} diff --git a/packages/backend-core/src/context/tests/index.spec.ts b/packages/backend-core/src/context/tests/index.spec.ts index c9b5870ffa..5c8ce6fc19 100644 --- a/packages/backend-core/src/context/tests/index.spec.ts +++ b/packages/backend-core/src/context/tests/index.spec.ts @@ -1,11 +1,14 @@ -require("../../../tests") +import { testEnv } from "../../../tests" const context = require("../") const { DEFAULT_TENANT_ID } = require("../../constants") -import env from "../../environment" describe("context", () => { describe("doInTenant", () => { describe("single-tenancy", () => { + beforeAll(() => { + testEnv.singleTenant() + }) + it("defaults to the default tenant", () => { const tenantId = context.getTenantId() expect(tenantId).toBe(DEFAULT_TENANT_ID) @@ -20,8 +23,8 @@ describe("context", () => { }) describe("multi-tenancy", () => { - beforeEach(() => { - env._set("MULTI_TENANCY", 1) + beforeAll(() => { + testEnv.multiTenant() }) it("fails when no tenant id is set", () => { diff --git a/packages/backend-core/src/db/db.ts b/packages/backend-core/src/db/db.ts index bd6b5e13c1..f13eb9a965 100644 --- a/packages/backend-core/src/db/db.ts +++ b/packages/backend-core/src/db/db.ts @@ -1,7 +1,6 @@ import env from "../environment" -import { directCouchQuery, getPouchDB } from "./couch" +import { directCouchQuery, DatabaseImpl } from "./couch" import { CouchFindOptions, Database } from "@budibase/types" -import { DatabaseImpl } from "../db" const dbList = new Set() diff --git a/packages/backend-core/src/db/tests/utils.seq.spec.ts b/packages/backend-core/src/db/tests/utils.seq.spec.ts deleted file mode 100644 index 83253402f7..0000000000 --- a/packages/backend-core/src/db/tests/utils.seq.spec.ts +++ /dev/null @@ -1,190 +0,0 @@ -require("../../../tests") -const { - getDevelopmentAppID, - getProdAppID, - isDevAppID, - isProdAppID, -} = require("../conversions") -const { generateAppID, getPlatformUrl, getScopedConfig } = require("../utils") -const tenancy = require("../../tenancy") -const { Config, DEFAULT_TENANT_ID } = require("../../constants") -import { generator } from "../../../tests" -import env from "../../environment" - -describe("utils", () => { - describe("app ID manipulation", () => { - function getID() { - const appId = generateAppID() - const split = appId.split("_") - const uuid = split[split.length - 1] - const devAppId = `app_dev_${uuid}` - return { appId, devAppId, split, uuid } - } - - it("should be able to generate a new app ID", () => { - expect(generateAppID().startsWith("app_")).toEqual(true) - }) - - it("should be able to convert a production app ID to development", () => { - const { appId, uuid } = getID() - expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) - }) - - it("should be able to convert a development app ID to development", () => { - const { devAppId, uuid } = getID() - expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) - }) - - it("should be able to convert a development ID to a production", () => { - const { devAppId, uuid } = getID() - expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) - }) - - it("should be able to convert a production ID to production", () => { - const { appId, uuid } = getID() - expect(getProdAppID(appId)).toEqual(`app_${uuid}`) - }) - - it("should be able to confirm dev app ID is development", () => { - const { devAppId } = getID() - expect(isDevAppID(devAppId)).toEqual(true) - }) - - it("should be able to confirm prod app ID is not development", () => { - const { appId } = getID() - expect(isDevAppID(appId)).toEqual(false) - }) - - it("should be able to confirm prod app ID is prod", () => { - const { appId } = getID() - expect(isProdAppID(appId)).toEqual(true) - }) - - it("should be able to confirm dev app ID is not prod", () => { - const { devAppId } = getID() - expect(isProdAppID(devAppId)).toEqual(false) - }) - }) -}) - -const DEFAULT_URL = "http://localhost:10000" -const ENV_URL = "http://env.com" - -const setDbPlatformUrl = async (dbUrl: string) => { - const db = tenancy.getGlobalDB() - await db.put({ - _id: "config_settings", - type: Config.SETTINGS, - config: { - platformUrl: dbUrl, - }, - }) -} - -const clearSettingsConfig = async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const db = tenancy.getGlobalDB() - try { - const config = await db.get("config_settings") - await db.remove("config_settings", config._rev) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - }) -} - -describe("getPlatformUrl", () => { - describe("self host", () => { - beforeEach(async () => { - env._set("SELF_HOST", 1) - await clearSettingsConfig() - }) - - it("gets the default url", async () => { - await tenancy.doInTenant(null, async () => { - const url = await getPlatformUrl() - expect(url).toBe(DEFAULT_URL) - }) - }) - - it("gets the platform url from the environment", async () => { - await tenancy.doInTenant(null, async () => { - env._set("PLATFORM_URL", ENV_URL) - const url = await getPlatformUrl() - expect(url).toBe(ENV_URL) - }) - }) - - it("gets the platform url from the database", async () => { - await tenancy.doInTenant(null, async () => { - const dbUrl = generator.url() - await setDbPlatformUrl(dbUrl) - const url = await getPlatformUrl() - expect(url).toBe(dbUrl) - }) - }) - }) - - describe("cloud", () => { - const TENANT_AWARE_URL = "http://default.env.com" - - beforeEach(async () => { - env._set("SELF_HOSTED", 0) - env._set("MULTI_TENANCY", 1) - env._set("PLATFORM_URL", ENV_URL) - await clearSettingsConfig() - }) - - it("gets the platform url from the environment without tenancy", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const url = await getPlatformUrl({ tenantAware: false }) - expect(url).toBe(ENV_URL) - }) - }) - - it("gets the platform url from the environment with tenancy", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const url = await getPlatformUrl() - expect(url).toBe(TENANT_AWARE_URL) - }) - }) - - it("never gets the platform url from the database", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - await setDbPlatformUrl(generator.url()) - const url = await getPlatformUrl() - expect(url).toBe(TENANT_AWARE_URL) - }) - }) - }) -}) - -describe("getScopedConfig", () => { - describe("settings config", () => { - beforeEach(async () => { - env._set("SELF_HOSTED", 1) - env._set("PLATFORM_URL", "") - await clearSettingsConfig() - }) - - it("returns the platform url with an existing config", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const dbUrl = generator.url() - await setDbPlatformUrl(dbUrl) - const db = tenancy.getGlobalDB() - const config = await getScopedConfig(db, { type: Config.SETTINGS }) - expect(config.platformUrl).toBe(dbUrl) - }) - }) - - it("returns the platform url without an existing config", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const db = tenancy.getGlobalDB() - const config = await getScopedConfig(db, { type: Config.SETTINGS }) - expect(config.platformUrl).toBe(DEFAULT_URL) - }) - }) - }) -}) diff --git a/packages/backend-core/src/db/tests/utils.spec.ts b/packages/backend-core/src/db/tests/utils.spec.ts new file mode 100644 index 0000000000..7bdca5ae8b --- /dev/null +++ b/packages/backend-core/src/db/tests/utils.spec.ts @@ -0,0 +1,192 @@ +import { generator, DBTestConfiguration, testEnv } from "../../../tests" +import { + getDevelopmentAppID, + getProdAppID, + isDevAppID, + isProdAppID, +} from "../conversions" +import { generateAppID, getPlatformUrl, getScopedConfig } from "../utils" +import * as context from "../../context" +import { Config } from "../../constants" +import env from "../../environment" + +describe("utils", () => { + const config = new DBTestConfiguration() + + describe("app ID manipulation", () => { + function getID() { + const appId = generateAppID() + const split = appId.split("_") + const uuid = split[split.length - 1] + const devAppId = `app_dev_${uuid}` + return { appId, devAppId, split, uuid } + } + + it("should be able to generate a new app ID", () => { + expect(generateAppID().startsWith("app_")).toEqual(true) + }) + + it("should be able to convert a production app ID to development", () => { + const { appId, uuid } = getID() + expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) + }) + + it("should be able to convert a development app ID to development", () => { + const { devAppId, uuid } = getID() + expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) + }) + + it("should be able to convert a development ID to a production", () => { + const { devAppId, uuid } = getID() + expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) + }) + + it("should be able to convert a production ID to production", () => { + const { appId, uuid } = getID() + expect(getProdAppID(appId)).toEqual(`app_${uuid}`) + }) + + it("should be able to confirm dev app ID is development", () => { + const { devAppId } = getID() + expect(isDevAppID(devAppId)).toEqual(true) + }) + + it("should be able to confirm prod app ID is not development", () => { + const { appId } = getID() + expect(isDevAppID(appId)).toEqual(false) + }) + + it("should be able to confirm prod app ID is prod", () => { + const { appId } = getID() + expect(isProdAppID(appId)).toEqual(true) + }) + + it("should be able to confirm dev app ID is not prod", () => { + const { devAppId } = getID() + expect(isProdAppID(devAppId)).toEqual(false) + }) + }) + + const DEFAULT_URL = "http://localhost:10000" + const ENV_URL = "http://env.com" + + const setDbPlatformUrl = async (dbUrl: string) => { + const db = context.getGlobalDB() + await db.put({ + _id: "config_settings", + type: Config.SETTINGS, + config: { + platformUrl: dbUrl, + }, + }) + } + + const clearSettingsConfig = async () => { + await config.doInTenant(async () => { + const db = context.getGlobalDB() + try { + const config = await db.get("config_settings") + await db.remove("config_settings", config._rev) + } catch (e: any) { + if (e.status !== 404) { + throw e + } + } + }) + } + + describe("getPlatformUrl", () => { + describe("self host", () => { + beforeEach(async () => { + testEnv.selfHosted() + await clearSettingsConfig() + }) + + it("gets the default url", async () => { + await config.doInTenant(async () => { + const url = await getPlatformUrl() + expect(url).toBe(DEFAULT_URL) + }) + }) + + it("gets the platform url from the environment", async () => { + await config.doInTenant(async () => { + env._set("PLATFORM_URL", ENV_URL) + const url = await getPlatformUrl() + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the database", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const url = await getPlatformUrl() + expect(url).toBe(dbUrl) + }) + }) + }) + + describe("cloud", () => { + const TENANT_AWARE_URL = `http://${config.tenantId}.env.com` + + beforeEach(async () => { + testEnv.cloudHosted() + testEnv.multiTenant() + + env._set("PLATFORM_URL", ENV_URL) + await clearSettingsConfig() + }) + + it("gets the platform url from the environment without tenancy", async () => { + await config.doInTenant(async () => { + const url = await getPlatformUrl({ tenantAware: false }) + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the environment with tenancy", async () => { + await config.doInTenant(async () => { + const url = await getPlatformUrl() + expect(url).toBe(TENANT_AWARE_URL) + }) + }) + + it("never gets the platform url from the database", async () => { + await config.doInTenant(async () => { + await setDbPlatformUrl(generator.url()) + const url = await getPlatformUrl() + expect(url).toBe(TENANT_AWARE_URL) + }) + }) + }) + }) + + describe("getScopedConfig", () => { + describe("settings config", () => { + beforeEach(async () => { + env._set("SELF_HOSTED", 1) + env._set("PLATFORM_URL", "") + await clearSettingsConfig() + }) + + it("returns the platform url with an existing config", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const db = context.getGlobalDB() + const config = await getScopedConfig(db, { type: Config.SETTINGS }) + expect(config.platformUrl).toBe(dbUrl) + }) + }) + + it("returns the platform url without an existing config", async () => { + await config.doInTenant(async () => { + const db = context.getGlobalDB() + const config = await getScopedConfig(db, { type: Config.SETTINGS }) + expect(config.platformUrl).toBe(DEFAULT_URL) + }) + }) + }) + }) +}) diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index 4a87be0a68..8a2c2e7efd 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -1,13 +1,14 @@ import { - DocumentType, - ViewName, DeprecatedViews, + DocumentType, SEPARATOR, StaticDatabases, + ViewName, } from "../constants" import { getGlobalDB } from "../context" import { doWithDB } from "./" import { Database, DatabaseQueryOpts } from "@budibase/types" +import env from "../environment" const DESIGN_DB = "_design/database" @@ -69,17 +70,6 @@ export const createNewUserEmailView = async () => { await createView(db, viewJs, ViewName.USER_BY_EMAIL) } -export const createAccountEmailView = async () => { - const viewJs = `function(doc) { - if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { - emit(doc.email.toLowerCase(), doc._id) - } - }` - await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { - await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) - }) -} - export const createUserAppView = async () => { const db = getGlobalDB() const viewJs = `function(doc) { @@ -113,17 +103,6 @@ export const createUserBuildersView = async () => { await createView(db, viewJs, ViewName.USER_BY_BUILDERS) } -export const createPlatformUserView = async () => { - const viewJs = `function(doc) { - if (doc.tenantId) { - emit(doc._id.toLowerCase(), doc._id) - } - }` - await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { - await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) - }) -} - export interface QueryViewOptions { arrayResponse?: boolean } @@ -162,13 +141,48 @@ export const queryView = async ( } } +// PLATFORM + +async function createPlatformView(viewJs: string, viewName: ViewName) { + try { + await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { + await createView(db, viewJs, viewName) + }) + } catch (e: any) { + if (e.status === 409 && env.isTest()) { + // multiple tests can try to initialise platforms views + // at once - safe to exit on conflict + return + } + throw e + } +} + +export const createPlatformAccountEmailView = async () => { + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }` + await createPlatformView(viewJs, ViewName.ACCOUNT_BY_EMAIL) +} + +export const createPlatformUserView = async () => { + const viewJs = `function(doc) { + if (doc.tenantId) { + emit(doc._id.toLowerCase(), doc._id) + } + }` + await createPlatformView(viewJs, ViewName.PLATFORM_USERS_LOWERCASE) +} + export const queryPlatformView = async ( viewName: ViewName, params: DatabaseQueryOpts, opts?: QueryViewOptions ): Promise => { const CreateFuncByName: any = { - [ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView, + [ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, } diff --git a/packages/backend-core/src/events/analytics.ts b/packages/backend-core/src/events/analytics.ts index f621a9c98b..7fbc6d9c2b 100644 --- a/packages/backend-core/src/events/analytics.ts +++ b/packages/backend-core/src/events/analytics.ts @@ -1,5 +1,5 @@ import env from "../environment" -import * as tenancy from "../tenancy" +import * as context from "../context" import * as dbUtils from "../db/utils" import { Config } from "../constants" import { withCache, TTL, CacheKey } from "../cache" @@ -42,7 +42,7 @@ export const enabled = async () => { } const getSettingsDoc = async () => { - const db = tenancy.getGlobalDB() + const db = context.getGlobalDB() let settings try { settings = await db.get(dbUtils.generateConfigID({ type: Config.SETTINGS })) diff --git a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts index 349a0427ac..2c1340d36e 100644 --- a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +++ b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts @@ -1,4 +1,4 @@ -import "../../../../../tests" +import { testEnv } from "../../../../../tests" import PosthogProcessor from "../PosthogProcessor" import { Event, IdentityType, Hosting } from "@budibase/types" const tk = require("timekeeper") @@ -16,6 +16,10 @@ const newIdentity = () => { } describe("PosthogProcessor", () => { + beforeAll(() => { + testEnv.singleTenant() + }) + beforeEach(async () => { jest.clearAllMocks() await cache.bustCache( diff --git a/packages/backend-core/src/featureFlags/index.ts b/packages/backend-core/src/featureFlags/index.ts index 34ee3599a5..877cd60e1a 100644 --- a/packages/backend-core/src/featureFlags/index.ts +++ b/packages/backend-core/src/featureFlags/index.ts @@ -1,5 +1,5 @@ import env from "../environment" -import * as tenancy from "../tenancy" +import * as context from "../context" /** * Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant. @@ -28,7 +28,7 @@ export function buildFeatureFlags() { } export function isEnabled(featureFlag: string) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() const flags = getTenantFeatureFlags(tenantId) return flags.includes(featureFlag) } diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index b38a53e9e4..d507d8175f 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -3,12 +3,11 @@ export * as migrations from "./migrations" export * as users from "./users" export * as roles from "./security/roles" export * as permissions from "./security/permissions" -export * as accounts from "./cloud/accounts" +export * as accounts from "./accounts" export * as installation from "./installation" -export * as tenancy from "./tenancy" export * as featureFlags from "./featureFlags" export * as sessions from "./security/sessions" -export * as deprovisioning from "./context/deprovision" +export * as platform from "./platform" export * as auth from "./auth" export * as constants from "./constants" export * as logging from "./logging" @@ -21,20 +20,27 @@ export * as context from "./context" export * as cache from "./cache" export * as objectStore from "./objectStore" export * as redis from "./redis" +export * as locks from "./redis/redlock" export * as utils from "./utils" export * as errors from "./errors" export { default as env } from "./environment" +// Add context to tenancy for backwards compatibility +// only do this for external usages to prevent internal +// circular dependencies +import * as context from "./context" +import * as _tenancy from "./tenancy" +export const tenancy = { + ..._tenancy, + ...context, +} + // expose error classes directly export * from "./errors" // expose constants directly export * from "./constants" -// expose inner locks from redis directly -import * as redis from "./redis" -export const locks = redis.redlock - // expose package init function import * as db from "./db" export const init = (opts: any = {}) => { diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 3b5e9ae162..4bb2aaba76 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -4,7 +4,7 @@ import { getUser } from "../cache/user" import { getSession, updateSessionTTL } from "../security/sessions" import { buildMatcherRegex, matches } from "./matchers" import { SEPARATOR, queryGlobalView, ViewName } from "../db" -import { getGlobalDB, doInTenant } from "../tenancy" +import { getGlobalDB, doInTenant } from "../context" import { decrypt } from "../security/encryption" import * as identity from "../context/identity" import env from "../environment" diff --git a/packages/backend-core/src/middleware/passport/utils.ts b/packages/backend-core/src/middleware/passport/utils.ts index 3d79aada28..6eb3bc29d1 100644 --- a/packages/backend-core/src/middleware/passport/utils.ts +++ b/packages/backend-core/src/middleware/passport/utils.ts @@ -1,6 +1,6 @@ -import { isMultiTenant, getTenantId } from "../../tenancy" +import { isMultiTenant, getTenantId } from "../../context" import { getScopedConfig } from "../../db" -import { ConfigType, Database, Config } from "@budibase/types" +import { ConfigType, Database } from "@budibase/types" /** * Utility to handle authentication errors. diff --git a/packages/backend-core/src/middleware/tenancy.ts b/packages/backend-core/src/middleware/tenancy.ts index a09c463045..22b7cc213d 100644 --- a/packages/backend-core/src/middleware/tenancy.ts +++ b/packages/backend-core/src/middleware/tenancy.ts @@ -1,4 +1,5 @@ -import { doInTenant, getTenantIDFromCtx } from "../tenancy" +import { doInTenant } from "../context" +import { getTenantIDFromCtx } from "../tenancy" import { buildMatcherRegex, matches } from "./matchers" import { Header } from "../constants" import { diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts index 55b8ab1938..79c7eb55ea 100644 --- a/packages/backend-core/src/migrations/migrations.ts +++ b/packages/backend-core/src/migrations/migrations.ts @@ -7,7 +7,7 @@ import { doWithDB, } from "../db" import environment from "../environment" -import { doInTenant, getTenantIds, getTenantId } from "../tenancy" +import * as platform from "../platform" import * as context from "../context" import { DEFINITIONS } from "." import { @@ -47,7 +47,7 @@ export const runMigration = async ( const migrationType = migration.type let tenantId: string | undefined if (migrationType !== MigrationType.INSTALLATION) { - tenantId = getTenantId() + tenantId = context.getTenantId() } const migrationName = migration.name const silent = migration.silent @@ -160,7 +160,7 @@ export const runMigrations = async ( tenantIds = [options.noOp.tenantId] } else if (!options.tenantIds || !options.tenantIds.length) { // run for all tenants - tenantIds = await getTenantIds() + tenantIds = await platform.tenants.getTenantIds() } else { tenantIds = options.tenantIds } @@ -185,7 +185,7 @@ export const runMigrations = async ( // for all migrations for (const migration of migrations) { // run the migration - await doInTenant(tenantId, () => runMigration(migration, options)) + await context.doInTenant(tenantId, () => runMigration(migration, options)) } } console.log("Migrations complete") diff --git a/packages/backend-core/src/objectStore/buckets/global.ts b/packages/backend-core/src/objectStore/buckets/global.ts index 8bf883b11e..69e201bb98 100644 --- a/packages/backend-core/src/objectStore/buckets/global.ts +++ b/packages/backend-core/src/objectStore/buckets/global.ts @@ -1,5 +1,5 @@ import env from "../../environment" -import * as tenancy from "../../tenancy" +import * as context from "../../context" import * as objectStore from "../objectStore" import * as cloudfront from "../cloudfront" @@ -22,7 +22,7 @@ export const getGlobalFileUrl = (type: string, name: string, etag?: string) => { export const getGlobalFileS3Key = (type: string, name: string) => { let file = `${type}/${name}` if (env.MULTI_TENANCY) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() file = `${tenantId}/${file}` } return file diff --git a/packages/backend-core/src/objectStore/buckets/plugins.ts b/packages/backend-core/src/objectStore/buckets/plugins.ts index cd3bf77e87..f7721afb23 100644 --- a/packages/backend-core/src/objectStore/buckets/plugins.ts +++ b/packages/backend-core/src/objectStore/buckets/plugins.ts @@ -1,6 +1,6 @@ import env from "../../environment" import * as objectStore from "../objectStore" -import * as tenancy from "../../tenancy" +import * as context from "../../context" import * as cloudfront from "../cloudfront" import { Plugin } from "@budibase/types" @@ -61,7 +61,7 @@ const getPluginS3Key = (plugin: Plugin, fileName: string) => { export const getPluginS3Dir = (pluginName: string) => { let s3Key = `${pluginName}` if (env.MULTI_TENANCY) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() s3Key = `${tenantId}/${s3Key}` } if (env.CLOUDFRONT_CDN) { diff --git a/packages/backend-core/src/platform/index.ts b/packages/backend-core/src/platform/index.ts new file mode 100644 index 0000000000..877d85ade0 --- /dev/null +++ b/packages/backend-core/src/platform/index.ts @@ -0,0 +1,3 @@ +export * as users from "./users" +export * as tenants from "./tenants" +export * from "./platformDb" diff --git a/packages/backend-core/src/platform/platformDb.ts b/packages/backend-core/src/platform/platformDb.ts new file mode 100644 index 0000000000..90b683dd33 --- /dev/null +++ b/packages/backend-core/src/platform/platformDb.ts @@ -0,0 +1,6 @@ +import { StaticDatabases } from "../constants" +import { getDB } from "../db/db" + +export function getPlatformDB() { + return getDB(StaticDatabases.PLATFORM_INFO.name) +} diff --git a/packages/backend-core/src/platform/tenants.ts b/packages/backend-core/src/platform/tenants.ts new file mode 100644 index 0000000000..b9f946a735 --- /dev/null +++ b/packages/backend-core/src/platform/tenants.ts @@ -0,0 +1,101 @@ +import { StaticDatabases } from "../constants" +import { getPlatformDB } from "./platformDb" +import { LockName, LockOptions, LockType, Tenants } from "@budibase/types" +import * as locks from "../redis/redlock" + +const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants + +export const tenacyLockOptions: LockOptions = { + type: LockType.DEFAULT, + name: LockName.UPDATE_TENANTS_DOC, + ttl: 10 * 1000, // auto expire after 10 seconds + systemLock: true, +} + +// READ + +export async function getTenantIds(): Promise { + const tenants = await getTenants() + return tenants.tenantIds +} + +async function getTenants(): Promise { + const db = getPlatformDB() + let tenants: Tenants + + try { + tenants = await db.get(TENANT_DOC) + } catch (e: any) { + // doesn't exist yet - create + if (e.status === 404) { + tenants = await createTenantsDoc() + } else { + throw e + } + } + + return tenants +} + +export async function exists(tenantId: string) { + const tenants = await getTenants() + return tenants.tenantIds.indexOf(tenantId) !== -1 +} + +// CREATE / UPDATE + +function newTenantsDoc(): Tenants { + return { + _id: TENANT_DOC, + tenantIds: [], + } +} + +async function createTenantsDoc(): Promise { + const db = getPlatformDB() + let tenants = newTenantsDoc() + + try { + const response = await db.put(tenants) + tenants._rev = response.rev + } catch (e: any) { + // don't throw 409 is doc has already been created + if (e.status === 409) { + return db.get(TENANT_DOC) + } + throw e + } + + return tenants +} + +export async function addTenant(tenantId: string) { + const db = getPlatformDB() + + // use a lock as tenant creation is conflict prone + await locks.doWithLock(tenacyLockOptions, async () => { + const tenants = await getTenants() + + // write the new tenant if it doesn't already exist + if (tenants.tenantIds.indexOf(tenantId) === -1) { + tenants.tenantIds.push(tenantId) + await db.put(tenants) + } + }) +} + +// DELETE + +export async function removeTenant(tenantId: string) { + try { + await locks.doWithLock(tenacyLockOptions, async () => { + const db = getPlatformDB() + const tenants = await getTenants() + tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) + await db.put(tenants) + }) + } catch (err) { + console.error(`Error removing tenant ${tenantId} from info db`, err) + throw err + } +} diff --git a/packages/backend-core/src/platform/tests/tenants.spec.ts b/packages/backend-core/src/platform/tests/tenants.spec.ts new file mode 100644 index 0000000000..92e999cb2d --- /dev/null +++ b/packages/backend-core/src/platform/tests/tenants.spec.ts @@ -0,0 +1,25 @@ +import { DBTestConfiguration, structures } from "../../../tests" +import * as tenants from "../tenants" + +describe("tenants", () => { + const config = new DBTestConfiguration() + + describe("addTenant", () => { + it("concurrently adds multiple tenants safely", async () => { + const tenant1 = structures.tenant.id() + const tenant2 = structures.tenant.id() + const tenant3 = structures.tenant.id() + + await Promise.all([ + tenants.addTenant(tenant1), + tenants.addTenant(tenant2), + tenants.addTenant(tenant3), + ]) + + const tenantIds = await tenants.getTenantIds() + expect(tenantIds.includes(tenant1)).toBe(true) + expect(tenantIds.includes(tenant2)).toBe(true) + expect(tenantIds.includes(tenant3)).toBe(true) + }) + }) +}) diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts new file mode 100644 index 0000000000..c65a7e0ec4 --- /dev/null +++ b/packages/backend-core/src/platform/users.ts @@ -0,0 +1,90 @@ +import { getPlatformDB } from "./platformDb" +import { DEFAULT_TENANT_ID } from "../constants" +import env from "../environment" +import { + PlatformUser, + PlatformUserByEmail, + PlatformUserById, + User, +} from "@budibase/types" + +// READ + +export async function lookupTenantId(userId: string) { + if (!env.MULTI_TENANCY) { + return DEFAULT_TENANT_ID + } + + const user = await getUserDoc(userId) + return user.tenantId +} + +async function getUserDoc(emailOrId: string): Promise { + const db = getPlatformDB() + return db.get(emailOrId) +} + +// CREATE + +function newUserIdDoc(id: string, tenantId: string): PlatformUserById { + return { + _id: id, + tenantId, + } +} + +function newUserEmailDoc( + userId: string, + email: string, + tenantId: string +): PlatformUserByEmail { + return { + _id: email, + userId, + tenantId, + } +} + +/** + * Add a new user id or email doc if it doesn't exist. + */ +async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) { + const db = getPlatformDB() + let user: PlatformUser + + try { + await db.get(emailOrId) + } catch (e: any) { + if (e.status === 404) { + user = newDocFn() + await db.put(user) + } else { + throw e + } + } +} + +export async function addUser(tenantId: string, userId: string, email: string) { + await Promise.all([ + addUserDoc(userId, () => newUserIdDoc(userId, tenantId)), + addUserDoc(email, () => newUserEmailDoc(userId, email, tenantId)), + ]) +} + +// DELETE + +export async function removeUser(user: User) { + const db = getPlatformDB() + const keys = [user._id!, user.email] + const userDocs = await db.allDocs({ + keys, + include_docs: true, + }) + const toDelete = userDocs.rows.map((row: any) => { + return { + ...row.doc, + _deleted: true, + } + }) + await db.bulkDocs(toDelete) +} diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index ea4379f048..5bf2c65c39 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -3,4 +3,4 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" -export * as redlock from "./redlock" +export * as locks from "./redlock" diff --git a/packages/backend-core/src/redis/redlock.ts b/packages/backend-core/src/redis/redlock.ts index 54b2c0a8d1..2021da2b56 100644 --- a/packages/backend-core/src/redis/redlock.ts +++ b/packages/backend-core/src/redis/redlock.ts @@ -1,29 +1,22 @@ import Redlock, { Options } from "redlock" import { getLockClient } from "./init" import { LockOptions, LockType } from "@budibase/types" -import * as tenancy from "../tenancy" - -let noRetryRedlock: Redlock | undefined +import * as context from "../context" +import env from "../environment" const getClient = async (type: LockType): Promise => { + if (env.isTest() && type !== LockType.TRY_ONCE) { + return newRedlock(OPTIONS.TEST) + } switch (type) { case LockType.TRY_ONCE: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.TRY_ONCE) - } - return noRetryRedlock + return newRedlock(OPTIONS.TRY_ONCE) } case LockType.DEFAULT: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.DEFAULT) - } - return noRetryRedlock + return newRedlock(OPTIONS.DEFAULT) } case LockType.DELAY_500: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.DELAY_500) - } - return noRetryRedlock + return newRedlock(OPTIONS.DELAY_500) } default: { throw new Error(`Could not get redlock client: ${type}`) @@ -36,6 +29,11 @@ export const OPTIONS = { // immediately throws an error if the lock is already held retryCount: 0, }, + TEST: { + // higher retry count in unit tests + // due to high contention. + retryCount: 100, + }, DEFAULT: { // the expected clock drift; for more details // see http://redis.io/topics/distlock @@ -69,12 +67,19 @@ export const doWithLock = async (opts: LockOptions, task: any) => { const redlock = await getClient(opts.type) let lock try { - // aquire lock - let name: string = `lock:${tenancy.getTenantId()}_${opts.name}` + // determine lock name + // by default use the tenantId for uniqueness, unless using a system lock + const prefix = opts.systemLock ? "system" : context.getTenantId() + let name: string = `lock:${prefix}_${opts.name}` + + // add additional unique name if required if (opts.nameSuffix) { name = name + `_${opts.nameSuffix}` } + + // create the lock lock = await redlock.lock(name, opts.ttl) + // perform locked task // need to await to ensure completion before unlocking const result = await task() diff --git a/packages/backend-core/src/tenancy/db.ts b/packages/backend-core/src/tenancy/db.ts new file mode 100644 index 0000000000..10477a8579 --- /dev/null +++ b/packages/backend-core/src/tenancy/db.ts @@ -0,0 +1,6 @@ +import { getDB } from "../db/db" +import { getGlobalDBName } from "../context" + +export function getTenantDB(tenantId: string) { + return getDB(getGlobalDBName(tenantId)) +} diff --git a/packages/backend-core/src/tenancy/index.ts b/packages/backend-core/src/tenancy/index.ts index 1618a136dd..3f17e33271 100644 --- a/packages/backend-core/src/tenancy/index.ts +++ b/packages/backend-core/src/tenancy/index.ts @@ -1,2 +1,2 @@ -export * from "../context" +export * from "./db" export * from "./tenancy" diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index 732402bcb7..e8ddf88226 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -1,4 +1,3 @@ -import { doWithDB, getGlobalDBName } from "../db" import { DEFAULT_TENANT_ID, getTenantId, @@ -11,10 +10,7 @@ import { TenantResolutionStrategy, GetTenantIdOptions, } from "@budibase/types" -import { Header, StaticDatabases } from "../constants" - -const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants -const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name +import { Header } from "../constants" export function addTenantToUrl(url: string) { const tenantId = getTenantId() @@ -27,89 +23,6 @@ export function addTenantToUrl(url: string) { return url } -export async function doesTenantExist(tenantId: string) { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - let tenants - try { - tenants = await db.get(TENANT_DOC) - } catch (err) { - // if theres an error the doc doesn't exist, no tenants exist - return false - } - return ( - tenants && - Array.isArray(tenants.tenantIds) && - tenants.tenantIds.indexOf(tenantId) !== -1 - ) - }) -} - -export async function tryAddTenant( - tenantId: string, - userId: string, - email: string, - afterCreateTenant: () => Promise -) { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - const getDoc = async (id: string) => { - if (!id) { - return null - } - try { - return await db.get(id) - } catch (err) { - return { _id: id } - } - } - let [tenants, userIdDoc, emailDoc] = await Promise.all([ - getDoc(TENANT_DOC), - getDoc(userId), - getDoc(email), - ]) - if (!Array.isArray(tenants.tenantIds)) { - tenants = { - _id: TENANT_DOC, - tenantIds: [], - } - } - let promises = [] - if (userIdDoc) { - userIdDoc.tenantId = tenantId - promises.push(db.put(userIdDoc)) - } - if (emailDoc) { - emailDoc.tenantId = tenantId - emailDoc.userId = userId - promises.push(db.put(emailDoc)) - } - if (tenants.tenantIds.indexOf(tenantId) === -1) { - tenants.tenantIds.push(tenantId) - promises.push(db.put(tenants)) - await afterCreateTenant() - } - await Promise.all(promises) - }) -} - -export function doWithGlobalDB(tenantId: string, cb: any) { - return doWithDB(getGlobalDBName(tenantId), cb) -} - -export async function lookupTenantId(userId: string) { - return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { - let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null - try { - const doc = await db.get(userId) - if (doc && doc.tenantId) { - tenantId = doc.tenantId - } - } catch (err) { - // just return the default - } - return tenantId - }) -} - export const isUserInAppTenant = (appId: string, user?: any) => { let userTenantId if (user) { @@ -121,19 +34,6 @@ export const isUserInAppTenant = (appId: string, user?: any) => { return tenantId === userTenantId } -export async function getTenantIds() { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - let tenants - try { - tenants = await db.get(TENANT_DOC) - } catch (err) { - // if theres an error the doc doesn't exist, no tenants exist - return [] - } - return (tenants && tenants.tenantIds) || [] - }) -} - const ALL_STRATEGIES = Object.values(TenantResolutionStrategy) export const getTenantIDFromCtx = ( diff --git a/packages/backend-core/src/utils/tests/utils.spec.ts b/packages/backend-core/src/utils/tests/utils.spec.ts index b3cd527fb3..7d6c5561e8 100644 --- a/packages/backend-core/src/utils/tests/utils.spec.ts +++ b/packages/backend-core/src/utils/tests/utils.spec.ts @@ -1,21 +1,12 @@ -import { structures } from "../../../tests" +import { structures, DBTestConfiguration } from "../../../tests" import * as utils from "../../utils" -import * as events from "../../events" import * as db from "../../db" import { Header } from "../../constants" -import { doInTenant } from "../../context" import { newid } from "../../utils" +import env from "../../environment" describe("utils", () => { - describe("platformLogout", () => { - it("should call platform logout", async () => { - await doInTenant(structures.tenant.id(), async () => { - const ctx = structures.koa.newContext() - await utils.platformLogout({ ctx, userId: "test" }) - expect(events.auth.logout).toBeCalledTimes(1) - }) - }) - }) + const config = new DBTestConfiguration() describe("getAppIdFromCtx", () => { it("gets appId from header", async () => { @@ -50,21 +41,28 @@ describe("utils", () => { }) it("gets appId from url", async () => { - const ctx = structures.koa.newContext() - const expected = db.generateAppID() - const app = structures.apps.app(expected) + await config.doInTenant(async () => { + const url = "http://test.com" + env._set("PLATFORM_URL", url) - // set custom url - const appUrl = newid() - app.url = `/${appUrl}` - ctx.path = `/app/${appUrl}` + const ctx = structures.koa.newContext() + ctx.host = `${config.tenantId}.test.com` - // save the app - const database = db.getDB(expected) - await database.put(app) + const expected = db.generateAppID(config.tenantId) + const app = structures.apps.app(expected) - const actual = await utils.getAppIdFromCtx(ctx) - expect(actual).toBe(expected) + // set custom url + const appUrl = newid() + app.url = `/${appUrl}` + ctx.path = `/app/${appUrl}` + + // save the app + const database = db.getDB(expected) + await database.put(app) + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(expected) + }) }) it("doesn't get appId from url when previewing", async () => { diff --git a/packages/backend-core/tests/utilities/DBTestConfiguration.ts b/packages/backend-core/tests/utilities/DBTestConfiguration.ts new file mode 100644 index 0000000000..cad62e2979 --- /dev/null +++ b/packages/backend-core/tests/utilities/DBTestConfiguration.ts @@ -0,0 +1,32 @@ +import "./mocks" +import * as structures from "./structures" +import * as testEnv from "./testEnv" +import * as context from "../../src/context" + +class DBTestConfiguration { + tenantId: string + + constructor() { + // db tests need to be multi tenant to prevent conflicts + testEnv.multiTenant() + this.tenantId = structures.tenant.id() + } + + // TENANCY + + doInTenant(task: any) { + return context.doInTenant(this.tenantId, () => { + return task() + }) + } + + getTenantId() { + try { + return context.getTenantId() + } catch (e) { + return this.tenantId! + } + } +} + +export default DBTestConfiguration diff --git a/packages/backend-core/tests/utilities/db.ts b/packages/backend-core/tests/utilities/db.ts deleted file mode 100644 index 84b77bb201..0000000000 --- a/packages/backend-core/tests/utilities/db.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as db from "../../src/db" - -const dbConfig = { - inMemory: true, -} - -export const init = () => { - db.init(dbConfig) -} diff --git a/packages/backend-core/tests/utilities/index.ts b/packages/backend-core/tests/utilities/index.ts index 468d980a7f..efe014908b 100644 --- a/packages/backend-core/tests/utilities/index.ts +++ b/packages/backend-core/tests/utilities/index.ts @@ -4,5 +4,4 @@ export { generator } from "./structures" export * as testEnv from "./testEnv" export * as testContainerUtils from "./testContainerUtils" -import * as dbConfig from "./db" -dbConfig.init() +export { default as DBTestConfiguration } from "./DBTestConfiguration" diff --git a/packages/backend-core/tests/utilities/testEnv.ts b/packages/backend-core/tests/utilities/testEnv.ts index b4f06b5153..b138e019fc 100644 --- a/packages/backend-core/tests/utilities/testEnv.ts +++ b/packages/backend-core/tests/utilities/testEnv.ts @@ -1,12 +1,12 @@ import env from "../../src/environment" -import * as tenancy from "../../src/tenancy" -import { newid } from "../../src/utils" +import * as context from "../../src/context" +import * as structures from "./structures" // TENANCY export async function withTenant(task: (tenantId: string) => any) { - const tenantId = newid() - return tenancy.doInTenant(tenantId, async () => { + const tenantId = structures.tenant.id() + return context.doInTenant(tenantId, async () => { await task(tenantId) }) } @@ -19,6 +19,14 @@ export function multiTenant() { env._set("MULTI_TENANCY", 1) } +export function selfHosted() { + env._set("SELF_HOSTED", 1) +} + +export function cloudHosted() { + env._set("SELF_HOSTED", 0) +} + // NODE export function nodeDev() { diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index d88b1058f9..5f8edb3df6 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -1197,10 +1197,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.1.tgz#2c8b6dc6ff85c33bcd07d0b62cb3d19ddfdb3ab9" - integrity sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ== +"@types/jest@28.1.1": + version "28.1.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-28.1.1.tgz#8c9ba63702a11f8c386ee211280e8b68cb093cd1" + integrity sha512-C2p7yqleUKtCkVjlOur9BWVA4HgUQmEj/HWCt5WzZ5mLXrWnyIfl0wGuArc+kBXsy0ZZfLp+7dywB4HtSVYGVA== dependencies: jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" diff --git a/packages/server/package.json b/packages/server/package.json index 391a5f326f..cfc5fa9fa3 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -141,6 +141,7 @@ "@types/pouchdb": "6.4.0", "@types/redis": "4.0.11", "@types/server-destroy": "1.0.1", + "@types/supertest": "2.0.12", "@types/tar": "6.1.3", "@typescript-eslint/parser": "5.45.0", "apidoc": "0.50.4", @@ -159,7 +160,7 @@ "path-to-regexp": "6.2.0", "prettier": "2.5.1", "rimraf": "3.0.2", - "supertest": "4.0.2", + "supertest": "6.2.2", "swagger-jsdoc": "6.1.0", "timekeeper": "2.2.0", "ts-jest": "28.0.4", diff --git a/packages/server/src/api/routes/tests/backup.spec.ts b/packages/server/src/api/routes/tests/backup.spec.ts index 7b325c080d..ef362ef403 100644 --- a/packages/server/src/api/routes/tests/backup.spec.ts +++ b/packages/server/src/api/routes/tests/backup.spec.ts @@ -19,7 +19,6 @@ describe("/backups", () => { .get(`/api/backups/export?appId=${config.getAppId()}&appname=test`) .set(config.defaultHeaders()) .expect(200) - expect(res.text).toBeDefined() expect(res.headers["content-type"]).toEqual("application/gzip") expect(events.app.exported).toBeCalledTimes(1) }) diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 5c45f89a2b..29a5f07607 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -236,42 +236,41 @@ class TestConfiguration { email = this.defaultUserValues.email, roles, }: any = {}) { - return tenancy.doWithGlobalDB(this.getTenantId(), async (db: Database) => { - let existing - try { - existing = await db.get(id) - } catch (err) { - existing = { email } - } - const user = { - _id: id, - ...existing, - roles: roles || {}, - tenantId: this.getTenantId(), - firstName, - lastName, - } - await sessions.createASession(id, { - sessionId: "sessionid", - tenantId: this.getTenantId(), - csrfToken: this.defaultUserValues.csrfToken, - }) - if (builder) { - user.builder = { global: true } - } else { - user.builder = { global: false } - } - if (admin) { - user.admin = { global: true } - } else { - user.admin = { global: false } - } - const resp = await db.put(user) - return { - _rev: resp.rev, - ...user, - } + const db = tenancy.getTenantDB(this.getTenantId()) + let existing + try { + existing = await db.get(id) + } catch (err) { + existing = { email } + } + const user = { + _id: id, + ...existing, + roles: roles || {}, + tenantId: this.getTenantId(), + firstName, + lastName, + } + await sessions.createASession(id, { + sessionId: "sessionid", + tenantId: this.getTenantId(), + csrfToken: this.defaultUserValues.csrfToken, }) + if (builder) { + user.builder = { global: true } + } else { + user.builder = { global: false } + } + if (admin) { + user.admin = { global: true } + } else { + user.admin = { global: false } + } + const resp = await db.put(user) + return { + _rev: resp.rev, + ...user, + } } async createUser( @@ -407,20 +406,19 @@ class TestConfiguration { // API async generateApiKey(userId = this.defaultUserValues.globalUserId) { - return tenancy.doWithGlobalDB(this.getTenantId(), async (db: any) => { - const id = dbCore.generateDevInfoID(userId) - let devInfo - try { - devInfo = await db.get(id) - } catch (err) { - devInfo = { _id: id, userId } - } - devInfo.apiKey = encryption.encrypt( - `${this.getTenantId()}${dbCore.SEPARATOR}${newid()}` - ) - await db.put(devInfo) - return devInfo.apiKey - }) + const db = tenancy.getTenantDB(this.getTenantId()) + const id = dbCore.generateDevInfoID(userId) + let devInfo + try { + devInfo = await db.get(id) + } catch (err) { + devInfo = { _id: id, userId } + } + devInfo.apiKey = encryption.encrypt( + `${this.getTenantId()}${dbCore.SEPARATOR}${newid()}` + ) + await db.put(devInfo) + return devInfo.apiKey } // APP diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 4982b57131..c3fc7b0bbe 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -3547,6 +3547,14 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/superagent@*": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.16.tgz#12c9c16f232f9d89beab91d69368f96ce8e2d881" + integrity sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + "@types/superagent@^4.1.12": version "4.1.15" resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" @@ -3555,6 +3563,13 @@ "@types/cookiejar" "*" "@types/node" "*" +"@types/supertest@2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + "@types/tar@6.1.3": version "6.1.3" resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.3.tgz#46a2ce7617950c4852dfd7e9cd41aa8161b9d750" @@ -4241,7 +4256,7 @@ arrify@^2.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -asap@^2.0.3: +asap@^2.0.0, asap@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== @@ -5416,7 +5431,7 @@ commoner@^0.10.1: q "^1.1.2" recast "^0.11.17" -component-emitter@^1.2.0, component-emitter@^1.2.1: +component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -5518,7 +5533,7 @@ cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookiejar@^2.1.0: +cookiejar@^2.1.3: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== @@ -5967,6 +5982,14 @@ detective@^4.3.1: acorn "^5.2.1" defined "^1.0.0" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-match-patch@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" @@ -6061,11 +6084,6 @@ dotenv@16.0.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== -dotenv@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - dotenv@^8.2.0: version "8.6.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" @@ -6888,7 +6906,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -6977,7 +6995,7 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.1.tgz#790fcff8f808c2e12fabbfb2be5cb2deda448fa0" integrity sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A== -fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8: +fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.0.8, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -7264,7 +7282,7 @@ form-data@4.0.0, form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@^2.3.1, form-data@^2.5.0: +form-data@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -7291,11 +7309,21 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^1.1.1, formidable@^1.2.0: +formidable@^1.1.1: version "1.2.6" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== +formidable@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.1.tgz#81269cbea1a613240049f5f61a9d97731517414f" + integrity sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + forwarded-parse@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" @@ -7947,6 +7975,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -10703,7 +10736,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@^1.1.1, methods@^1.1.2: +methods@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== @@ -10755,7 +10788,12 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.24, mime-types@^2.1.27, dependencies: mime-db "1.52.0" -mime@^1.3.4, mime@^1.4.1: +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mime@^1.3.4: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -12502,14 +12540,14 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qs@^6.11.0: +qs@^6.10.3, qs@^6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" -qs@^6.4.0, qs@^6.5.1: +qs@^6.4.0: version "6.10.5" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== @@ -13990,29 +14028,30 @@ sublevel-pouchdb@7.2.2: ltgt "2.2.1" readable-stream "1.1.14" -superagent@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" - integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== +superagent@^7.1.0: + version "7.1.6" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-7.1.6.tgz#64f303ed4e4aba1e9da319f134107a54cacdc9c6" + integrity sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g== dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.2.0" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.3.5" + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.10.3" + readable-stream "^3.6.0" + semver "^7.3.7" -supertest@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36" - integrity sha512-1BAbvrOZsGA3YTCWqbmh14L0YEq0EGICX/nBnfkfVJn7SrxQV1I3pMYjSzG9y/7ZU2V9dWqyqk2POwxlb09duQ== +supertest@6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.2.tgz#04a5998fd3efaff187cb69f07a169755d655b001" + integrity sha512-wCw9WhAtKJsBvh07RaS+/By91NNE0Wh0DN19/hWPlBOU8tAfOtbZoVSV4xXeoKoxgPx0rx2y+y+8660XtE7jzg== dependencies: methods "^1.1.2" - superagent "^3.8.3" + superagent "^7.1.0" supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" diff --git a/packages/types/src/sdk/locks.ts b/packages/types/src/sdk/locks.ts index e6809319b1..d868691891 100644 --- a/packages/types/src/sdk/locks.ts +++ b/packages/types/src/sdk/locks.ts @@ -12,6 +12,7 @@ export enum LockName { MIGRATIONS = "migrations", TRIGGER_QUOTA = "trigger_quota", SYNC_ACCOUNT_LICENSE = "sync_account_license", + UPDATE_TENANTS_DOC = "update_tenants_doc", } export interface LockOptions { diff --git a/packages/worker/package.json b/packages/worker/package.json index 9a55f527df..061a44c958 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -22,7 +22,7 @@ "build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION", "dev:stack:init": "node ./scripts/dev/manage.js init", "dev:builder": "npm run dev:stack:init && nodemon", - "test": "jest --coverage --maxWorkers=2", + "test": "jest --coverage", "test:watch": "jest --watch", "env:multi:enable": "node scripts/multiTenancy.js enable", "env:multi:disable": "node scripts/multiTenancy.js disable", @@ -73,7 +73,7 @@ "@swc/core": "^1.3.25", "@swc/jest": "^0.2.24", "@trendyol/jest-testcontainers": "^2.1.1", - "@types/jest": "26.0.23", + "@types/jest": "28.1.1", "@types/jsonwebtoken": "8.5.1", "@types/koa": "2.13.4", "@types/koa__router": "8.0.8", @@ -81,6 +81,7 @@ "@types/node-fetch": "2.6.1", "@types/pouchdb": "6.4.0", "@types/server-destroy": "1.0.1", + "@types/supertest": "2.0.12", "@types/uuid": "8.3.4", "@typescript-eslint/parser": "5.45.0", "copyfiles": "2.4.1", diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 817480151d..43ec23eade 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -1,5 +1,5 @@ import { checkInviteCode } from "../../../utilities/redis" -import sdk from "../../../sdk" +import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { BulkUserRequest, @@ -8,6 +8,7 @@ import { CreateAdminUserRequest, InviteUserRequest, InviteUsersRequest, + MigrationType, SearchUsersRequest, User, } from "@budibase/types" @@ -16,7 +17,9 @@ import { cache, errors, events, + migrations, tenancy, + platform, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" @@ -25,7 +28,7 @@ const MAX_USERS_UPLOAD_LIMIT = 1000 export const save = async (ctx: any) => { try { const currentUserId = ctx.user._id - ctx.body = await sdk.users.save(ctx.request.body, { currentUserId }) + ctx.body = await userSdk.save(ctx.request.body, { currentUserId }) } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -35,7 +38,7 @@ const bulkDelete = async (userIds: string[], currentUserId: string) => { if (userIds?.indexOf(currentUserId) !== -1) { throw new Error("Unable to delete self.") } - return await sdk.users.bulkDelete(userIds) + return await userSdk.bulkDelete(userIds) } const bulkCreate = async (users: User[], groupIds: string[]) => { @@ -44,7 +47,7 @@ const bulkCreate = async (users: User[], groupIds: string[]) => { "Max limit for upload is 1000 users. Please reduce file size and try again." ) } - return await sdk.users.bulkCreate(users, groupIds) + return await userSdk.bulkCreate(users, groupIds) } export const bulkUpdate = async (ctx: any) => { @@ -71,16 +74,26 @@ const parseBooleanParam = (param: any) => { export const adminUser = async (ctx: any) => { const { email, password, tenantId } = ctx.request .body as CreateAdminUserRequest + + if (await platform.tenants.exists(tenantId)) { + ctx.throw(403, "Organisation already exists.") + } + + if (env.MULTI_TENANCY) { + // store the new tenant record in the platform db + await platform.tenants.addTenant(tenantId) + await migrations.backPopulateMigrations({ + type: MigrationType.GLOBAL, + tenantId, + }) + } + await tenancy.doInTenant(tenantId, async () => { // account portal sends a pre-hashed password - honour param to prevent double hashing const hashPassword = parseBooleanParam(ctx.request.query.hashPassword) // account portal sends no password for SSO users const requirePassword = parseBooleanParam(ctx.request.query.requirePassword) - if (await tenancy.doesTenantExist(tenantId)) { - ctx.throw(403, "Organisation already exists.") - } - const userExists = await checkAnyUserExists() if (userExists) { ctx.throw( @@ -106,7 +119,7 @@ export const adminUser = async (ctx: any) => { // always bust checklist beforehand, if an error occurs but can proceed, don't get // stuck in a cycle await cache.bustCache(cache.CacheKey.CHECKLIST) - const finalUser = await sdk.users.save(user, { + const finalUser = await userSdk.save(user, { hashPassword, requirePassword, }) @@ -128,7 +141,7 @@ export const adminUser = async (ctx: any) => { export const countByApp = async (ctx: any) => { const appId = ctx.params.appId try { - ctx.body = await sdk.users.countUsersByApp(appId) + ctx.body = await userSdk.countUsersByApp(appId) } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -140,7 +153,7 @@ export const destroy = async (ctx: any) => { ctx.throw(400, "Unable to delete self.") } - await sdk.users.destroy(id, ctx.user) + await userSdk.destroy(id, ctx.user) ctx.body = { message: `User ${id} deleted.`, @@ -149,7 +162,7 @@ export const destroy = async (ctx: any) => { export const search = async (ctx: any) => { const body = ctx.request.body as SearchUsersRequest - const paginated = await sdk.users.paginatedUsers(body) + const paginated = await userSdk.paginatedUsers(body) // user hashed password shouldn't ever be returned for (let user of paginated.data) { if (user) { @@ -161,7 +174,7 @@ export const search = async (ctx: any) => { // called internally by app server user fetch export const fetch = async (ctx: any) => { - const all = await sdk.users.allUsers() + const all = await userSdk.allUsers() // user hashed password shouldn't ever be returned for (let user of all) { if (user) { @@ -173,12 +186,12 @@ export const fetch = async (ctx: any) => { // called internally by app server user find export const find = async (ctx: any) => { - ctx.body = await sdk.users.getUser(ctx.params.id) + ctx.body = await userSdk.getUser(ctx.params.id) } export const tenantUserLookup = async (ctx: any) => { const id = ctx.params.id - const user = await sdk.users.getPlatformUser(id) + const user = await userSdk.getPlatformUser(id) if (user) { ctx.body = user } else { @@ -188,7 +201,7 @@ export const tenantUserLookup = async (ctx: any) => { export const invite = async (ctx: any) => { const request = ctx.request.body as InviteUserRequest - const response = await sdk.users.invite([request]) + const response = await userSdk.invite([request]) // explicitly throw for single user invite if (response.unsuccessful.length) { @@ -207,7 +220,7 @@ export const invite = async (ctx: any) => { export const inviteMultiple = async (ctx: any) => { const request = ctx.request.body as InviteUsersRequest - ctx.body = await sdk.users.invite(request) + ctx.body = await userSdk.invite(request) } export const checkInvite = async (ctx: any) => { @@ -229,7 +242,7 @@ export const inviteAccept = async (ctx: any) => { // info is an extension of the user object that was stored by global const { email, info }: any = await checkInviteCode(inviteCode) ctx.body = await tenancy.doInTenant(info.tenantId, async () => { - const saved = await sdk.users.save({ + const saved = await userSdk.save({ firstName, lastName, password, diff --git a/packages/worker/src/api/controllers/system/tenants.ts b/packages/worker/src/api/controllers/system/tenants.ts index 6916049534..151507358f 100644 --- a/packages/worker/src/api/controllers/system/tenants.ts +++ b/packages/worker/src/api/controllers/system/tenants.ts @@ -1,8 +1,7 @@ -import { BBContext } from "@budibase/types" -import { deprovisioning } from "@budibase/backend-core" -import { quotas } from "@budibase/pro" +import { UserCtx } from "@budibase/types" +import * as tenantSdk from "../../../sdk/tenants" -const _delete = async (ctx: BBContext) => { +export async function destroy(ctx: UserCtx) { const user = ctx.user! const tenantId = ctx.params.tenantId @@ -11,13 +10,10 @@ const _delete = async (ctx: BBContext) => { } try { - await quotas.bustCache() - await deprovisioning.deleteTenant(tenantId) + await tenantSdk.deleteTenant(tenantId) ctx.status = 204 } catch (err) { ctx.log.error(err) throw err } } - -export { _delete as delete } diff --git a/packages/worker/src/api/routes/global/tests/configs.spec.ts b/packages/worker/src/api/routes/global/tests/configs.spec.ts index ee27c4d451..39ad74d295 100644 --- a/packages/worker/src/api/routes/global/tests/configs.spec.ts +++ b/packages/worker/src/api/routes/global/tests/configs.spec.ts @@ -2,7 +2,7 @@ jest.mock("nodemailer") import { TestConfiguration, structures, mocks } from "../../../../tests" mocks.email.mock() -import { Config, context, events } from "@budibase/backend-core" +import { Config, events } from "@budibase/backend-core" describe("configs", () => { const config = new TestConfiguration() @@ -113,64 +113,56 @@ describe("configs", () => { describe("create", () => { it("should create activated OIDC config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await saveOIDCConfig() - expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) - expect(events.auth.SSODeactivated).not.toBeCalled() - expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) - }) + await saveOIDCConfig() + expect(events.auth.SSOCreated).toBeCalledTimes(1) + expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSODeactivated).not.toBeCalled() + expect(events.auth.SSOActivated).toBeCalledTimes(1) + expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) + await config.deleteConfig(Config.OIDC) }) it("should create deactivated OIDC config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await saveOIDCConfig({ activated: false }) - expect(events.auth.SSOCreated).toBeCalledTimes(1) - expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) - expect(events.auth.SSOActivated).not.toBeCalled() - expect(events.auth.SSODeactivated).not.toBeCalled() - await config.deleteConfig(Config.OIDC) - }) + await saveOIDCConfig({ activated: false }) + expect(events.auth.SSOCreated).toBeCalledTimes(1) + expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOActivated).not.toBeCalled() + expect(events.auth.SSODeactivated).not.toBeCalled() + await config.deleteConfig(Config.OIDC) }) }) describe("update", () => { it("should update OIDC config to deactivated", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const oidcConf = await saveOIDCConfig() - jest.clearAllMocks() - await saveOIDCConfig( - { ...oidcConf.config.configs[0], activated: false }, - oidcConf._id, - oidcConf._rev - ) - expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) - expect(events.auth.SSOActivated).not.toBeCalled() - expect(events.auth.SSODeactivated).toBeCalledTimes(1) - expect(events.auth.SSODeactivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) - }) + const oidcConf = await saveOIDCConfig() + jest.clearAllMocks() + await saveOIDCConfig( + { ...oidcConf.config.configs[0], activated: false }, + oidcConf._id, + oidcConf._rev + ) + expect(events.auth.SSOUpdated).toBeCalledTimes(1) + expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSOActivated).not.toBeCalled() + expect(events.auth.SSODeactivated).toBeCalledTimes(1) + expect(events.auth.SSODeactivated).toBeCalledWith(Config.OIDC) + await config.deleteConfig(Config.OIDC) }) it("should update OIDC config to activated", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const oidcConf = await saveOIDCConfig({ activated: false }) - jest.clearAllMocks() - await saveOIDCConfig( - { ...oidcConf.config.configs[0], activated: true }, - oidcConf._id, - oidcConf._rev - ) - expect(events.auth.SSOUpdated).toBeCalledTimes(1) - expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) - expect(events.auth.SSODeactivated).not.toBeCalled() - expect(events.auth.SSOActivated).toBeCalledTimes(1) - expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) - await config.deleteConfig(Config.OIDC) - }) + const oidcConf = await saveOIDCConfig({ activated: false }) + jest.clearAllMocks() + await saveOIDCConfig( + { ...oidcConf.config.configs[0], activated: true }, + oidcConf._id, + oidcConf._rev + ) + expect(events.auth.SSOUpdated).toBeCalledTimes(1) + expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC) + expect(events.auth.SSODeactivated).not.toBeCalled() + expect(events.auth.SSOActivated).toBeCalledTimes(1) + expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC) + await config.deleteConfig(Config.OIDC) }) }) }) @@ -187,26 +179,22 @@ describe("configs", () => { describe("create", () => { it("should create SMTP config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await config.deleteConfig(Config.SMTP) - await saveSMTPConfig() - expect(events.email.SMTPUpdated).not.toBeCalled() - expect(events.email.SMTPCreated).toBeCalledTimes(1) - await config.deleteConfig(Config.SMTP) - }) + await config.deleteConfig(Config.SMTP) + await saveSMTPConfig() + expect(events.email.SMTPUpdated).not.toBeCalled() + expect(events.email.SMTPCreated).toBeCalledTimes(1) + await config.deleteConfig(Config.SMTP) }) }) describe("update", () => { it("should update SMTP config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const smtpConf = await saveSMTPConfig() - jest.clearAllMocks() - await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev) - expect(events.email.SMTPCreated).not.toBeCalled() - expect(events.email.SMTPUpdated).toBeCalledTimes(1) - await config.deleteConfig(Config.SMTP) - }) + const smtpConf = await saveSMTPConfig() + jest.clearAllMocks() + await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev) + expect(events.email.SMTPCreated).not.toBeCalled() + expect(events.email.SMTPUpdated).toBeCalledTimes(1) + await config.deleteConfig(Config.SMTP) }) }) }) @@ -223,73 +211,65 @@ describe("configs", () => { describe("create", () => { it("should create settings config with default settings", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await config.deleteConfig(Config.SETTINGS) + await config.deleteConfig(Config.SETTINGS) - await saveSettingsConfig() + await saveSettingsConfig() - expect(events.org.nameUpdated).not.toBeCalled() - expect(events.org.logoUpdated).not.toBeCalled() - expect(events.org.platformURLUpdated).not.toBeCalled() - }) + expect(events.org.nameUpdated).not.toBeCalled() + expect(events.org.logoUpdated).not.toBeCalled() + expect(events.org.platformURLUpdated).not.toBeCalled() }) it("should create settings config with non-default settings", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - config.modeSelf() - await config.deleteConfig(Config.SETTINGS) - const conf = { - company: "acme", - logoUrl: "http://example.com", - platformUrl: "http://example.com", - } + config.selfHosted() + await config.deleteConfig(Config.SETTINGS) + const conf = { + company: "acme", + logoUrl: "http://example.com", + platformUrl: "http://example.com", + } - await saveSettingsConfig(conf) + await saveSettingsConfig(conf) - expect(events.org.nameUpdated).toBeCalledTimes(1) - expect(events.org.logoUpdated).toBeCalledTimes(1) - expect(events.org.platformURLUpdated).toBeCalledTimes(1) - config.modeCloud() - }) + expect(events.org.nameUpdated).toBeCalledTimes(1) + expect(events.org.logoUpdated).toBeCalledTimes(1) + expect(events.org.platformURLUpdated).toBeCalledTimes(1) + config.cloudHosted() }) }) describe("update", () => { it("should update settings config", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - config.modeSelf() - await config.deleteConfig(Config.SETTINGS) - const settingsConfig = await saveSettingsConfig() - settingsConfig.config.company = "acme" - settingsConfig.config.logoUrl = "http://example.com" - settingsConfig.config.platformUrl = "http://example.com" + config.selfHosted() + await config.deleteConfig(Config.SETTINGS) + const settingsConfig = await saveSettingsConfig() + settingsConfig.config.company = "acme" + settingsConfig.config.logoUrl = "http://example.com" + settingsConfig.config.platformUrl = "http://example.com" - await saveSettingsConfig( - settingsConfig.config, - settingsConfig._id, - settingsConfig._rev - ) + await saveSettingsConfig( + settingsConfig.config, + settingsConfig._id, + settingsConfig._rev + ) - expect(events.org.nameUpdated).toBeCalledTimes(1) - expect(events.org.logoUpdated).toBeCalledTimes(1) - expect(events.org.platformURLUpdated).toBeCalledTimes(1) - config.modeCloud() - }) + expect(events.org.nameUpdated).toBeCalledTimes(1) + expect(events.org.logoUpdated).toBeCalledTimes(1) + expect(events.org.platformURLUpdated).toBeCalledTimes(1) + config.cloudHosted() }) }) }) }) it("should return the correct checklist status based on the state of the budibase installation", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - await config.saveSmtpConfig() + await config.saveSmtpConfig() - const res = await config.api.configs.getConfigChecklist() - const checklist = res.body + const res = await config.api.configs.getConfigChecklist() + const checklist = res.body - expect(checklist.apps.checked).toBeFalsy() - expect(checklist.smtp.checked).toBeTruthy() - expect(checklist.adminUser.checked).toBeTruthy() - }) + expect(checklist.apps.checked).toBeFalsy() + expect(checklist.smtp.checked).toBeTruthy() + expect(checklist.adminUser.checked).toBeTruthy() }) }) diff --git a/packages/worker/src/api/routes/global/tests/roles.spec.ts b/packages/worker/src/api/routes/global/tests/roles.spec.ts index 622a643f25..477ccaf94c 100644 --- a/packages/worker/src/api/routes/global/tests/roles.spec.ts +++ b/packages/worker/src/api/routes/global/tests/roles.spec.ts @@ -32,6 +32,7 @@ async function addAppMetadata() { describe("/api/global/roles", () => { const config = new TestConfiguration() + const role = new roles.Role( db.generateRoleID("newRole"), roles.BUILTIN_ROLE_IDS.BASIC, @@ -43,13 +44,13 @@ describe("/api/global/roles", () => { }) beforeEach(async () => { - appId = db.generateAppID() + appId = db.generateAppID(config.tenantId) appDb = db.getDB(appId) const mockAppDB = context.getAppDB as Mock mockAppDB.mockReturnValue(appDb) await addAppMetadata() - appDb.put(role) + await appDb.put(role) }) afterAll(async () => { diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index a8b07ec815..31ef1d9b0c 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -3,7 +3,9 @@ import { InviteUsersResponse, User } from "@budibase/types" jest.mock("nodemailer") import { TestConfiguration, mocks, structures } from "../../../../tests" const sendMailMock = mocks.email.mock() -import { context, events, tenancy } from "@budibase/backend-core" +import { events, tenancy, accounts as _accounts } from "@budibase/backend-core" + +const accounts = jest.mocked(_accounts) describe("/api/global/users", () => { const config = new TestConfiguration() @@ -20,26 +22,24 @@ describe("/api/global/users", () => { jest.clearAllMocks() }) - describe("invite", () => { + describe("POST /api/global/users/invite", () => { it("should be able to generate an invitation", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const email = structures.users.newEmail() - const { code, res } = await config.api.users.sendUserInvite( - sendMailMock, - email - ) + const email = structures.users.newEmail() + const { code, res } = await config.api.users.sendUserInvite( + sendMailMock, + email + ) - expect(res.body).toEqual({ message: "Invitation has been sent." }) - expect(sendMailMock).toHaveBeenCalled() - expect(code).toBeDefined() - expect(events.user.invited).toBeCalledTimes(1) - }) + expect(res.body).toEqual({ message: "Invitation has been sent." }) + expect(sendMailMock).toHaveBeenCalled() + expect(code).toBeDefined() + expect(events.user.invited).toBeCalledTimes(1) }) it("should not be able to generate an invitation for existing user", async () => { const { code, res } = await config.api.users.sendUserInvite( sendMailMock, - config.defaultUser!.email, + config.user!.email, 400 ) @@ -50,26 +50,24 @@ describe("/api/global/users", () => { }) it("should be able to create new user from invite", async () => { - await context.doInTenant(config.tenant1User!.tenantId, async () => { - const email = structures.users.newEmail() - const { code } = await config.api.users.sendUserInvite( - sendMailMock, - email - ) + const email = structures.users.newEmail() + const { code } = await config.api.users.sendUserInvite( + sendMailMock, + email + ) - const res = await config.api.users.acceptInvite(code) + const res = await config.api.users.acceptInvite(code) - expect(res.body._id).toBeDefined() - const user = await config.getUser(email) - expect(user).toBeDefined() - expect(user._id).toEqual(res.body._id) - expect(events.user.inviteAccepted).toBeCalledTimes(1) - expect(events.user.inviteAccepted).toBeCalledWith(user) - }) + expect(res.body._id).toBeDefined() + const user = await config.getUser(email) + expect(user).toBeDefined() + expect(user._id).toEqual(res.body._id) + expect(events.user.inviteAccepted).toBeCalledTimes(1) + expect(events.user.inviteAccepted).toBeCalledWith(user) }) }) - describe("inviteMultiple", () => { + describe("POST /api/global/users/multi/invite", () => { it("should be able to generate an invitation", async () => { const newUserInvite = () => ({ email: structures.users.newEmail(), @@ -87,7 +85,7 @@ describe("/api/global/users", () => { }) it("should not be able to generate an invitation for existing user", async () => { - const request = [{ email: config.defaultUser!.email, userInfo: {} }] + const request = [{ email: config.user!.email, userInfo: {} }] const res = await config.api.users.sendMultiUserInvite(request) @@ -100,7 +98,7 @@ describe("/api/global/users", () => { }) }) - describe("bulk (create)", () => { + describe("POST /api/global/users/bulk", () => { it("should ignore users existing in the same tenant", async () => { const user = await config.createUser() jest.clearAllMocks() @@ -163,7 +161,7 @@ describe("/api/global/users", () => { }) }) - describe("create", () => { + describe("POST /api/global/users", () => { it("should be able to create a basic user", async () => { const user = structures.users.user() @@ -242,7 +240,7 @@ describe("/api/global/users", () => { it("should not be able to create user with the same email as an account", async () => { const user = structures.users.user() const account = structures.accounts.cloudAccount() - mocks.accounts.getAccount.mockReturnValueOnce(account) + accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.saveUser(user, 400) @@ -283,7 +281,7 @@ describe("/api/global/users", () => { }) }) - describe("update", () => { + describe("POST /api/global/users (update)", () => { it("should be able to update a basic user", async () => { const user = await config.createUser() jest.clearAllMocks() @@ -298,7 +296,7 @@ describe("/api/global/users", () => { }) it("should not allow a user to update their own admin/builder status", async () => { - const user = (await config.api.users.getUser(config.defaultUser?._id!)) + const user = (await config.api.users.getUser(config.user?._id!)) .body as User await config.api.users.saveUser({ ...user, @@ -468,9 +466,9 @@ describe("/api/global/users", () => { }) }) - describe("bulk (delete)", () => { + describe("POST /api/global/users/bulk (delete)", () => { it("should not be able to bulk delete current user", async () => { - const user = await config.defaultUser! + const user = await config.user! const response = await config.api.users.bulkDeleteUsers([user._id!], 400) @@ -482,7 +480,7 @@ describe("/api/global/users", () => { const user = await config.createUser() const account = structures.accounts.cloudAccount() account.budibaseUserId = user._id! - mocks.accounts.getAccountByTenantId.mockReturnValue(account) + accounts.getAccountByTenantId.mockReturnValue(Promise.resolve(account)) const response = await config.api.users.bulkDeleteUsers([user._id!]) @@ -497,7 +495,7 @@ describe("/api/global/users", () => { it("should be able to bulk delete users", async () => { const account = structures.accounts.cloudAccount() - mocks.accounts.getAccountByTenantId.mockReturnValue(account) + accounts.getAccountByTenantId.mockReturnValue(Promise.resolve(account)) const builder = structures.users.builderUser() const admin = structures.users.adminUser() @@ -521,7 +519,7 @@ describe("/api/global/users", () => { }) }) - describe("destroy", () => { + describe("DELETE /api/global/users/:userId", () => { it("should be able to destroy a basic user", async () => { const user = await config.createUser() jest.clearAllMocks() @@ -558,7 +556,7 @@ describe("/api/global/users", () => { it("should not be able to destroy account owner", async () => { const user = await config.createUser() const account = structures.accounts.cloudAccount() - mocks.accounts.getAccount.mockReturnValueOnce(account) + accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.deleteUser(user._id!, 400) @@ -566,10 +564,10 @@ describe("/api/global/users", () => { }) it("should not be able to destroy account owner as account owner", async () => { - const user = await config.defaultUser! + const user = await config.user! const account = structures.accounts.cloudAccount() account.email = user.email - mocks.accounts.getAccount.mockReturnValueOnce(account) + accounts.getAccount.mockReturnValueOnce(Promise.resolve(account)) const response = await config.api.users.deleteUser(user._id!, 400) diff --git a/packages/worker/src/api/routes/system/tenants.ts b/packages/worker/src/api/routes/system/tenants.ts index 111cfc5819..234459cfdc 100644 --- a/packages/worker/src/api/routes/system/tenants.ts +++ b/packages/worker/src/api/routes/system/tenants.ts @@ -7,7 +7,7 @@ const router: Router = new Router() router.delete( "/api/system/tenants/:tenantId", middleware.adminOnly, - controller.delete + controller.destroy ) export default router diff --git a/packages/worker/src/api/routes/system/tests/restore.spec.ts b/packages/worker/src/api/routes/system/tests/restore.spec.ts index 4dd973270f..2130fd8dde 100644 --- a/packages/worker/src/api/routes/system/tests/restore.spec.ts +++ b/packages/worker/src/api/routes/system/tests/restore.spec.ts @@ -25,12 +25,12 @@ describe("/api/system/restore", () => { }) it("restores in self host", async () => { - config.modeSelf() + config.selfHosted() const res = await config.api.restore.restored() expect(res.body).toEqual({ message: "System prepared after restore.", }) - config.modeCloud() + config.cloudHosted() }) }) }) diff --git a/packages/worker/src/api/routes/system/tests/status.spec.ts b/packages/worker/src/api/routes/system/tests/status.spec.ts index afd3f8ac46..fe0ff13551 100644 --- a/packages/worker/src/api/routes/system/tests/status.spec.ts +++ b/packages/worker/src/api/routes/system/tests/status.spec.ts @@ -1,6 +1,6 @@ import { TestConfiguration } from "../../../../tests" -import { accounts } from "@budibase/backend-core" -import { mocks } from "@budibase/backend-core/tests" +import { accounts as _accounts } from "@budibase/backend-core" +const accounts = jest.mocked(_accounts) describe("/api/system/status", () => { const config = new TestConfiguration() @@ -19,7 +19,7 @@ describe("/api/system/status", () => { describe("GET /api/system/status", () => { it("returns status in self host", async () => { - config.modeSelf() + config.selfHosted() const res = await config.api.status.getStatus() expect(res.body).toEqual({ health: { @@ -27,7 +27,7 @@ describe("/api/system/status", () => { }, }) expect(accounts.getStatus).toBeCalledTimes(0) - config.modeCloud() + config.cloudHosted() }) it("returns status in cloud", async () => { @@ -37,7 +37,7 @@ describe("/api/system/status", () => { }, } - mocks.accounts.getStatus.mockReturnValueOnce(value) + accounts.getStatus.mockReturnValueOnce(Promise.resolve(value)) const res = await config.api.status.getStatus() diff --git a/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts b/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts index 941791fe93..1ffcc762c4 100644 --- a/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts +++ b/packages/worker/src/migrations/functions/globalInfoSyncUsers.ts @@ -1,5 +1,6 @@ import { User } from "@budibase/types" -import sdk from "../../sdk" +import * as usersSdk from "../../sdk/users" +import { platform } from "@budibase/backend-core" /** * Date: @@ -9,11 +10,11 @@ import sdk from "../../sdk" * Re-sync the global-db users to the global-info db users */ export const run = async (globalDb: any) => { - const users = (await sdk.users.allUsers()) as User[] + const users = (await usersSdk.allUsers()) as User[] const promises = [] for (let user of users) { promises.push( - sdk.users.addTenant(user.tenantId, user._id as string, user.email) + platform.users.addUser(user.tenantId, user._id as string, user.email) ) } await Promise.all(promises) diff --git a/packages/worker/src/sdk/tenants/index.ts b/packages/worker/src/sdk/tenants/index.ts new file mode 100644 index 0000000000..19b7ea6615 --- /dev/null +++ b/packages/worker/src/sdk/tenants/index.ts @@ -0,0 +1 @@ +export * from "./tenants" diff --git a/packages/worker/src/sdk/tenants/tenants.ts b/packages/worker/src/sdk/tenants/tenants.ts new file mode 100644 index 0000000000..3ba9c3f3a7 --- /dev/null +++ b/packages/worker/src/sdk/tenants/tenants.ts @@ -0,0 +1,76 @@ +import { App } from "@budibase/types" +import { tenancy, db as dbCore, platform } from "@budibase/backend-core" +import { quotas } from "@budibase/pro" + +export async function deleteTenant(tenantId: string) { + await quotas.bustCache() + await platform.tenants.removeTenant(tenantId) + await removeGlobalDB(tenantId) + await removeTenantUsers(tenantId) + await removeTenantApps(tenantId) +} + +async function removeGlobalDB(tenantId: string) { + try { + const db = tenancy.getTenantDB(tenantId) + await db.destroy() + } catch (err) { + console.error(`Error removing tenant ${tenantId} users from info db`, err) + throw err + } +} + +async function removeTenantApps(tenantId: string) { + try { + const apps = (await dbCore.getAllApps({ all: true })) as App[] + const destroyPromises = apps.map(app => { + const db = dbCore.getDB(app.appId) + return db.destroy() + }) + await Promise.allSettled(destroyPromises) + } catch (err) { + console.error(`Error removing tenant ${tenantId} apps`, err) + throw err + } +} + +function getTenantUsers(tenantId: string) { + const db = tenancy.getTenantDB(tenantId) + + return db.allDocs( + dbCore.getGlobalUserParams(null, { + include_docs: true, + }) + ) +} + +async function removeTenantUsers(tenantId: string) { + try { + const allUsers = await getTenantUsers(tenantId) + const allEmails = allUsers.rows.map((row: any) => row.doc.email) + + // get the id and email doc ids + let keys = allUsers.rows.map((row: any) => row.id) + keys = keys.concat(allEmails) + + const platformDb = platform.getPlatformDB() + + // retrieve the docs + const userDocs = await platformDb.allDocs({ + keys, + include_docs: true, + }) + + // delete the docs + const toDelete = userDocs.rows.map((row: any) => { + return { + ...row.doc, + _deleted: true, + } + }) + await platformDb.bulkDocs(toDelete) + } catch (err) { + console.error(`Error removing tenant ${tenantId} users from info db`, err) + throw err + } +} diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 8410d0b2e0..330cdfde6d 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -549,7 +549,7 @@ export const bulkDelete = async ( export const destroy = async (id: string, currentUser: any) => { const db = tenancy.getGlobalDB() - const dbUser = await db.get(id) + const dbUser = (await db.get(id)) as User const userId = dbUser._id as string if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 0cc1e61e65..7d075e7fef 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -15,61 +15,29 @@ const supertest = require("supertest") import { Config } from "../constants" import { users, - tenancy, + context, sessions, auth, constants, env as coreEnv, - DEFAULT_TENANT_ID, } from "@budibase/backend-core" -import structures, { TENANT_ID, CSRF_TOKEN } from "./structures" +import structures, { CSRF_TOKEN } from "./structures" import { CreateUserResponse, User, AuthToken } from "@budibase/types" import API from "./api" -import sdk from "../sdk" - -enum Mode { - CLOUD = "cloud", - SELF = "self", -} - -async function retry any>( - fn: T, - maxTry: number = 5, - retryCount = 1 -): Promise>> { - const currRetry = typeof retryCount === "number" ? retryCount : 1 - try { - const result = await fn() - return result - } catch (e) { - console.log(`Retry ${currRetry} failed.`) - if (currRetry > maxTry) { - console.log(`All ${maxTry} retry attempts exhausted`) - throw e - } - return retry(fn, maxTry, currRetry + 1) - } -} class TestConfiguration { server: any request: any api: API - defaultUser?: User - tenant1User?: User - #tenantId?: string + tenantId: string + user?: User + userPassword = "test" - constructor( - opts: { openServer: boolean; mode: Mode } = { - openServer: true, - mode: Mode.CLOUD, - } - ) { - if (opts.mode === Mode.CLOUD) { - this.modeCloud() - } else if (opts.mode === Mode.SELF) { - this.modeSelf() - } + constructor(opts: { openServer: boolean } = { openServer: true }) { + // default to cloud hosting + this.cloudHosted() + + this.tenantId = structures.tenant.id() if (opts.openServer) { env.PORT = "0" // random port @@ -85,26 +53,19 @@ class TestConfiguration { return this.request } - // MODES - - setMultiTenancy = (value: boolean) => { - env._set("MULTI_TENANCY", value) - coreEnv._set("MULTI_TENANCY", value) - } + // HOSTING setSelfHosted = (value: boolean) => { env._set("SELF_HOSTED", value) coreEnv._set("SELF_HOSTED", value) } - modeCloud = () => { + cloudHosted = () => { this.setSelfHosted(false) - this.setMultiTenancy(true) } - modeSelf = () => { + selfHosted = () => { this.setSelfHosted(true) - this.setMultiTenancy(false) } // UTILS @@ -125,7 +86,7 @@ class TestConfiguration { if (params) { request.params = params } - await tenancy.doInTenant(this.getTenantId(), () => { + await context.doInTenant(this.getTenantId(), () => { return controlFunc(request) }) return request.body @@ -135,18 +96,10 @@ class TestConfiguration { async beforeAll() { try { - this.#tenantId = structures.tenant.id() - - // Running tests in parallel causes issues creating the globaldb twice. This ensures the db is properly created before starting - await retry(async () => await this.createDefaultUser()) - await this.createSession(this.defaultUser!) - - await tenancy.doInTenant(this.#tenantId, async () => { - await this.createTenant1User() - await this.createSession(this.tenant1User!) - }) + await this.createDefaultUser() + await this.createSession(this.user!) } catch (e: any) { - console.log(e) + console.error(e) throw new Error(e.message) } } @@ -159,12 +112,16 @@ class TestConfiguration { // TENANCY + doInTenant(task: any) { + return context.doInTenant(this.tenantId, () => { + return task() + }) + } + createTenant = async (): Promise => { // create user / new tenant const res = await this.api.users.createAdminUser() - await sdk.users.addTenant(res.tenantId, res.userId, res.email) - // return the created user const userRes = await this.api.users.getUser(res.userId, { headers: { @@ -182,9 +139,9 @@ class TestConfiguration { getTenantId() { try { - return tenancy.getTenantId() - } catch (e: any) { - return DEFAULT_TENANT_ID + return context.getTenantId() + } catch (e) { + return this.tenantId! } } @@ -232,14 +189,11 @@ class TestConfiguration { } defaultHeaders() { - const tenantId = this.getTenantId() - if (tenantId === TENANT_ID) { - return this.authHeaders(this.defaultUser!) - } else if (tenantId === this.getTenantId()) { - return this.authHeaders(this.tenant1User!) - } else { - throw new Error("could not determine auth headers to use") - } + return this.authHeaders(this.user!) + } + + tenantIdHeaders() { + return { [constants.Header.TENANT_ID]: this.tenantId } } internalAPIHeaders() { @@ -254,20 +208,15 @@ class TestConfiguration { async createDefaultUser() { const user = structures.users.adminUser({ - password: "test", + password: this.userPassword, }) - this.defaultUser = await this.createUser(user) - } - - async createTenant1User() { - const user = structures.users.adminUser({ - password: "test", + await context.doInTenant(this.tenantId!, async () => { + this.user = await this.createUser(user) }) - this.tenant1User = await this.createUser(user) } async getUser(email: string): Promise { - return tenancy.doInTenant(this.getTenantId(), () => { + return context.doInTenant(this.getTenantId(), () => { return users.getGlobalUserByEmail(email) }) } diff --git a/packages/worker/src/tests/api/base.ts b/packages/worker/src/tests/api/base.ts index c1263ed5cb..460d61e70a 100644 --- a/packages/worker/src/tests/api/base.ts +++ b/packages/worker/src/tests/api/base.ts @@ -1,4 +1,5 @@ import TestConfiguration from "../TestConfiguration" +import { SuperTest, Test } from "supertest" export interface TestAPIOpts { headers?: any @@ -7,7 +8,7 @@ export interface TestAPIOpts { export abstract class TestAPI { config: TestConfiguration - request: any + request: SuperTest protected constructor(config: TestConfiguration) { this.config = config diff --git a/packages/worker/src/tests/api/restore.ts b/packages/worker/src/tests/api/restore.ts index 6069c20185..c6a646317d 100644 --- a/packages/worker/src/tests/api/restore.ts +++ b/packages/worker/src/tests/api/restore.ts @@ -9,6 +9,7 @@ export class RestoreAPI extends TestAPI { restored = (opts?: TestAPIOpts) => { return this.request .post(`/api/system/restored`) + .set(this.config.tenantIdHeaders()) .expect(opts?.status ? opts.status : 200) } } diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index e7a02fce1f..b03e9d9d7c 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -800,17 +800,6 @@ slash "^3.0.0" write-file-atomic "^4.0.1" -"@jest/types@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" - integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^15.0.0" - chalk "^4.0.0" - "@jest/types@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" @@ -1317,6 +1306,11 @@ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3" integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA== +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/cookies@*": version "0.7.7" resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81" @@ -1413,13 +1407,13 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@26.0.23": - version "26.0.23" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7" - integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA== +"@types/jest@28.1.1": + version "28.1.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-28.1.1.tgz#8c9ba63702a11f8c386ee211280e8b68cb093cd1" + integrity sha512-C2p7yqleUKtCkVjlOur9BWVA4HgUQmEj/HWCt5WzZ5mLXrWnyIfl0wGuArc+kBXsy0ZZfLp+7dywB4HtSVYGVA== dependencies: - jest-diff "^26.0.0" - pretty-format "^26.0.0" + jest-matcher-utils "^27.0.0" + pretty-format "^27.0.0" "@types/json-buffer@~3.0.0": version "3.0.0" @@ -1689,6 +1683,21 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/superagent@*": + version "4.1.16" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.16.tgz#12c9c16f232f9d89beab91d69368f96ce8e2d881" + integrity sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + "@types/tough-cookie@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" @@ -1704,13 +1713,6 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== -"@types/yargs@^15.0.0": - version "15.0.14" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" - integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== - dependencies: - "@types/yargs-parser" "*" - "@types/yargs@^16.0.0": version "16.0.5" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3" @@ -1898,7 +1900,7 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== -ansi-regex@^5.0.0, ansi-regex@^5.0.1: +ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -2999,10 +3001,10 @@ dezalgo@1.0.3: asap "^2.0.0" wrappy "1" -diff-sequences@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" - integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== diff-sequences@^28.1.1: version "28.1.1" @@ -3066,11 +3068,6 @@ dotenv@16.0.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== -dotenv@8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" - integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== - double-ended-queue@2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" @@ -4727,15 +4724,15 @@ jest-config@^28.1.3: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^26.0.0: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" - integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: chalk "^4.0.0" - diff-sequences "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" jest-diff@^28.1.3: version "28.1.3" @@ -4777,10 +4774,10 @@ jest-environment-node@^28.1.3: jest-mock "^28.1.3" jest-util "^28.1.3" -jest-get-type@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" - integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== jest-get-type@^28.0.2: version "28.0.2" @@ -4814,6 +4811,16 @@ jest-leak-detector@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" +jest-matcher-utils@^27.0.0: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== + dependencies: + chalk "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + jest-matcher-utils@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz#5a77f1c129dd5ba3b4d7fc20728806c78893146e" @@ -6624,14 +6631,13 @@ prettier@2.3.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== -pretty-format@^26.0.0, pretty-format@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" - integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== +pretty-format@^27.0.0, pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== dependencies: - "@jest/types" "^26.6.2" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" react-is "^17.0.1" pretty-format@^28.1.3: From 07e5598538f745b8495e5f58d7920c2d1a22fc38 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 12:09:16 +0000 Subject: [PATCH 03/53] Enable use of redis container in worker tests --- packages/backend-core/src/environment.ts | 5 +++-- packages/backend-core/src/queue/queue.ts | 2 +- packages/backend-core/src/redis/redis.ts | 13 +++++++++---- packages/backend-core/src/redis/utils.ts | 10 ++++------ .../tests/utilities/testContainerUtils.ts | 5 +++++ 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index d742ca1cc9..95e636c9b4 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -44,8 +44,9 @@ const environment = { GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, SALT_ROUNDS: process.env.SALT_ROUNDS, - REDIS_URL: process.env.REDIS_URL, - REDIS_PASSWORD: process.env.REDIS_PASSWORD, + REDIS_URL: process.env.REDIS_URL || "localhost:6379", + REDIS_PASSWORD: process.env.REDIS_PASSWORD || "budibase", + MOCK_REDIS: process.env.MOCK_REDIS, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, AWS_REGION: process.env.AWS_REGION, diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index b34d46e463..8e1fc1fbf3 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -4,7 +4,6 @@ import { JobQueue } from "./constants" import InMemoryQueue from "./inMemoryQueue" import BullQueue from "bull" import { addListeners, StalledFn } from "./listeners" -const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const CLEANUP_PERIOD_MS = 60 * 1000 let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = [] @@ -20,6 +19,7 @@ export function createQueue( jobQueue: JobQueue, opts: { removeStalledCb?: StalledFn } = {} ): BullQueue.Queue { + const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const queueConfig: any = redisProtocolUrl || { redis: redisOpts } let queue: any if (!env.isTest()) { diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 0267709cdc..2669cd816a 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -1,6 +1,6 @@ import env from "../environment" // ioredis mock is all in memory -const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") +const Redis = env.MOCK_REDIS ? require("ioredis-mock") : require("ioredis") import { addDbPrefix, removeDbPrefix, @@ -17,8 +17,13 @@ const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT // for testing just generate the client once let CLOSED = false let CLIENTS: { [key: number]: any } = {} -// if in test always connected -let CONNECTED = env.isTest() + +let CONNECTED = false + +// mock redis always connected +if (env.MOCK_REDIS) { + CONNECTED = true +} function pickClient(selectDb: number): any { return CLIENTS[selectDb] @@ -57,7 +62,7 @@ function init(selectDb = DEFAULT_SELECT_DB) { return } // testing uses a single in memory client - if (env.isTest()) { + if (env.MOCK_REDIS) { CLIENTS[selectDb] = new Redis(getRedisOptions()) } // start the timer - only allowed 5 seconds to connect diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts index 4c556ebd54..7606c77b87 100644 --- a/packages/backend-core/src/redis/utils.ts +++ b/packages/backend-core/src/redis/utils.ts @@ -2,8 +2,6 @@ import env from "../environment" const SLOT_REFRESH_MS = 2000 const CONNECT_TIMEOUT_MS = 10000 -const REDIS_URL = !env.REDIS_URL ? "localhost:6379" : env.REDIS_URL -const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD export const SEPARATOR = "-" /** @@ -60,8 +58,8 @@ export enum SelectableDatabase { } export function getRedisOptions(clustered = false) { - let password = REDIS_PASSWORD - let url: string[] | string = REDIS_URL.split("//") + let password = env.REDIS_PASSWORD + let url: string[] | string = env.REDIS_URL.split("//") // get rid of the protocol url = url.length > 1 ? url[1] : url[0] // check for a password etc @@ -78,8 +76,8 @@ export function getRedisOptions(clustered = false) { let redisProtocolUrl // fully qualified redis URL - if (/rediss?:\/\//.test(REDIS_URL)) { - redisProtocolUrl = REDIS_URL + if (/rediss?:\/\//.test(env.REDIS_URL)) { + redisProtocolUrl = env.REDIS_URL } const opts: any = { diff --git a/packages/backend-core/tests/utilities/testContainerUtils.ts b/packages/backend-core/tests/utilities/testContainerUtils.ts index 22198bd496..11c5fca806 100644 --- a/packages/backend-core/tests/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/utilities/testContainerUtils.ts @@ -34,12 +34,17 @@ function getMinioConfig() { return getContainerInfo("minio-service", 9000) } +function getRedisConfig() { + return getContainerInfo("redis-service", 6379) +} + export function setupEnv(...envs: any[]) { const configs = [ { key: "COUCH_DB_PORT", value: getCouchConfig().port }, { key: "COUCH_DB_URL", value: getCouchConfig().url }, { key: "MINIO_PORT", value: getMinioConfig().port }, { key: "MINIO_URL", value: getMinioConfig().url }, + { key: "REDIS_URL", value: getRedisConfig().url }, ] for (const config of configs.filter(x => !!x.value)) { From f53faff7ad80fd7530a20036cd51d22d5933048d Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 12:27:49 +0000 Subject: [PATCH 04/53] Add LOG_4XX to environment --- packages/backend-core/src/environment.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index d742ca1cc9..9a07695734 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -82,6 +82,7 @@ const environment = { SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", + LOG_4XX: process.env.LOG_4XX, _set(key: any, value: any) { process.env[key] = value // @ts-ignore From cc7eb64a3bcb6be5429c6f232e39d2161a7a6355 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 13 Feb 2023 14:39:24 +0000 Subject: [PATCH 05/53] Rename LOG_4XX to ENABLE_4XX_HTTP_LOGGING and enable by default --- packages/backend-core/src/environment.ts | 2 +- packages/backend-core/src/middleware/errorHandling.ts | 2 +- packages/backend-core/tests/jestEnv.ts | 1 + packages/server/src/tests/jestEnv.ts | 1 + packages/worker/src/tests/jestEnv.ts | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index df44416609..ed7a161160 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -83,7 +83,7 @@ const environment = { SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", - LOG_4XX: process.env.LOG_4XX, + ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts index 1baaa92501..5ac70c33e5 100644 --- a/packages/backend-core/src/middleware/errorHandling.ts +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -9,7 +9,7 @@ export async function errorHandling(ctx: any, next: any) { const status = err.status || err.statusCode || 500 ctx.status = status - if (status > 499 || env.LOG_4XX) { + if (status > 499 || env.ENABLE_4XX_HTTP_LOGGING) { ctx.log.error(err) } diff --git a/packages/backend-core/tests/jestEnv.ts b/packages/backend-core/tests/jestEnv.ts index 71cf865737..ec8de2942e 100644 --- a/packages/backend-core/tests/jestEnv.ts +++ b/packages/backend-core/tests/jestEnv.ts @@ -3,3 +3,4 @@ process.env.MULTI_TENANCY = "1" process.env.NODE_ENV = "jest" process.env.MOCK_REDIS = "1" process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.ENABLE_4XX_HTTP_LOGGING = "0" diff --git a/packages/server/src/tests/jestEnv.ts b/packages/server/src/tests/jestEnv.ts index b1ef038c1b..c567b260b3 100644 --- a/packages/server/src/tests/jestEnv.ts +++ b/packages/server/src/tests/jestEnv.ts @@ -6,4 +6,5 @@ process.env.MULTI_TENANCY = "1" // @ts-ignore process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.ENABLE_4XX_HTTP_LOGGING = "0" process.env.MOCK_REDIS = "1" diff --git a/packages/worker/src/tests/jestEnv.ts b/packages/worker/src/tests/jestEnv.ts index 602a505c1b..061897451e 100644 --- a/packages/worker/src/tests/jestEnv.ts +++ b/packages/worker/src/tests/jestEnv.ts @@ -2,6 +2,7 @@ process.env.SELF_HOSTED = "0" process.env.NODE_ENV = "jest" process.env.JWT_SECRET = "test-jwtsecret" process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.ENABLE_4XX_HTTP_LOGGING = "0" process.env.MULTI_TENANCY = "1" process.env.MINIO_URL = "http://localhost" process.env.MINIO_ACCESS_KEY = "test" From 8fe0cdf89f1ca3012f276c95704413b4f9936796 Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Thu, 16 Feb 2023 16:23:44 +0000 Subject: [PATCH 06/53] Handle webhook errors (#9715) --- .../server/src/automations/steps/discord.ts | 38 +++++++++++----- .../src/automations/steps/integromat.ts | 42 ++++++++++++------ .../server/src/automations/steps/slack.ts | 34 ++++++++++---- .../server/src/automations/steps/zapier.ts | 44 +++++++++++++------ 4 files changed, 111 insertions(+), 47 deletions(-) diff --git a/packages/server/src/automations/steps/discord.ts b/packages/server/src/automations/steps/discord.ts index ae484fa42e..5d7487ed3b 100644 --- a/packages/server/src/automations/steps/discord.ts +++ b/packages/server/src/automations/steps/discord.ts @@ -67,17 +67,33 @@ export async function run({ inputs }: AutomationStepInput) { if (!avatar_url) { avatar_url = DEFAULT_AVATAR_URL } - const response = await fetch(url, { - method: "post", - body: JSON.stringify({ - username, - avatar_url, - content, - }), - headers: { - "Content-Type": "application/json", - }, - }) + if (!url?.trim()?.length) { + return { + httpStatus: 400, + response: "Missing Webhook URL", + success: false, + } + } + let response + try { + response = await fetch(url, { + method: "post", + body: JSON.stringify({ + username, + avatar_url, + content, + }), + headers: { + "Content-Type": "application/json", + }, + }) + } catch (err: any) { + return { + httpStatus: 400, + response: err.message, + success: false, + } + } const { status, message } = await getFetchResponse(response) return { diff --git a/packages/server/src/automations/steps/integromat.ts b/packages/server/src/automations/steps/integromat.ts index dd897b5429..811c0a3d91 100644 --- a/packages/server/src/automations/steps/integromat.ts +++ b/packages/server/src/automations/steps/integromat.ts @@ -69,19 +69,35 @@ export const definition: AutomationStepSchema = { export async function run({ inputs }: AutomationStepInput) { const { url, value1, value2, value3, value4, value5 } = inputs - const response = await fetch(url, { - method: "post", - body: JSON.stringify({ - value1, - value2, - value3, - value4, - value5, - }), - headers: { - "Content-Type": "application/json", - }, - }) + if (!url?.trim()?.length) { + return { + httpStatus: 400, + response: "Missing Webhook URL", + success: false, + } + } + let response + try { + response = await fetch(url, { + method: "post", + body: JSON.stringify({ + value1, + value2, + value3, + value4, + value5, + }), + headers: { + "Content-Type": "application/json", + }, + }) + } catch (err: any) { + return { + httpStatus: 400, + response: err.message, + success: false, + } + } const { status, message } = await getFetchResponse(response) return { diff --git a/packages/server/src/automations/steps/slack.ts b/packages/server/src/automations/steps/slack.ts index 47c66bebf3..0c9320a699 100644 --- a/packages/server/src/automations/steps/slack.ts +++ b/packages/server/src/automations/steps/slack.ts @@ -50,15 +50,31 @@ export const definition: AutomationStepSchema = { export async function run({ inputs }: AutomationStepInput) { let { url, text } = inputs - const response = await fetch(url, { - method: "post", - body: JSON.stringify({ - text, - }), - headers: { - "Content-Type": "application/json", - }, - }) + if (!url?.trim()?.length) { + return { + httpStatus: 400, + response: "Missing Webhook URL", + success: false, + } + } + let response + try { + response = await fetch(url, { + method: "post", + body: JSON.stringify({ + text, + }), + headers: { + "Content-Type": "application/json", + }, + }) + } catch (err: any) { + return { + httpStatus: 400, + response: err.message, + success: false, + } + } const { status, message } = await getFetchResponse(response) return { diff --git a/packages/server/src/automations/steps/zapier.ts b/packages/server/src/automations/steps/zapier.ts index 1a48c1ec92..90068e685d 100644 --- a/packages/server/src/automations/steps/zapier.ts +++ b/packages/server/src/automations/steps/zapier.ts @@ -63,22 +63,38 @@ export const definition: AutomationStepSchema = { export async function run({ inputs }: AutomationStepInput) { const { url, value1, value2, value3, value4, value5 } = inputs + if (!url?.trim()?.length) { + return { + httpStatus: 400, + response: "Missing Webhook URL", + success: false, + } + } // send the platform to make sure zaps always work, even // if no values supplied - const response = await fetch(url, { - method: "post", - body: JSON.stringify({ - platform: "budibase", - value1, - value2, - value3, - value4, - value5, - }), - headers: { - "Content-Type": "application/json", - }, - }) + let response + try { + response = await fetch(url, { + method: "post", + body: JSON.stringify({ + platform: "budibase", + value1, + value2, + value3, + value4, + value5, + }), + headers: { + "Content-Type": "application/json", + }, + }) + } catch (err: any) { + return { + httpStatus: 400, + response: err.message, + success: false, + } + } const { status, message } = await getFetchResponse(response) From cd0e7d41a590a91efa66100ccded842c49f7b519 Mon Sep 17 00:00:00 2001 From: Budibase Release Bot <> Date: Thu, 16 Feb 2023 16:37:12 +0000 Subject: [PATCH 07/53] v2.3.17 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 0377c73101..cfbed56e23 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.16", + "version": "2.3.17", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 455436ff6a..25e73b9e4d 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.16", + "version": "2.3.17", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -23,7 +23,7 @@ }, "dependencies": { "@budibase/nano": "10.1.1", - "@budibase/types": "^2.3.16", + "@budibase/types": "^2.3.17", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 073f57f094..8c9629b314 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.16", + "version": "2.3.17", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "^2.3.16", + "@budibase/string-templates": "^2.3.17", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 7def4ba371..5e968c23a3 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.16", + "version": "2.3.17", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "^2.3.16", - "@budibase/client": "^2.3.16", - "@budibase/frontend-core": "^2.3.16", - "@budibase/string-templates": "^2.3.16", + "@budibase/bbui": "^2.3.17", + "@budibase/client": "^2.3.17", + "@budibase/frontend-core": "^2.3.17", + "@budibase/string-templates": "^2.3.17", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 5209c51b2e..fb08c290fc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.16", + "version": "2.3.17", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "^2.3.16", - "@budibase/string-templates": "^2.3.16", - "@budibase/types": "^2.3.16", + "@budibase/backend-core": "^2.3.17", + "@budibase/string-templates": "^2.3.17", + "@budibase/types": "^2.3.17", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index 5e2dad5217..78df2b67f7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.16", + "version": "2.3.17", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^2.3.16", - "@budibase/frontend-core": "^2.3.16", - "@budibase/string-templates": "^2.3.16", + "@budibase/bbui": "^2.3.17", + "@budibase/frontend-core": "^2.3.17", + "@budibase/string-templates": "^2.3.17", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 897b7107cf..502df0fbd4 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.16", + "version": "2.3.17", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "^2.3.16", + "@budibase/bbui": "^2.3.17", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 51f5ca70e0..d8a5c44c5b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.16", + "version": "2.3.17", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index aec1e3d047..78f03e0bfe 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.16", + "version": "2.3.17", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "^2.3.16", - "@budibase/client": "^2.3.16", + "@budibase/backend-core": "^2.3.17", + "@budibase/client": "^2.3.17", "@budibase/pro": "2.3.16", - "@budibase/string-templates": "^2.3.16", - "@budibase/types": "^2.3.16", + "@budibase/string-templates": "^2.3.17", + "@budibase/types": "^2.3.17", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 6b99e321e5..87b7c69dbb 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.16", + "version": "2.3.17", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 7d679edcf6..8417cadc06 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.16", + "version": "2.3.17", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 7c2518e8b5..a146e431c2 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.16", + "version": "2.3.17", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "^2.3.16", + "@budibase/backend-core": "^2.3.17", "@budibase/pro": "2.3.16", - "@budibase/string-templates": "^2.3.16", - "@budibase/types": "^2.3.16", + "@budibase/string-templates": "^2.3.17", + "@budibase/types": "^2.3.17", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From 580822f4a8427048a5c4475f6424d778b0e2e06e Mon Sep 17 00:00:00 2001 From: Budibase Release Bot <> Date: Thu, 16 Feb 2023 16:41:23 +0000 Subject: [PATCH 08/53] Update pro version to 2.3.17 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 78f03e0bfe..f5787302fe 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "^2.3.17", "@budibase/client": "^2.3.17", - "@budibase/pro": "2.3.16", + "@budibase/pro": "2.3.17", "@budibase/string-templates": "^2.3.17", "@budibase/types": "^2.3.17", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index e88dfaf333..31a5b3350e 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1273,13 +1273,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.16": - version "2.3.16" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.16.tgz#05a8434501718b9eab0109be03c677c1d546fe17" - integrity sha512-wMuqxKVua3/3XejUMH/fJQgu1kK6t4HYpB5AY58sumNSLbFFp1MyqL+1LMSmpUY0nbjExq+9+wseNsnbWicWUw== +"@budibase/backend-core@2.3.17": + version "2.3.17" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17.tgz#27c8c2144bfda1533b43da6de7111c0819aea6a5" + integrity sha512-KcmF2OrNLjLbFtNbYD4ZufnsnwmN2Ez/occgWiecvFRAHOhpkm+Hoy6VggpG1YJBp1DG9kLh3WAZbeYI3QoJbw== dependencies: "@budibase/nano" "10.1.1" - "@budibase/types" "^2.3.16" + "@budibase/types" "^2.3.17" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1374,13 +1374,13 @@ qs "^6.11.0" tough-cookie "^4.1.2" -"@budibase/pro@2.3.16": - version "2.3.16" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.16.tgz#3eca93b826ed6da5b6941d8b384c34c57da2b1b4" - integrity sha512-lIbPXOs61WP7jE80XHRDkBRmSEMYjiaog+qw0dUVP+Kp1QvBDa5Bdg7ESiy8YBae2+55FqXsb8nXjsqqbwFWDA== +"@budibase/pro@2.3.17": + version "2.3.17" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17.tgz#1a05d3d13195fcfacac410305fcd0943fbbcd5c8" + integrity sha512-sdWuKRDbseu2POkyGfmiqAWp8M9jGmpD0FqaIEWGQmKdezvOKh3sGg0PGT4InoibbXcFf4vVB+HiofBedDFLkA== dependencies: - "@budibase/backend-core" "2.3.16" - "@budibase/types" "2.3.16" + "@budibase/backend-core" "2.3.17" + "@budibase/types" "2.3.17" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1406,10 +1406,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.16", "@budibase/types@^2.3.16": - version "2.3.16" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.16.tgz#6d94b5f34ca58bcca1cca45737d0d1d0b21c9413" - integrity sha512-7caUKOlhleQL5gRqcgxSWvHcWIbl8hRPFl5ttWlLTfGO7BDMIRrcW7Wmptmgzoc6MiNCQAQ/uuZ8DeVOlJKRBA== +"@budibase/types@2.3.17", "@budibase/types@^2.3.17": + version "2.3.17" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17.tgz#d97c1de5fb03c91ff7e55d7c8c3901e5e2e95995" + integrity sha512-p/6WgwNjVGfwyNLOofhPEG7S3tt5URxAVs+mPXuLn5bsAqRxxJ5XObvw8chijYXmewhGP0hjONQDkmDJ0FkHuA== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index a146e431c2..71b8ed6e9c 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "^2.3.17", - "@budibase/pro": "2.3.16", + "@budibase/pro": "2.3.17", "@budibase/string-templates": "^2.3.17", "@budibase/types": "^2.3.17", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 826a6bd680..43d5ffb1bd 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -470,13 +470,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.16": - version "2.3.16" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.16.tgz#05a8434501718b9eab0109be03c677c1d546fe17" - integrity sha512-wMuqxKVua3/3XejUMH/fJQgu1kK6t4HYpB5AY58sumNSLbFFp1MyqL+1LMSmpUY0nbjExq+9+wseNsnbWicWUw== +"@budibase/backend-core@2.3.17": + version "2.3.17" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17.tgz#27c8c2144bfda1533b43da6de7111c0819aea6a5" + integrity sha512-KcmF2OrNLjLbFtNbYD4ZufnsnwmN2Ez/occgWiecvFRAHOhpkm+Hoy6VggpG1YJBp1DG9kLh3WAZbeYI3QoJbw== dependencies: "@budibase/nano" "10.1.1" - "@budibase/types" "^2.3.16" + "@budibase/types" "^2.3.17" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -521,13 +521,13 @@ qs "^6.11.0" tough-cookie "^4.1.2" -"@budibase/pro@2.3.16": - version "2.3.16" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.16.tgz#3eca93b826ed6da5b6941d8b384c34c57da2b1b4" - integrity sha512-lIbPXOs61WP7jE80XHRDkBRmSEMYjiaog+qw0dUVP+Kp1QvBDa5Bdg7ESiy8YBae2+55FqXsb8nXjsqqbwFWDA== +"@budibase/pro@2.3.17": + version "2.3.17" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17.tgz#1a05d3d13195fcfacac410305fcd0943fbbcd5c8" + integrity sha512-sdWuKRDbseu2POkyGfmiqAWp8M9jGmpD0FqaIEWGQmKdezvOKh3sGg0PGT4InoibbXcFf4vVB+HiofBedDFLkA== dependencies: - "@budibase/backend-core" "2.3.16" - "@budibase/types" "2.3.16" + "@budibase/backend-core" "2.3.17" + "@budibase/types" "2.3.17" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -535,10 +535,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.16", "@budibase/types@^2.3.16": - version "2.3.16" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.16.tgz#6d94b5f34ca58bcca1cca45737d0d1d0b21c9413" - integrity sha512-7caUKOlhleQL5gRqcgxSWvHcWIbl8hRPFl5ttWlLTfGO7BDMIRrcW7Wmptmgzoc6MiNCQAQ/uuZ8DeVOlJKRBA== +"@budibase/types@2.3.17", "@budibase/types@^2.3.17": + version "2.3.17" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17.tgz#d97c1de5fb03c91ff7e55d7c8c3901e5e2e95995" + integrity sha512-p/6WgwNjVGfwyNLOofhPEG7S3tt5URxAVs+mPXuLn5bsAqRxxJ5XObvw8chijYXmewhGP0hjONQDkmDJ0FkHuA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From 246f5eb5e477f6d6aa716b50b0e8c6e1bb03a78e Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Mon, 20 Feb 2023 10:15:29 +0000 Subject: [PATCH 09/53] bumping qa core types and backend core dependencies --- qa-core/package.json | 6 +- qa-core/yarn.lock | 353 ++++++++++++++++++++++++++++++++----------- 2 files changed, 265 insertions(+), 94 deletions(-) diff --git a/qa-core/package.json b/qa-core/package.json index 7733e95d46..15246af294 100644 --- a/qa-core/package.json +++ b/qa-core/package.json @@ -38,7 +38,7 @@ ] }, "devDependencies": { - "@budibase/types": "1.3.4", + "@budibase/types": "^2.3.17", "@types/jest": "29.0.0", "@types/node-fetch": "2.6.2", "chance": "1.1.8", @@ -53,8 +53,8 @@ "typescript": "4.7.3" }, "dependencies": { - "@budibase/backend-core": "^2.0.5", + "@budibase/backend-core": "^2.3.17", "form-data": "^4.0.0", "node-fetch": "2" } -} \ No newline at end of file +} diff --git a/qa-core/yarn.lock b/qa-core/yarn.lock index b090ff872f..be6207bbad 100644 --- a/qa-core/yarn.lock +++ b/qa-core/yarn.lock @@ -297,27 +297,30 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.0.5.tgz#e720ad8a0fd0eb0157d8ec332e530b481fd5b912" - integrity sha512-uY/YQgZ1xTm3npzWNRgZQBY/nj2ZxSkGtGbgK4NyWwZzvVUwd9vfNAIdKf7crECMJncH1x4H9TalQoFXb/cmbA== +"@budibase/backend-core@^2.3.17": + version "2.3.17" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17.tgz#27c8c2144bfda1533b43da6de7111c0819aea6a5" + integrity sha512-KcmF2OrNLjLbFtNbYD4ZufnsnwmN2Ez/occgWiecvFRAHOhpkm+Hoy6VggpG1YJBp1DG9kLh3WAZbeYI3QoJbw== dependencies: - "@budibase/types" "^2.0.5" + "@budibase/nano" "10.1.1" + "@budibase/types" "^2.3.17" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" + aws-cloudfront-sign "2.2.0" aws-sdk "2.1030.0" bcrypt "5.0.1" bcryptjs "2.4.3" + bull "4.10.1" + correlation-id "4.0.0" dotenv "16.0.1" emitter-listener "1.1.2" ioredis "4.28.0" joi "17.6.0" - jsonwebtoken "8.5.1" + jsonwebtoken "9.0.0" koa-passport "4.1.4" lodash "4.17.21" lodash.isarguments "3.1.0" node-fetch "2.6.7" - passport-google-auth "1.0.2" passport-google-oauth "2.0.0" passport-jwt "4.0.0" passport-local "1.0.0" @@ -333,15 +336,22 @@ uuid "8.3.2" zlib "1.0.5" -"@budibase/types@1.3.4": - version "1.3.4" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-1.3.4.tgz#25f087b024e843eb372e50c81f8f925fb39f1dfd" - integrity sha512-ndyWs8yeCS7cpZjApDB1HhY6UUM2SRBUgAMCZOZaWABG9JHeCbx7x0e/pA2SZjswdMXqS5WmnEd3br5wuvUzJw== +"@budibase/nano@10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@budibase/nano/-/nano-10.1.1.tgz#36ccda4d9bb64b5ee14dd2b27a295b40739b1038" + integrity sha512-kbMIzMkjVtl+xI0UPwVU0/pn8/ccxTyfzwBz6Z+ZiN2oUSb0fJCe0qwA6o8dxwSa8nZu4MbGAeMJl3CJndmWtA== + dependencies: + "@types/tough-cookie" "^4.0.2" + axios "^1.1.3" + http-cookie-agent "^4.0.2" + node-abort-controller "^3.0.1" + qs "^6.11.0" + tough-cookie "^4.1.2" -"@budibase/types@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.0.5.tgz#852c86611f237640b59d8dc4ae0c8c5fec491cf1" - integrity sha512-MnnDEB22kbXRsztmHPgvFDSYavpb0qm6H6Y/3UHXKqyFEg/KRpiF1p7lYsN+FAUDAWxpFgI+kp2Yw6gWyA5FLQ== +"@budibase/types@^2.3.17": + version "2.3.17" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17.tgz#d97c1de5fb03c91ff7e55d7c8c3901e5e2e95995" + integrity sha512-p/6WgwNjVGfwyNLOofhPEG7S3tt5URxAVs+mPXuLn5bsAqRxxJ5XObvw8chijYXmewhGP0hjONQDkmDJ0FkHuA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -660,6 +670,36 @@ semver "^7.3.5" tar "^6.1.11" +"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.0.tgz#d31a238c943ffc34bab73ad6ce7a6466d65888ef" + integrity sha512-5qpnNHUyyEj9H3sm/4Um/bnx1lrQGhe8iqry/1d+cQYCRd/gzYA0YLeq0ezlk4hKx4vO+dsEsNyeowqRqslwQA== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.0.tgz#2f6fbbec3d3f0bbe9c6678c899f1c1a6e25ed980" + integrity sha512-ZphTFFd6SFweNAMKD+QJCrWpgkjf4qBuHltiMkKkD6FFrB3NOTRVmetAGTkJ57pa+s6J0yCH06LujWB9rZe94g== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.0.tgz#19875441da50b9aa8f8e726eb097a4cead435a3f" + integrity sha512-NEX6hdSvP4BmVyegaIbrGxvHzHvTzzsPaxXCsUt0mbLbPpEftsvNwaEVKOowXnLoeuGeD4MaqSwL3BUK2elsUA== + +"@msgpackr-extract/msgpackr-extract-linux-arm@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.0.tgz#3b855ac72cc16e89db2f72adf47ddc964c20a53d" + integrity sha512-ztKVV1dO/sSZyGse0PBCq3Pk1PkYjsA/dsEWE7lfrGoAK3i9HpS2o7XjGQ7V4va6nX+xPPOiuYpQwa4Bi6vlww== + +"@msgpackr-extract/msgpackr-extract-linux-x64@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.0.tgz#455f1d5bb00e87f78c67711f26e7bff9f1457684" + integrity sha512-9uvdAkZMOPCY7SPRxZLW8XGqBOVNVEhqlgffenN8shA1XR9FWVsSM13nr/oHtNgXg6iVyML7RwWPyqUeThlwxg== + +"@msgpackr-extract/msgpackr-extract-win32-x64@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.0.tgz#03c6bfcd3acb179ea69546c20d50895b9d623ada" + integrity sha512-Wg0+9615kHKlr9iLVcG5I+/CHnf6w3x5UADRv8Ad16yA0Bu5l9eVOROjV7aHPG6uC8ZPFIVVaoSjDChD+Y0pzg== + "@shopify/jest-koa-mocks@5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@shopify/jest-koa-mocks/-/jest-koa-mocks-5.0.1.tgz#fba490b6b7985fbb571eb9974897d396a3642e94" @@ -825,6 +865,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/tough-cookie@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" + integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -889,7 +934,7 @@ acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== -agent-base@6: +agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -987,18 +1032,18 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== -async@~2.1.4: - version "2.1.5" - resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" - integrity sha512-+g/Ncjbx0JSq2Mk03WQkyKvNh5q9Qvyo/RIqIqnmC5feJY70PNl2ESwZU2BhAB+AZPkHNzzyC2Dq2AS5VnTKhQ== - dependencies: - lodash "^4.14.0" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +aws-cloudfront-sign@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/aws-cloudfront-sign/-/aws-cloudfront-sign-2.2.0.tgz#3910f5a6d0d90fec07f2b4ef8ab07f3eefb5625d" + integrity sha512-qG+rwZMP3KRTPPbVmWY8DlrT56AkA4iVOeo23vkdK2EXeW/brJFN2haSNKzVz+oYhFMEIzVVloeAcrEzuRkuVQ== + dependencies: + lodash "^3.6.0" + aws-sdk@2.1030.0: version "2.1030.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1030.0.tgz#24a856af3d2b8b37c14a8f59974993661c66fd82" @@ -1046,6 +1091,15 @@ axios@^0.21.1: dependencies: follow-redirects "^1.14.0" +axios@^1.1.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.3.tgz#e7011384ba839b885007c9c9fae1ff23dceb295b" + integrity sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.3.tgz#c1187258197c099072156a0a121c11ee1e3917d5" @@ -1226,6 +1280,21 @@ buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bull@4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/bull/-/bull-4.10.1.tgz#f14974b6089358b62b495a2cbf838aadc098e43f" + integrity sha512-Fp21tRPb2EaZPVfmM+ONZKVz2RA+to+zGgaTLyCKt3JMSU8OOBqK8143OQrnGuGpsyE5G+9FevFAGhdZZfQP2g== + dependencies: + cron-parser "^4.2.1" + debuglog "^1.0.0" + get-port "^5.1.1" + ioredis "^4.28.5" + lodash "^4.17.21" + msgpackr "^1.5.2" + p-timeout "^3.2.0" + semver "^7.3.2" + uuid "^8.3.0" + cache-content-type@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" @@ -1234,6 +1303,14 @@ cache-content-type@^1.0.0: mime-types "^2.1.18" ylru "^1.2.0" +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1438,11 +1515,25 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +correlation-id@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/correlation-id/-/correlation-id-4.0.0.tgz#c1d3038e5f30d7bfeae5728ff96f27a7506bc2c0" + integrity sha512-WvXtJBlovvOBKqTz/YwWP2gm6CXJZJArfGimp9s/ehmhJMPFbmnPMQe3K60Q9idGNixMvKojMjleyDhZEFdHfg== + dependencies: + uuid "^8.3.1" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cron-parser@^4.2.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.7.1.tgz#1e325a6a18e797a634ada1e2599ece0b6b5ed177" + integrity sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA== + dependencies: + luxon "^3.2.1" + cross-spawn@^7.0.0, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -1478,6 +1569,11 @@ debug@4.3.2: dependencies: ms "2.1.2" +debuglog@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw== + dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -1820,7 +1916,7 @@ follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== -follow-redirects@^1.14.4: +follow-redirects@^1.14.4, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -1919,11 +2015,25 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -1953,47 +2063,11 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -google-auth-library@~0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e" - integrity sha512-KM54Y9GhdAzfXUHmWEoYmaOykSLuMG7W4HvVLYqyogxOyE6px8oSS8W13ngqW0oDGZ915GFW3V6OM6+qcdvPOA== - dependencies: - gtoken "^1.2.1" - jws "^3.1.4" - lodash.noop "^3.0.1" - request "^2.74.0" - -google-p12-pem@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" - integrity sha512-puhMlJ2+E/rgvxWaqgN/nC7x623OAE8MR9vBUqxF0inCE7HoVfCHvTeQ9+BR+rj9KM0fIg6XV6tmbt7XHHssoQ== - dependencies: - node-forge "^0.7.1" - -googleapis@^16.0.0: - version "16.1.0" - resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-16.1.0.tgz#0f19f2d70572d918881a0f626e3b1a2fa8629576" - integrity sha512-5czmF7xkIlJKc1+/+5tltrI1skoR3HKtkDOld9rk+DOucTpZRjOhCoJzoSjxB3M8rP2tEb1VIr1TPyzR3V2PUQ== - dependencies: - async "~2.1.4" - google-auth-library "~0.10.0" - string-template "~1.0.0" - graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -gtoken@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8" - integrity sha512-wQAJflfoqSgMWrSBk9Fg86q+sd6s7y6uJhIvvIPz++RElGlMtEqsdAR2oWwZ/WTEtp7P9xFbJRrT976oRgzJ/w== - dependencies: - google-p12-pem "^0.1.0" - jws "^3.0.0" - mime "^1.4.1" - request "^2.72.0" - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -2017,7 +2091,7 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.2: +has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== @@ -2054,6 +2128,13 @@ http-assert@^1.3.0: deep-equal "~1.0.1" http-errors "~1.8.0" +http-cookie-agent@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/http-cookie-agent/-/http-cookie-agent-4.0.2.tgz#dcdaae18ed1f7452d81ae4d5cd80b227d6831b69" + integrity sha512-noTmxdH5CuytTnLj/Qv3Z84e/YFq8yLXAw3pqIYZ25Edhb9pQErIAC+ednw40Cic6Le/h9ryph5/TqsvkOaUCw== + dependencies: + agent-base "^6.0.2" + http-errors@^1.6.3, http-errors@~1.8.0: version "1.8.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" @@ -2150,6 +2231,23 @@ ioredis@4.28.0: redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ioredis@^4.28.5: + version "4.28.5" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f" + integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.3.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + lodash.isarguments "^3.1.0" + p-map "^2.1.0" + redis-commands "1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -2745,7 +2843,17 @@ json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== -jsonwebtoken@8.5.1, jsonwebtoken@^8.2.0: +jsonwebtoken@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jsonwebtoken@^8.2.0: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== @@ -2780,7 +2888,7 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.0.0, jws@^3.1.4, jws@^3.2.2: +jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== @@ -3017,11 +3125,6 @@ lodash.memoize@4.x: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== -lodash.noop@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" - integrity sha512-TmYdmu/pebrdTIBDK/FDx9Bmfzs9x0sZG6QIJuMDTqEPfeciLcN13ij+cOd0i9vwJfBtbG9UQ+C7MkXgYxrIJg== - lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -3032,11 +3135,16 @@ lodash.pick@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== -lodash@4.17.21, lodash@^4.14.0, lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lodash@^3.6.0: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + integrity sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -3049,6 +3157,11 @@ ltgt@2.2.1, ltgt@^2.1.2: resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA== +luxon@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f" + integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg== + make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -3122,7 +3235,7 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24, dependencies: mime-db "1.52.0" -mime@^1.3.4, mime@^1.4.1: +mime@^1.3.4: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -3179,6 +3292,27 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msgpackr-extract@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.0.tgz#5b5c5fbfff25be5ee5b5a82a9cbe02e37f72bed0" + integrity sha512-oy6KCk1+X4Bn5m6Ycq5N1EWl9npqG/cLrE8ga8NX7ZqfqYUUBS08beCQaGq80fjbKBySur0E6x//yZjzNJDt3A== + dependencies: + node-gyp-build-optional-packages "5.0.7" + optionalDependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.0" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.0" + "@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.0" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.0" + "@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.0" + "@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.0" + +msgpackr@^1.5.2: + version "1.8.3" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.3.tgz#78c1b91359f72707f4abeaca40cc423bd2d75185" + integrity sha512-m2JefwcKNzoHYXkH/5jzHRxAw7XLWsAdvu0FOJ+OLwwozwOV/J6UA62iLkfIMbg7G8+dIuRwgg6oz+QoQ4YkoA== + optionalDependencies: + msgpackr-extract "^3.0.0" + napi-macros@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" @@ -3204,6 +3338,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +node-abort-controller@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + node-addon-api@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" @@ -3221,10 +3360,10 @@ node-fetch@2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-forge@^0.7.1: - version "0.7.6" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" - integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== +node-gyp-build-optional-packages@5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3" + integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w== node-gyp-build@~4.1.0: version "4.1.1" @@ -3301,6 +3440,11 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -3327,6 +3471,11 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -3353,6 +3502,13 @@ p-map@^2.1.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -3373,14 +3529,6 @@ parseurl@^1.3.2, parseurl@^1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -passport-google-auth@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938" - integrity sha512-cfAqna6jZLyMEwUdd4PIwAh2mQKQVEDAaRIaom1pG6h4x4Gwjllf/Jflt3TkR1Sen5Rkvr3l7kSXCWE1EKkh8g== - dependencies: - googleapis "^16.0.0" - passport-strategy "1.x" - passport-google-oauth1@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz#af74a803df51ec646f66a44d82282be6f108e0cc" @@ -3443,7 +3591,7 @@ passport-oauth2@1.x.x: uid2 "0.0.x" utils-merge "1.x.x" -passport-strategy@1.x, passport-strategy@1.x.x, passport-strategy@^1.0.0: +passport-strategy@1.x.x, passport-strategy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== @@ -3720,6 +3868,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -3755,6 +3908,13 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@^6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -3851,7 +4011,7 @@ remove-trailing-slash@^0.1.1: resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d" integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== -request@^2.72.0, request@^2.74.0, request@^2.88.0: +request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -3974,6 +4134,13 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.2, semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -4001,6 +4168,15 @@ shimmer@^1.2.0: resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -4123,11 +4299,6 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-template@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" - integrity sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg== - "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -4315,7 +4486,7 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0": +"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== @@ -4486,7 +4657,7 @@ uuid@8.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== -uuid@8.3.2, uuid@^8.3.2: +uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.1, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== From 53f6b2b6e62443acfb6f79a17edcce6c18006842 Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Mon, 20 Feb 2023 11:03:37 +0000 Subject: [PATCH 10/53] Null safety (#9746) --- .../design/settings/controls/FilterEditor/FilterDrawer.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte index bf07cddf23..f56d7a78d4 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte @@ -254,8 +254,8 @@ {:else if filter.type === "datetime"} {:else} From 1eb2307fed412287ee53c9552fb543a5caed0d68 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 20 Feb 2023 15:35:14 +0000 Subject: [PATCH 11/53] Fix for #9749 - static formulas would sometimes attempt to update the same row multiple times, filter down to just the unique row list which requires updating. --- packages/server/src/api/controllers/row/staticFormula.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/staticFormula.ts b/packages/server/src/api/controllers/row/staticFormula.ts index 47a5af8f5a..6ba44dc23a 100644 --- a/packages/server/src/api/controllers/row/staticFormula.ts +++ b/packages/server/src/api/controllers/row/staticFormula.ts @@ -38,7 +38,13 @@ export async function updateRelatedFormula( if (!relatedRows[relatedTableId]) { relatedRows[relatedTableId] = [] } - relatedRows[relatedTableId] = relatedRows[relatedTableId].concat(field) + // filter down to the rows which are not already included in related + const currentIds = relatedRows[relatedTableId].map(row => row._id) + const uniqueRelatedRows = field.filter( + (row: Row) => !currentIds.includes(row._id) + ) + relatedRows[relatedTableId] = + relatedRows[relatedTableId].concat(uniqueRelatedRows) } } for (let tableId of table.relatedFormula) { From cacf275a997fa1274a05a98b3bc86637ec69a8dd Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 21 Feb 2023 08:23:53 +0000 Subject: [PATCH 12/53] Prevent SSO users from setting / resetting a password (#9672) * Prevent SSO users from setting / resetting a password * Add support for ENABLE_SSO_MAINTENANCE_MODE * Add typing to self api and build out user update sdk * Integrate sso checks with user sdk. Integrate user sdk with self api * Test fixes * Move self update into SDK * Lock down maintenance mode to admin user * Fix typo * Add health status response and return type signature to accounts.getStatus * Remove some unnecessary comments * Make sso save user function non optional * Remove redundant check on sso auth details provider * Update syncProfilePicture function name to getProfilePictureUrl * Update packages/worker/src/sdk/users/events.ts Co-authored-by: Adria Navarro * Add ENABLE_EMAIL_TEST_MODE flag * Fix for logging in as sso user when existing user has password already * Hide password update and force reset from ui for sso users * Always disable sso maintenance mode in cloud --------- Co-authored-by: Adria Navarro --- .../src/{cloud => accounts}/accounts.ts | 23 +- .../src/{cloud => accounts}/api.ts | 0 packages/backend-core/src/accounts/index.ts | 1 + packages/backend-core/src/auth/auth.ts | 50 +++- .../backend-core/src/auth/tests/auth.spec.ts | 13 + .../backend-core/src/events/identification.ts | 6 +- .../middleware/passport/datasource/google.ts | 11 +- .../src/middleware/passport/local.ts | 56 +--- .../middleware/passport/{ => sso}/google.ts | 28 +- .../src/middleware/passport/{ => sso}/oidc.ts | 40 ++- .../src/middleware/passport/sso/sso.ts | 165 +++++++++++ .../passport/sso/tests/google.spec.ts | 67 +++++ .../passport/sso/tests/oidc.spec.ts | 152 ++++++++++ .../middleware/passport/sso/tests/sso.spec.ts | 196 +++++++++++++ .../middleware/passport/tests/google.spec.js | 79 ----- .../middleware/passport/tests/oidc.spec.js | 144 ---------- .../tests/third-party-common.seq.spec.js | 178 ------------ .../passport/tests/utilities/mock-data.js | 54 ---- .../middleware/passport/third-party-common.ts | 177 ------------ packages/backend-core/src/users.ts | 6 + packages/backend-core/src/utils/utils.ts | 45 +-- .../tests/utilities/mocks/accounts.ts | 13 - .../tests/utilities/mocks/index.ts | 5 +- .../tests/utilities/structures/accounts.ts | 36 ++- .../tests/utilities/structures/index.ts | 4 +- .../tests/utilities/structures/sso.ts | 100 +++++++ .../tests/utilities/structures/users.ts | 70 +++++ .../portal/_components/UserDropdown.svelte | 8 +- .../portal/users/users/[userId].svelte | 9 +- packages/builder/src/stores/portal/auth.js | 1 + packages/server/specs/openapi.json | 12 +- packages/server/specs/openapi.yaml | 6 +- .../src/integrations/tests/couchdb.spec.ts | 2 - packages/types/src/api/account/index.ts | 1 + packages/types/src/api/account/status.ts | 7 + packages/types/src/api/web/auth.ts | 25 ++ packages/types/src/api/web/index.ts | 1 + packages/types/src/api/web/user.ts | 21 +- .../types/src/documents/account/account.ts | 18 +- packages/types/src/documents/global/config.ts | 27 +- packages/types/src/documents/global/user.ts | 63 ++-- packages/types/src/sdk/index.ts | 2 + packages/types/src/sdk/sso.ts | 37 +++ packages/types/src/sdk/user.ts | 12 + packages/worker/scripts/dev/manage.js | 1 + .../worker/src/api/controllers/global/auth.ts | 147 +++++----- .../worker/src/api/controllers/global/self.ts | 70 ++--- .../src/api/controllers/global/users.ts | 43 ++- .../src/api/controllers/system/accounts.ts | 12 +- packages/worker/src/api/routes/global/auth.ts | 17 +- .../src/api/routes/global/tests/auth.spec.ts | 271 +++++++++++++++--- .../src/api/routes/global/tests/self.spec.ts | 7 +- .../api/routes/system/tests/accounts.spec.ts | 8 +- packages/worker/src/environment.ts | 16 +- packages/worker/src/index.ts | 6 + packages/worker/src/sdk/accounts/index.ts | 3 +- .../sdk/accounts/{accounts.ts => metadata.ts} | 1 - packages/worker/src/sdk/auth/auth.ts | 86 ++++++ packages/worker/src/sdk/auth/index.ts | 1 + packages/worker/src/sdk/users/events.ts | 4 + packages/worker/src/sdk/users/index.ts | 1 + .../worker/src/sdk/users/tests/users.spec.ts | 52 ++++ packages/worker/src/sdk/users/users.ts | 97 ++++--- .../worker/src/tests/TestConfiguration.ts | 4 +- packages/worker/src/tests/api/auth.ts | 60 ++-- packages/worker/src/tests/structures/index.ts | 2 - packages/worker/src/tests/structures/users.ts | 37 --- packages/worker/src/utilities/email.ts | 6 +- 68 files changed, 1803 insertions(+), 1120 deletions(-) rename packages/backend-core/src/{cloud => accounts}/accounts.ts (69%) rename packages/backend-core/src/{cloud => accounts}/api.ts (100%) create mode 100644 packages/backend-core/src/accounts/index.ts create mode 100644 packages/backend-core/src/auth/tests/auth.spec.ts rename packages/backend-core/src/middleware/passport/{ => sso}/google.ts (76%) rename packages/backend-core/src/middleware/passport/{ => sso}/oidc.ts (85%) create mode 100644 packages/backend-core/src/middleware/passport/sso/sso.ts create mode 100644 packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts create mode 100644 packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts create mode 100644 packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts delete mode 100644 packages/backend-core/src/middleware/passport/tests/google.spec.js delete mode 100644 packages/backend-core/src/middleware/passport/tests/oidc.spec.js delete mode 100644 packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js delete mode 100644 packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js delete mode 100644 packages/backend-core/src/middleware/passport/third-party-common.ts delete mode 100644 packages/backend-core/tests/utilities/mocks/accounts.ts create mode 100644 packages/backend-core/tests/utilities/structures/sso.ts create mode 100644 packages/backend-core/tests/utilities/structures/users.ts create mode 100644 packages/types/src/api/account/status.ts create mode 100644 packages/types/src/api/web/auth.ts create mode 100644 packages/types/src/sdk/sso.ts create mode 100644 packages/types/src/sdk/user.ts rename packages/worker/src/sdk/accounts/{accounts.ts => metadata.ts} (99%) create mode 100644 packages/worker/src/sdk/auth/auth.ts create mode 100644 packages/worker/src/sdk/auth/index.ts create mode 100644 packages/worker/src/sdk/users/tests/users.spec.ts delete mode 100644 packages/worker/src/tests/structures/users.ts diff --git a/packages/backend-core/src/cloud/accounts.ts b/packages/backend-core/src/accounts/accounts.ts similarity index 69% rename from packages/backend-core/src/cloud/accounts.ts rename to packages/backend-core/src/accounts/accounts.ts index 90fa7ab824..a16d0f1074 100644 --- a/packages/backend-core/src/cloud/accounts.ts +++ b/packages/backend-core/src/accounts/accounts.ts @@ -1,13 +1,24 @@ import API from "./api" import env from "../environment" import { Header } from "../constants" -import { CloudAccount } from "@budibase/types" +import { CloudAccount, HealthStatusResponse } from "@budibase/types" const api = new API(env.ACCOUNT_PORTAL_URL) +/** + * This client is intended to be used in a cloud hosted deploy only. + * Rather than relying on each consumer to perform the necessary environmental checks + * we use the following check to exit early with a undefined response which should be + * handled by the caller. + */ +const EXIT_EARLY = env.SELF_HOSTED || env.DISABLE_ACCOUNT_PORTAL + export const getAccount = async ( email: string ): Promise => { + if (EXIT_EARLY) { + return + } const payload = { email, } @@ -29,6 +40,9 @@ export const getAccount = async ( export const getAccountByTenantId = async ( tenantId: string ): Promise => { + if (EXIT_EARLY) { + return + } const payload = { tenantId, } @@ -47,7 +61,12 @@ export const getAccountByTenantId = async ( return json[0] } -export const getStatus = async () => { +export const getStatus = async (): Promise< + HealthStatusResponse | undefined +> => { + if (EXIT_EARLY) { + return + } const response = await api.get(`/api/status`, { headers: { [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, diff --git a/packages/backend-core/src/cloud/api.ts b/packages/backend-core/src/accounts/api.ts similarity index 100% rename from packages/backend-core/src/cloud/api.ts rename to packages/backend-core/src/accounts/api.ts diff --git a/packages/backend-core/src/accounts/index.ts b/packages/backend-core/src/accounts/index.ts new file mode 100644 index 0000000000..f2ae03040e --- /dev/null +++ b/packages/backend-core/src/accounts/index.ts @@ -0,0 +1 @@ +export * from "./accounts" diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index bbefb2933d..bee245a3ae 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -1,10 +1,11 @@ const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -import { getGlobalDB } from "../tenancy" +import { getGlobalDB } from "../context" const refresh = require("passport-oauth2-refresh") -import { Config } from "../constants" +import { Config, Cookie } from "../constants" import { getScopedConfig } from "../db" +import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { jwt as jwtPassport, local, @@ -15,8 +16,11 @@ import { google, } from "../middleware" import { invalidateUser } from "../cache/user" -import { User } from "@budibase/types" +import { PlatformLogoutOpts, User } from "@budibase/types" import { logAlert } from "../logging" +import * as events from "../events" +import * as userCache from "../cache/user" +import { clearCookie, getCookie } from "../utils" export { auditLog, authError, @@ -29,6 +33,7 @@ export { google, oidc, } from "../middleware" +import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" export const buildAuthMiddleware = authenticated export const buildTenancyMiddleware = tenancy export const buildCsrfMiddleware = csrf @@ -71,7 +76,7 @@ async function refreshOIDCAccessToken( if (!enrichedConfig) { throw new Error("OIDC Config contents invalid") } - strategy = await oidc.strategyFactory(enrichedConfig) + strategy = await oidc.strategyFactory(enrichedConfig, ssoSaveUserNoOp) } catch (err) { console.error(err) throw new Error("Could not refresh OAuth Token") @@ -103,7 +108,11 @@ async function refreshGoogleAccessToken( let strategy try { - strategy = await google.strategyFactory(config, callbackUrl) + strategy = await google.strategyFactory( + config, + callbackUrl, + ssoSaveUserNoOp + ) } catch (err: any) { console.error(err) throw new Error( @@ -161,6 +170,8 @@ export async function refreshOAuthToken( return refreshResponse } +// TODO: Refactor to use user save function instead to prevent the need for +// manually saving and invalidating on callback export async function updateUserOAuth(userId: string, oAuthConfig: any) { const details = { accessToken: oAuthConfig.accessToken, @@ -188,3 +199,32 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) { console.error("Could not update OAuth details for current user", e) } } + +/** + * Logs a user out from budibase. Re-used across account portal and builder. + */ +export async function platformLogout(opts: PlatformLogoutOpts) { + const ctx = opts.ctx + const userId = opts.userId + const keepActiveSession = opts.keepActiveSession + + if (!ctx) throw new Error("Koa context must be supplied to logout.") + + const currentSession = getCookie(ctx, Cookie.Auth) + let sessions = await getSessionsForUser(userId) + + if (keepActiveSession) { + sessions = sessions.filter( + session => session.sessionId !== currentSession.sessionId + ) + } else { + // clear cookies + clearCookie(ctx, Cookie.Auth) + clearCookie(ctx, Cookie.CurrentApp) + } + + const sessionIds = sessions.map(({ sessionId }) => sessionId) + await invalidateSessions(userId, { sessionIds, reason: "logout" }) + await events.auth.logout() + await userCache.invalidateUser(userId) +} diff --git a/packages/backend-core/src/auth/tests/auth.spec.ts b/packages/backend-core/src/auth/tests/auth.spec.ts new file mode 100644 index 0000000000..307f6a63c8 --- /dev/null +++ b/packages/backend-core/src/auth/tests/auth.spec.ts @@ -0,0 +1,13 @@ +import { structures, testEnv } from "../../../tests" +import * as auth from "../auth" +import * as events from "../../events" + +describe("platformLogout", () => { + it("should call platform logout", async () => { + await testEnv.withTenant(async () => { + const ctx = structures.koa.newContext() + await auth.platformLogout({ ctx, userId: "test" }) + expect(events.auth.logout).toBeCalledTimes(1) + }) + }) +}) diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 8ac22b471c..7cade9e14b 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -16,6 +16,7 @@ import { InstallationGroup, UserContext, Group, + isSSOUser, } from "@budibase/types" import { processors } from "./processors" import * as dbUtils from "../db/utils" @@ -166,7 +167,10 @@ const identifyUser = async ( const type = IdentityType.USER let builder = user.builder?.global || false let admin = user.admin?.global || false - let providerType = user.providerType + let providerType + if (isSSOUser(user)) { + providerType = user.providerType + } const accountHolder = account?.budibaseUserId === user._id || false const verified = account && account?.budibaseUserId === user._id ? account.verified : false diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index 65620d7aa3..112f8d2096 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -1,10 +1,11 @@ -import * as google from "../google" +import * as google from "../sso/google" import { Cookie, Config } from "../../../constants" import { clearCookie, getCookie } from "../../../utils" import { getScopedConfig, getPlatformUrl, doWithDB } from "../../../db" import environment from "../../../environment" -import { getGlobalDB } from "../../../tenancy" +import { getGlobalDB } from "../../../context" import { BBContext, Database, SSOProfile } from "@budibase/types" +import { ssoSaveUserNoOp } from "../sso/sso" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy type Passport = { @@ -36,7 +37,11 @@ export async function preAuth( const platformUrl = await getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` - const strategy = await google.strategyFactory(googleConfig, callbackUrl) + const strategy = await google.strategyFactory( + googleConfig, + callbackUrl, + ssoSaveUserNoOp + ) if (!ctx.query.appId || !ctx.query.datasourceId) { ctx.throw(400, "appId and datasourceId query params not present.") diff --git a/packages/backend-core/src/middleware/passport/local.ts b/packages/backend-core/src/middleware/passport/local.ts index 8b85d3734c..e198032532 100644 --- a/packages/backend-core/src/middleware/passport/local.ts +++ b/packages/backend-core/src/middleware/passport/local.ts @@ -1,15 +1,10 @@ import { UserStatus } from "../../constants" -import { compare, newid } from "../../utils" -import env from "../../environment" +import { compare } from "../../utils" import * as users from "../../users" import { authError } from "./utils" -import { createASession } from "../../security/sessions" -import { getTenantId } from "../../tenancy" import { BBContext } from "@budibase/types" -const jwt = require("jsonwebtoken") const INVALID_ERR = "Invalid credentials" -const SSO_NO_PASSWORD = "SSO user does not have a password set" const EXPIRED = "This account has expired. Please reset your password" export const options = { @@ -35,50 +30,25 @@ export async function authenticate( const dbUser = await users.getGlobalUserByEmail(email) if (dbUser == null) { - return authError(done, `User not found: [${email}]`) - } - - // check that the user is currently inactive, if this is the case throw invalid - if (dbUser.status === UserStatus.INACTIVE) { + console.info(`user=${email} could not be found`) return authError(done, INVALID_ERR) } - // check that the user has a stored password before proceeding - if (!dbUser.password) { - if ( - (dbUser.account && dbUser.account.authType === "sso") || // root account sso - dbUser.thirdPartyProfile // internal sso - ) { - return authError(done, SSO_NO_PASSWORD) - } + if (dbUser.status === UserStatus.INACTIVE) { + console.info(`user=${email} is inactive`, dbUser) + return authError(done, INVALID_ERR) + } - console.error("Non SSO usser has no password set", dbUser) + if (!dbUser.password) { + console.info(`user=${email} has no password set`, dbUser) return authError(done, EXPIRED) } - // authenticate - if (await compare(password, dbUser.password)) { - const sessionId = newid() - const tenantId = getTenantId() - - await createASession(dbUser._id!, { sessionId, tenantId }) - - const token = jwt.sign( - { - userId: dbUser._id, - sessionId, - tenantId, - }, - env.JWT_SECRET - ) - // Remove users password in payload - delete dbUser.password - - return done(null, { - ...dbUser, - token, - }) - } else { + if (!(await compare(password, dbUser.password))) { return authError(done, INVALID_ERR) } + + // intentionally remove the users password in payload + delete dbUser.password + return done(null, dbUser) } diff --git a/packages/backend-core/src/middleware/passport/google.ts b/packages/backend-core/src/middleware/passport/sso/google.ts similarity index 76% rename from packages/backend-core/src/middleware/passport/google.ts rename to packages/backend-core/src/middleware/passport/sso/google.ts index dd3dc8b86d..d26d7d6a8d 100644 --- a/packages/backend-core/src/middleware/passport/google.ts +++ b/packages/backend-core/src/middleware/passport/sso/google.ts @@ -1,18 +1,26 @@ -import { ssoCallbackUrl } from "./utils" -import { authenticateThirdParty, SaveUserFunction } from "./third-party-common" -import { ConfigType, GoogleConfig, Database, SSOProfile } from "@budibase/types" +import { ssoCallbackUrl } from "../utils" +import * as sso from "./sso" +import { + ConfigType, + GoogleConfig, + Database, + SSOProfile, + SSOAuthDetails, + SSOProviderType, + SaveSSOUserFunction, +} from "@budibase/types" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy -export function buildVerifyFn(saveUserFn?: SaveUserFunction) { +export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { return ( accessToken: string, refreshToken: string, profile: SSOProfile, done: Function ) => { - const thirdPartyUser = { - provider: profile.provider, // should always be 'google' - providerType: "google", + const details: SSOAuthDetails = { + provider: "google", + providerType: SSOProviderType.GOOGLE, userId: profile.id, profile: profile, email: profile._json.email, @@ -22,8 +30,8 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { }, } - return authenticateThirdParty( - thirdPartyUser, + return sso.authenticate( + details, true, // require local accounts to exist done, saveUserFn @@ -39,7 +47,7 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { export async function strategyFactory( config: GoogleConfig["config"], callbackUrl: string, - saveUserFn?: SaveUserFunction + saveUserFn: SaveSSOUserFunction ) { try { const { clientID, clientSecret } = config diff --git a/packages/backend-core/src/middleware/passport/oidc.ts b/packages/backend-core/src/middleware/passport/sso/oidc.ts similarity index 85% rename from packages/backend-core/src/middleware/passport/oidc.ts rename to packages/backend-core/src/middleware/passport/sso/oidc.ts index 7caa177cf0..1fb44b84a3 100644 --- a/packages/backend-core/src/middleware/passport/oidc.ts +++ b/packages/backend-core/src/middleware/passport/sso/oidc.ts @@ -1,22 +1,20 @@ import fetch from "node-fetch" -import { authenticateThirdParty, SaveUserFunction } from "./third-party-common" -import { ssoCallbackUrl } from "./utils" +import * as sso from "./sso" +import { ssoCallbackUrl } from "../utils" import { ConfigType, - OIDCInnerCfg, + OIDCInnerConfig, Database, SSOProfile, - ThirdPartyUser, - OIDCConfiguration, + OIDCStrategyConfiguration, + SSOAuthDetails, + SSOProviderType, + JwtClaims, + SaveSSOUserFunction, } from "@budibase/types" const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy -type JwtClaims = { - preferred_username: string - email: string -} - -export function buildVerifyFn(saveUserFn?: SaveUserFunction) { +export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { /** * @param {*} issuer The identity provider base URL * @param {*} sub The user ID @@ -39,10 +37,10 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { params: any, done: Function ) => { - const thirdPartyUser: ThirdPartyUser = { + const details: SSOAuthDetails = { // store the issuer info to enable sync in future provider: issuer, - providerType: "oidc", + providerType: SSOProviderType.OIDC, userId: profile.id, profile: profile, email: getEmail(profile, jwtClaims), @@ -52,8 +50,8 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { }, } - return authenticateThirdParty( - thirdPartyUser, + return sso.authenticate( + details, false, // don't require local accounts to exist done, saveUserFn @@ -104,8 +102,8 @@ function validEmail(value: string) { * @returns Dynamically configured Passport OIDC Strategy */ export async function strategyFactory( - config: OIDCConfiguration, - saveUserFn?: SaveUserFunction + config: OIDCStrategyConfiguration, + saveUserFn: SaveSSOUserFunction ) { try { const verify = buildVerifyFn(saveUserFn) @@ -119,14 +117,14 @@ export async function strategyFactory( } export async function fetchStrategyConfig( - enrichedConfig: OIDCInnerCfg, + oidcConfig: OIDCInnerConfig, callbackUrl?: string -): Promise { +): Promise { try { - const { clientID, clientSecret, configUrl } = enrichedConfig + const { clientID, clientSecret, configUrl } = oidcConfig if (!clientID || !clientSecret || !callbackUrl || !configUrl) { - //check for remote config and all required elements + // check for remote config and all required elements throw new Error( "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" ) diff --git a/packages/backend-core/src/middleware/passport/sso/sso.ts b/packages/backend-core/src/middleware/passport/sso/sso.ts new file mode 100644 index 0000000000..2fc1184722 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/sso.ts @@ -0,0 +1,165 @@ +import { generateGlobalUserID } from "../../../db" +import { authError } from "../utils" +import * as users from "../../../users" +import * as context from "../../../context" +import fetch from "node-fetch" +import { + SaveSSOUserFunction, + SaveUserOpts, + SSOAuthDetails, + SSOUser, + User, +} from "@budibase/types" + +// no-op function for user save +// - this allows datasource auth and access token refresh to work correctly +// - prefer no-op over an optional argument to ensure function is provided to login flows +export const ssoSaveUserNoOp: SaveSSOUserFunction = ( + user: SSOUser, + opts: SaveUserOpts +) => Promise.resolve(user) + +/** + * Common authentication logic for third parties. e.g. OAuth, OIDC. + */ +export async function authenticate( + details: SSOAuthDetails, + requireLocalAccount: boolean = true, + done: any, + saveUserFn: SaveSSOUserFunction +) { + if (!saveUserFn) { + throw new Error("Save user function must be provided") + } + if (!details.userId) { + return authError(done, "sso user id required") + } + if (!details.email) { + return authError(done, "sso user email required") + } + + // use the third party id + const userId = generateGlobalUserID(details.userId) + + let dbUser: User | undefined + + // try to load by id + try { + dbUser = await users.getById(userId) + } catch (err: any) { + // abort when not 404 error + if (!err.status || err.status !== 404) { + return authError( + done, + "Unexpected error when retrieving existing user", + err + ) + } + } + + // fallback to loading by email + if (!dbUser) { + dbUser = await users.getGlobalUserByEmail(details.email) + } + + // exit early if there is still no user and auto creation is disabled + if (!dbUser && requireLocalAccount) { + return authError( + done, + "Email does not yet exist. You must set up your local budibase account first." + ) + } + + // first time creation + if (!dbUser) { + // setup a blank user using the third party id + dbUser = { + _id: userId, + email: details.email, + roles: {}, + tenantId: context.getTenantId(), + } + } + + let ssoUser = await syncUser(dbUser, details) + // never prompt for password reset + ssoUser.forceResetPassword = false + + try { + // don't try to re-save any existing password + delete ssoUser.password + // create or sync the user + ssoUser = (await saveUserFn(ssoUser, { + hashPassword: false, + requirePassword: false, + })) as SSOUser + } catch (err: any) { + return authError(done, "Error saving user", err) + } + + return done(null, ssoUser) +} + +async function getProfilePictureUrl(user: User, details: SSOAuthDetails) { + const pictureUrl = details.profile?._json.picture + if (pictureUrl) { + const response = await fetch(pictureUrl) + if (response.status === 200) { + const type = response.headers.get("content-type") as string + if (type.startsWith("image/")) { + return pictureUrl + } + } + } +} + +/** + * @returns a user that has been sync'd with third party information + */ +async function syncUser(user: User, details: SSOAuthDetails): Promise { + let firstName + let lastName + let pictureUrl + let oauth2 + let thirdPartyProfile + + if (details.profile) { + const profile = details.profile + + if (profile.name) { + const name = profile.name + // first name + if (name.givenName) { + firstName = name.givenName + } + // last name + if (name.familyName) { + lastName = name.familyName + } + } + + pictureUrl = await getProfilePictureUrl(user, details) + + thirdPartyProfile = { + ...profile._json, + } + } + + // oauth tokens for future use + if (details.oauth2) { + oauth2 = { + ...details.oauth2, + } + } + + return { + ...user, + provider: details.provider, + providerType: details.providerType, + firstName, + lastName, + thirdPartyProfile, + pictureUrl, + oauth2, + } +} diff --git a/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts new file mode 100644 index 0000000000..eb8ffc9b71 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts @@ -0,0 +1,67 @@ +import { generator, structures } from "../../../../../tests" +import { SSOProviderType } from "@budibase/types" + +jest.mock("passport-google-oauth") +const mockStrategy = require("passport-google-oauth").OAuth2Strategy + +jest.mock("../sso") +import * as _sso from "../sso" +const sso = jest.mocked(_sso) + +const mockSaveUserFn = jest.fn() +const mockDone = jest.fn() + +import * as google from "../google" + +describe("google", () => { + describe("strategyFactory", () => { + const googleConfig = structures.sso.googleConfig() + const callbackUrl = generator.url() + + it("should create successfully create a google strategy", async () => { + await google.strategyFactory(googleConfig, callbackUrl) + + const expectedOptions = { + clientID: googleConfig.clientID, + clientSecret: googleConfig.clientSecret, + callbackURL: callbackUrl, + } + + expect(mockStrategy).toHaveBeenCalledWith( + expectedOptions, + expect.anything() + ) + }) + }) + + describe("authenticate", () => { + const details = structures.sso.authDetails() + details.provider = "google" + details.providerType = SSOProviderType.GOOGLE + + const profile = details.profile! + profile.provider = "google" + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("delegates authentication to third party common", async () => { + const authenticate = await google.buildVerifyFn(mockSaveUserFn) + + await authenticate( + details.oauth2.accessToken, + details.oauth2.refreshToken!, + profile, + mockDone + ) + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + true, + mockDone, + mockSaveUserFn + ) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts new file mode 100644 index 0000000000..a705739bd6 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts @@ -0,0 +1,152 @@ +import { generator, mocks, structures } from "../../../../../tests" +import { + JwtClaims, + OIDCInnerConfig, + SSOAuthDetails, + SSOProviderType, +} from "@budibase/types" +import * as _sso from "../sso" +import * as oidc from "../oidc" + +jest.mock("@techpass/passport-openidconnect") +const mockStrategy = require("@techpass/passport-openidconnect").Strategy + +jest.mock("../sso") +const sso = jest.mocked(_sso) + +const mockSaveUser = jest.fn() +const mockDone = jest.fn() + +describe("oidc", () => { + const callbackUrl = generator.url() + const oidcConfig: OIDCInnerConfig = structures.sso.oidcConfig() + const wellKnownConfig = structures.sso.oidcWellKnownConfig() + + function mockRetrieveWellKnownConfig() { + // mock the request to retrieve the oidc configuration + mocks.fetch.mockReturnValue({ + ok: true, + json: () => wellKnownConfig, + }) + } + + beforeEach(() => { + mockRetrieveWellKnownConfig() + }) + + describe("strategyFactory", () => { + it("should create successfully create an oidc strategy", async () => { + const strategyConfiguration = await oidc.fetchStrategyConfig( + oidcConfig, + callbackUrl + ) + await oidc.strategyFactory(strategyConfiguration, mockSaveUser) + + expect(mocks.fetch).toHaveBeenCalledWith(oidcConfig.configUrl) + + const expectedOptions = { + issuer: wellKnownConfig.issuer, + authorizationURL: wellKnownConfig.authorization_endpoint, + tokenURL: wellKnownConfig.token_endpoint, + userInfoURL: wellKnownConfig.userinfo_endpoint, + clientID: oidcConfig.clientID, + clientSecret: oidcConfig.clientSecret, + callbackURL: callbackUrl, + } + expect(mockStrategy).toHaveBeenCalledWith( + expectedOptions, + expect.anything() + ) + }) + }) + + describe("authenticate", () => { + const details: SSOAuthDetails = structures.sso.authDetails() + details.providerType = SSOProviderType.OIDC + const profile = details.profile! + const issuer = profile.provider + + const sub = generator.string() + const idToken = generator.string() + const params = {} + + let authenticateFn: any + let jwtClaims: JwtClaims + + beforeEach(async () => { + jest.clearAllMocks() + authenticateFn = await oidc.buildVerifyFn(mockSaveUser) + }) + + async function authenticate() { + await authenticateFn( + issuer, + sub, + profile, + jwtClaims, + details.oauth2.accessToken, + details.oauth2.refreshToken, + idToken, + params, + mockDone + ) + } + + it("passes auth details to sso module", async () => { + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT email to get email", async () => { + delete profile._json.email + + jwtClaims = { + email: details.email, + } + + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT username to get email", async () => { + delete profile._json.email + + jwtClaims = { + email: details.email, + } + + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT invalid username to get email", async () => { + delete profile._json.email + + jwtClaims = { + preferred_username: "invalidUsername", + } + + await expect(authenticate()).rejects.toThrow( + "Could not determine user email from profile" + ) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts new file mode 100644 index 0000000000..ae42fc01ea --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts @@ -0,0 +1,196 @@ +import { structures, testEnv, mocks } from "../../../../../tests" +import { SSOAuthDetails, User } from "@budibase/types" + +import { HTTPError } from "../../../../errors" +import * as sso from "../sso" +import * as context from "../../../../context" + +const mockDone = jest.fn() +const mockSaveUser = jest.fn() + +jest.mock("../../../../users") +import * as _users from "../../../../users" +const users = jest.mocked(_users) + +const getErrorMessage = () => { + return mockDone.mock.calls[0][2].message +} + +describe("sso", () => { + describe("authenticate", () => { + beforeEach(() => { + jest.clearAllMocks() + testEnv.singleTenant() + }) + + describe("validation", () => { + const testValidation = async ( + details: SSOAuthDetails, + message: string + ) => { + await sso.authenticate(details, false, mockDone, mockSaveUser) + + expect(mockDone.mock.calls.length).toBe(1) + expect(getErrorMessage()).toContain(message) + } + + it("user id fails", async () => { + const details = structures.sso.authDetails() + details.userId = undefined! + + await testValidation(details, "sso user id required") + }) + + it("email fails", async () => { + const details = structures.sso.authDetails() + details.email = undefined! + + await testValidation(details, "sso user email required") + }) + }) + + function mockGetProfilePicture() { + mocks.fetch.mockReturnValueOnce( + Promise.resolve({ + status: 200, + headers: { get: () => "image/" }, + }) + ) + } + + describe("when the user doesn't exist", () => { + let user: User + let details: SSOAuthDetails + + beforeEach(() => { + users.getById.mockImplementationOnce(() => { + throw new HTTPError("", 404) + }) + mockGetProfilePicture() + + user = structures.users.user() + delete user._rev + delete user._id + + details = structures.sso.authDetails(user) + details.userId = structures.uuid() + }) + + describe("when a local account is required", () => { + it("returns an error message", async () => { + const details = structures.sso.authDetails() + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + expect(mockDone.mock.calls.length).toBe(1) + expect(getErrorMessage()).toContain( + "Email does not yet exist. You must set up your local budibase account first." + ) + }) + }) + + describe("when a local account isn't required", () => { + it("creates and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ user, details }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, false, mockDone, mockSaveUser) + + // default roles for new user + ssoUser.roles = {} + + // modified external id to match user format + ssoUser._id = "us_" + details.userId + + // new sso user won't have a password + delete ssoUser.password + + // new user isn't saved with rev + delete ssoUser._rev + + // tenant id added + ssoUser.tenantId = context.getTenantId() + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + }) + + describe("when the user exists", () => { + let existingUser: User + let details: SSOAuthDetails + + beforeEach(() => { + existingUser = structures.users.user() + existingUser._id = structures.uuid() + details = structures.sso.authDetails(existingUser) + mockGetProfilePicture() + }) + + describe("exists by email", () => { + beforeEach(() => { + users.getById.mockImplementationOnce(() => { + throw new HTTPError("", 404) + }) + users.getGlobalUserByEmail.mockReturnValueOnce( + Promise.resolve(existingUser) + ) + }) + + it("syncs and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ + user: existingUser, + details, + }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + // roles preserved + ssoUser.roles = existingUser.roles + + // existing id preserved + ssoUser._id = existingUser._id + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + + describe("exists by id", () => { + beforeEach(() => { + users.getById.mockReturnValueOnce(Promise.resolve(existingUser)) + }) + + it("syncs and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ + user: existingUser, + details, + }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + // roles preserved + ssoUser.roles = existingUser.roles + + // existing id preserved + ssoUser._id = existingUser._id + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/tests/google.spec.js b/packages/backend-core/src/middleware/passport/tests/google.spec.js deleted file mode 100644 index c5580ea309..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/google.spec.js +++ /dev/null @@ -1,79 +0,0 @@ -// Mock data - -const { data } = require("./utilities/mock-data") - -const TENANT_ID = "default" - -const googleConfig = { - clientID: data.clientID, - clientSecret: data.clientSecret, -} - -const profile = { - id: "mockId", - _json: { - email : data.email - }, - provider: "google" -} - -const user = data.buildThirdPartyUser("google", "google", profile) - -describe("google", () => { - describe("strategyFactory", () => { - // mock passport strategy factory - jest.mock("passport-google-oauth") - const mockStrategy = require("passport-google-oauth").OAuth2Strategy - - it("should create successfully create a google strategy", async () => { - const google = require("../google") - - const callbackUrl = `/api/global/auth/${TENANT_ID}/google/callback` - await google.strategyFactory(googleConfig, callbackUrl) - - const expectedOptions = { - clientID: googleConfig.clientID, - clientSecret: googleConfig.clientSecret, - callbackURL: callbackUrl, - } - - expect(mockStrategy).toHaveBeenCalledWith( - expectedOptions, - expect.anything() - ) - }) - }) - - describe("authenticate", () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - // mock third party common authentication - jest.mock("../third-party-common") - const authenticateThirdParty = require("../third-party-common").authenticateThirdParty - - // mock the passport callback - const mockDone = jest.fn() - - it("delegates authentication to third party common", async () => { - const google = require("../google") - const mockSaveUserFn = jest.fn() - const authenticate = await google.buildVerifyFn(mockSaveUserFn) - - await authenticate( - data.accessToken, - data.refreshToken, - profile, - mockDone - ) - - expect(authenticateThirdParty).toHaveBeenCalledWith( - user, - true, - mockDone, - mockSaveUserFn) - }) - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js deleted file mode 100644 index 4c8aa94ddf..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ /dev/null @@ -1,144 +0,0 @@ -// Mock data -const mockFetch = require("node-fetch") -const { data } = require("./utilities/mock-data") -const issuer = "mockIssuer" -const sub = "mockSub" -const profile = { - id: "mockId", - _json: { - email : data.email - } -} -let jwtClaims = {} -const idToken = "mockIdToken" -const params = {} - -const callbackUrl = "http://somecallbackurl" - -// response from .well-known/openid-configuration -const oidcConfigUrlResponse = { - issuer: issuer, - authorization_endpoint: "mockAuthorizationEndpoint", - token_endpoint: "mockTokenEndpoint", - userinfo_endpoint: "mockUserInfoEndpoint" -} - -const oidcConfig = { - configUrl: "http://someconfigurl", - clientID: data.clientID, - clientSecret: data.clientSecret, -} - -const user = data.buildThirdPartyUser(issuer, "oidc", profile) - -describe("oidc", () => { - describe("strategyFactory", () => { - // mock passport strategy factory - jest.mock("@techpass/passport-openidconnect") - const mockStrategy = require("@techpass/passport-openidconnect").Strategy - - // mock the request to retrieve the oidc configuration - mockFetch.mockReturnValue({ - ok: true, - json: () => oidcConfigUrlResponse - }) - - it("should create successfully create an oidc strategy", async () => { - const oidc = require("../oidc") - const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl) - await oidc.strategyFactory(enrichedConfig, callbackUrl) - - expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) - - const expectedOptions = { - issuer: oidcConfigUrlResponse.issuer, - authorizationURL: oidcConfigUrlResponse.authorization_endpoint, - tokenURL: oidcConfigUrlResponse.token_endpoint, - userInfoURL: oidcConfigUrlResponse.userinfo_endpoint, - clientID: oidcConfig.clientID, - clientSecret: oidcConfig.clientSecret, - callbackURL: callbackUrl, - } - expect(mockStrategy).toHaveBeenCalledWith( - expectedOptions, - expect.anything() - ) - }) - }) - - describe("authenticate", () => { - afterEach(() => { - jest.clearAllMocks() - }); - - // mock third party common authentication - jest.mock("../third-party-common") - const authenticateThirdParty = require("../third-party-common").authenticateThirdParty - - // mock the passport callback - const mockDone = jest.fn() - const mockSaveUserFn = jest.fn() - - async function doAuthenticate() { - const oidc = require("../oidc") - const authenticate = await oidc.buildVerifyFn(mockSaveUserFn) - - await authenticate( - issuer, - sub, - profile, - jwtClaims, - data.accessToken, - data.refreshToken, - idToken, - params, - mockDone - ) - } - - async function doTest() { - await doAuthenticate() - - expect(authenticateThirdParty).toHaveBeenCalledWith( - user, - false, - mockDone, - mockSaveUserFn, - ) - } - - it("delegates authentication to third party common", async () => { - await doTest() - }) - - it("uses JWT email to get email", async () => { - delete profile._json.email - jwtClaims = { - email : "mock@budibase.com" - } - - await doTest() - }) - - it("uses JWT username to get email", async () => { - delete profile._json.email - jwtClaims = { - preferred_username : "mock@budibase.com" - } - - await doTest() - }) - - it("uses JWT invalid username to get email", async () => { - delete profile._json.email - - jwtClaims = { - preferred_username : "invalidUsername" - } - - await expect(doAuthenticate()).rejects.toThrow("Could not determine user email from profile"); - }) - - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js b/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js deleted file mode 100644 index d377d602f1..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/third-party-common.seq.spec.js +++ /dev/null @@ -1,178 +0,0 @@ -require("../../../../tests") -const { authenticateThirdParty } = require("../third-party-common") -const { data } = require("./utilities/mock-data") -const { DEFAULT_TENANT_ID } = require("../../../constants") - -const { generateGlobalUserID } = require("../../../db/utils") -const { newid } = require("../../../utils") -const { doWithGlobalDB, doInTenant } = require("../../../tenancy") - -const done = jest.fn() - -const getErrorMessage = () => { - return done.mock.calls[0][2].message -} - -const saveUser = async (user) => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - return await db.put(user) - }) -} - -function authenticate(user, requireLocal, saveFn) { - return doInTenant(DEFAULT_TENANT_ID, () => { - return authenticateThirdParty(user, requireLocal, done, saveFn) - }) -} - -describe("third party common", () => { - describe("authenticateThirdParty", () => { - let thirdPartyUser - - beforeEach(() => { - thirdPartyUser = data.buildThirdPartyUser() - }) - - afterEach(async () => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - jest.clearAllMocks() - await db.destroy() - }) - }) - - describe("validation", () => { - const testValidation = async (message) => { - await authenticate(thirdPartyUser, false, saveUser) - expect(done.mock.calls.length).toBe(1) - expect(getErrorMessage()).toContain(message) - } - - it("provider fails", async () => { - delete thirdPartyUser.provider - await testValidation("third party user provider required") - }) - - it("user id fails", async () => { - delete thirdPartyUser.userId - await testValidation("third party user id required") - }) - - it("email fails", async () => { - delete thirdPartyUser.email - await testValidation("third party user email required") - }) - }) - - const expectUserIsAuthenticated = () => { - const user = done.mock.calls[0][1] - expect(user).toBeDefined() - expect(user._id).toBeDefined() - expect(user._rev).toBeDefined() - expect(user.token).toBeDefined() - return user - } - - const expectUserIsSynced = (user, thirdPartyUser) => { - expect(user.provider).toBe(thirdPartyUser.provider) - expect(user.firstName).toBe(thirdPartyUser.profile.name.givenName) - expect(user.lastName).toBe(thirdPartyUser.profile.name.familyName) - expect(user.thirdPartyProfile).toStrictEqual(thirdPartyUser.profile._json) - expect(user.oauth2).toStrictEqual(thirdPartyUser.oauth2) - } - - describe("when the user doesn't exist", () => { - describe("when a local account is required", () => { - it("returns an error message", async () => { - await authenticate(thirdPartyUser, true, saveUser) - expect(done.mock.calls.length).toBe(1) - expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.") - }) - }) - - describe("when a local account isn't required", () => { - it("creates and authenticates the user", async () => { - await authenticate(thirdPartyUser, false, saveUser) - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expect(user.roles).toStrictEqual({}) - }) - }) - }) - - describe("when the user exists", () => { - let dbUser - let id - let email - - const createUser = async () => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - dbUser = { - _id: id, - email: email, - } - const response = await db.put(dbUser) - dbUser._rev = response.rev - return dbUser - }) - } - - const expectUserIsUpdated = (user) => { - // id is unchanged - expect(user._id).toBe(id) - // user is updated - expect(user._rev).not.toBe(dbUser._rev) - } - - describe("exists by email", () => { - beforeEach(async () => { - id = generateGlobalUserID(newid()) // random id - email = thirdPartyUser.email // matching email - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - }) - }) - - describe("exists by email with different casing", () => { - beforeEach(async () => { - id = generateGlobalUserID(newid()) // random id - email = thirdPartyUser.email.toUpperCase() // matching email except for casing - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - expect(user.email).toBe(thirdPartyUser.email.toUpperCase()) - }) - }) - - - describe("exists by id", () => { - beforeEach(async () => { - id = generateGlobalUserID(thirdPartyUser.userId) // matching id - email = "test@test.com" // random email - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - }) - }) - }) - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js b/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js deleted file mode 100644 index 00ae82e47e..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js +++ /dev/null @@ -1,54 +0,0 @@ -// Mock Data - -const mockClientID = "mockClientID" -const mockClientSecret = "mockClientSecret" - -const mockEmail = "mock@budibase.com" -const mockAccessToken = "mockAccessToken" -const mockRefreshToken = "mockRefreshToken" - -const mockProvider = "mockProvider" -const mockProviderType = "mockProviderType" - -const mockProfile = { - id: "mockId", - name: { - givenName: "mockGivenName", - familyName: "mockFamilyName", - }, - _json: { - email: mockEmail, - }, -} - -const buildOauth2 = ( - accessToken = mockAccessToken, - refreshToken = mockRefreshToken -) => ({ - accessToken: accessToken, - refreshToken: refreshToken, -}) - -const buildThirdPartyUser = ( - provider = mockProvider, - providerType = mockProviderType, - profile = mockProfile, - email = mockEmail, - oauth2 = buildOauth2() -) => ({ - provider: provider, - providerType: providerType, - userId: profile.id, - profile: profile, - email: email, - oauth2: oauth2, -}) - -exports.data = { - clientID: mockClientID, - clientSecret: mockClientSecret, - email: mockEmail, - accessToken: mockAccessToken, - refreshToken: mockRefreshToken, - buildThirdPartyUser, -} diff --git a/packages/backend-core/src/middleware/passport/third-party-common.ts b/packages/backend-core/src/middleware/passport/third-party-common.ts deleted file mode 100644 index 9d7b93f370..0000000000 --- a/packages/backend-core/src/middleware/passport/third-party-common.ts +++ /dev/null @@ -1,177 +0,0 @@ -import env from "../../environment" -import { generateGlobalUserID } from "../../db" -import { authError } from "./utils" -import { newid } from "../../utils" -import { createASession } from "../../security/sessions" -import * as users from "../../users" -import { getGlobalDB, getTenantId } from "../../tenancy" -import fetch from "node-fetch" -import { ThirdPartyUser } from "@budibase/types" -const jwt = require("jsonwebtoken") - -type SaveUserOpts = { - requirePassword?: boolean - hashPassword?: boolean - currentUserId?: string -} - -export type SaveUserFunction = ( - user: ThirdPartyUser, - opts: SaveUserOpts -) => Promise - -/** - * Common authentication logic for third parties. e.g. OAuth, OIDC. - */ -export async function authenticateThirdParty( - thirdPartyUser: ThirdPartyUser, - requireLocalAccount: boolean = true, - done: Function, - saveUserFn?: SaveUserFunction -) { - if (!saveUserFn) { - throw new Error("Save user function must be provided") - } - if (!thirdPartyUser.provider) { - return authError(done, "third party user provider required") - } - if (!thirdPartyUser.userId) { - return authError(done, "third party user id required") - } - if (!thirdPartyUser.email) { - return authError(done, "third party user email required") - } - - // use the third party id - const userId = generateGlobalUserID(thirdPartyUser.userId) - const db = getGlobalDB() - - let dbUser - - // try to load by id - try { - dbUser = await db.get(userId) - } catch (err: any) { - // abort when not 404 error - if (!err.status || err.status !== 404) { - return authError( - done, - "Unexpected error when retrieving existing user", - err - ) - } - } - - // fallback to loading by email - if (!dbUser) { - dbUser = await users.getGlobalUserByEmail(thirdPartyUser.email) - } - - // exit early if there is still no user and auto creation is disabled - if (!dbUser && requireLocalAccount) { - return authError( - done, - "Email does not yet exist. You must set up your local budibase account first." - ) - } - - // first time creation - if (!dbUser) { - // setup a blank user using the third party id - dbUser = { - _id: userId, - email: thirdPartyUser.email, - roles: {}, - } - } - - dbUser = await syncUser(dbUser, thirdPartyUser) - - // never prompt for password reset - dbUser.forceResetPassword = false - - // create or sync the user - try { - await saveUserFn(dbUser, { hashPassword: false, requirePassword: false }) - } catch (err: any) { - return authError(done, "Error saving user", err) - } - - // now that we're sure user exists, load them from the db - dbUser = await db.get(dbUser._id) - - // authenticate - const sessionId = newid() - const tenantId = getTenantId() - await createASession(dbUser._id, { sessionId, tenantId }) - - dbUser.token = jwt.sign( - { - userId: dbUser._id, - sessionId, - }, - env.JWT_SECRET - ) - - return done(null, dbUser) -} - -async function syncProfilePicture( - user: ThirdPartyUser, - thirdPartyUser: ThirdPartyUser -) { - const pictureUrl = thirdPartyUser.profile?._json.picture - if (pictureUrl) { - const response = await fetch(pictureUrl) - - if (response.status === 200) { - const type = response.headers.get("content-type") as string - if (type.startsWith("image/")) { - user.pictureUrl = pictureUrl - } - } - } - - return user -} - -/** - * @returns a user that has been sync'd with third party information - */ -async function syncUser(user: ThirdPartyUser, thirdPartyUser: ThirdPartyUser) { - // provider - user.provider = thirdPartyUser.provider - user.providerType = thirdPartyUser.providerType - - if (thirdPartyUser.profile) { - const profile = thirdPartyUser.profile - - if (profile.name) { - const name = profile.name - // first name - if (name.givenName) { - user.firstName = name.givenName - } - // last name - if (name.familyName) { - user.lastName = name.familyName - } - } - - user = await syncProfilePicture(user, thirdPartyUser) - - // profile - user.thirdPartyProfile = { - ...profile._json, - } - } - - // oauth tokens for future use - if (thirdPartyUser.oauth2) { - user.oauth2 = { - ...thirdPartyUser.oauth2, - } - } - - return user -} diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 1720a79a83..ef76af390d 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -8,6 +8,7 @@ import { } from "./db" import { BulkDocsResponse, User } from "@budibase/types" import { getGlobalDB } from "./context" +import * as context from "./context" export const bulkGetGlobalUsersById = async (userIds: string[]) => { const db = getGlobalDB() @@ -24,6 +25,11 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => { return (await db.bulkDocs(users)) as BulkDocsResponse } +export async function getById(id: string): Promise { + const db = context.getGlobalDB() + return db.get(id) +} + /** * Given an email address this will use a view to search through * all the users to find one with this email address. diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index c608686431..3731e134ad 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -2,23 +2,15 @@ import { getAllApps, queryGlobalView } from "../db" import { options } from "../middleware/passport/jwt" import { Header, - Cookie, MAX_VALID_DATE, DocumentType, SEPARATOR, ViewName, } from "../constants" import env from "../environment" -import * as userCache from "../cache/user" -import { getSessionsForUser, invalidateSessions } from "../security/sessions" -import * as events from "../events" import * as tenancy from "../tenancy" -import { - App, - Ctx, - PlatformLogoutOpts, - TenantResolutionStrategy, -} from "@budibase/types" +import * as context from "../context" +import { App, Ctx, TenantResolutionStrategy } from "@budibase/types" import { SetOption } from "cookies" const jwt = require("jsonwebtoken") @@ -38,7 +30,7 @@ export async function resolveAppUrl(ctx: Ctx) { const appUrl = ctx.path.split("/")[2] let possibleAppUrl = `/${appUrl.toLowerCase()}` - let tenantId: string | null = tenancy.getTenantId() + let tenantId: string | null = context.getTenantId() if (env.MULTI_TENANCY) { // always use the tenant id from the subdomain in multi tenancy // this ensures the logged-in user tenant id doesn't overwrite @@ -49,7 +41,7 @@ export async function resolveAppUrl(ctx: Ctx) { } // search prod apps for a url that matches - const apps: App[] = await tenancy.doInTenant(tenantId, () => + const apps: App[] = await context.doInTenant(tenantId, () => getAllApps({ dev: false }) ) const app = apps.filter( @@ -222,35 +214,6 @@ export async function getBuildersCount() { return builders.length } -/** - * Logs a user out from budibase. Re-used across account portal and builder. - */ -export async function platformLogout(opts: PlatformLogoutOpts) { - const ctx = opts.ctx - const userId = opts.userId - const keepActiveSession = opts.keepActiveSession - - if (!ctx) throw new Error("Koa context must be supplied to logout.") - - const currentSession = getCookie(ctx, Cookie.Auth) - let sessions = await getSessionsForUser(userId) - - if (keepActiveSession) { - sessions = sessions.filter( - session => session.sessionId !== currentSession.sessionId - ) - } else { - // clear cookies - clearCookie(ctx, Cookie.Auth) - clearCookie(ctx, Cookie.CurrentApp) - } - - const sessionIds = sessions.map(({ sessionId }) => sessionId) - await invalidateSessions(userId, { sessionIds, reason: "logout" }) - await events.auth.logout() - await userCache.invalidateUser(userId) -} - export function timeout(timeMs: number) { return new Promise(resolve => setTimeout(resolve, timeMs)) } diff --git a/packages/backend-core/tests/utilities/mocks/accounts.ts b/packages/backend-core/tests/utilities/mocks/accounts.ts deleted file mode 100644 index e40d32b276..0000000000 --- a/packages/backend-core/tests/utilities/mocks/accounts.ts +++ /dev/null @@ -1,13 +0,0 @@ -const mockGetAccount = jest.fn() -const mockGetAccountByTenantId = jest.fn() -const mockGetStatus = jest.fn() - -jest.mock("../../../src/cloud/accounts", () => ({ - getAccount: mockGetAccount, - getAccountByTenantId: mockGetAccountByTenantId, - getStatus: mockGetStatus, -})) - -export const getAccount = mockGetAccount -export const getAccountByTenantId = mockGetAccountByTenantId -export const getStatus = mockGetStatus diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts index 401fd7d7a7..f5f45c0342 100644 --- a/packages/backend-core/tests/utilities/mocks/index.ts +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -1,4 +1,7 @@ -export * as accounts from "./accounts" +jest.mock("../../../src/accounts") +import * as _accounts from "../../../src/accounts" +export const accounts = jest.mocked(_accounts) + export * as date from "./date" export * as licenses from "./licenses" export { default as fetch } from "./fetch" diff --git a/packages/backend-core/tests/utilities/structures/accounts.ts b/packages/backend-core/tests/utilities/structures/accounts.ts index f1718aecc0..6bfeedf196 100644 --- a/packages/backend-core/tests/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/utilities/structures/accounts.ts @@ -1,6 +1,15 @@ import { generator, uuid } from "." import * as db from "../../../src/db/utils" -import { Account, AuthType, CloudAccount, Hosting } from "@budibase/types" +import { + Account, + AccountSSOProvider, + AccountSSOProviderType, + AuthType, + CloudAccount, + Hosting, + SSOAccount, +} from "@budibase/types" +import _ from "lodash" export const account = (): Account => { return { @@ -27,3 +36,28 @@ export const cloudAccount = (): CloudAccount => { budibaseUserId: db.generateGlobalUserID(), } } + +function providerType(): AccountSSOProviderType { + return _.sample( + Object.values(AccountSSOProviderType) + ) as AccountSSOProviderType +} + +function provider(): AccountSSOProvider { + return _.sample(Object.values(AccountSSOProvider)) as AccountSSOProvider +} + +export function ssoAccount(): SSOAccount { + return { + ...cloudAccount(), + authType: AuthType.SSO, + oauth2: { + accessToken: generator.string(), + refreshToken: generator.string(), + }, + pictureUrl: generator.url(), + provider: provider(), + providerType: providerType(), + thirdPartyProfile: {}, + } +} diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts index e74751e479..d0073ba851 100644 --- a/packages/backend-core/tests/utilities/structures/index.ts +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -5,8 +5,10 @@ export const generator = new Chance() export * as accounts from "./accounts" export * as apps from "./apps" +export * as db from "./db" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" +export * as sso from "./sso" export * as tenant from "./tenants" -export * as db from "./db" +export * as users from "./users" diff --git a/packages/backend-core/tests/utilities/structures/sso.ts b/packages/backend-core/tests/utilities/structures/sso.ts new file mode 100644 index 0000000000..ad5e8e87ef --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/sso.ts @@ -0,0 +1,100 @@ +import { + GoogleInnerConfig, + JwtClaims, + OIDCInnerConfig, + OIDCWellKnownConfig, + SSOAuthDetails, + SSOProfile, + SSOProviderType, + User, +} from "@budibase/types" +import { uuid, generator, users, email } from "./index" +import _ from "lodash" + +export function providerType(): SSOProviderType { + return _.sample(Object.values(SSOProviderType)) as SSOProviderType +} + +export function ssoProfile(user?: User): SSOProfile { + if (!user) { + user = users.user() + } + return { + id: user._id!, + name: { + givenName: user.firstName, + familyName: user.lastName, + }, + _json: { + email: user.email, + picture: "http://test.com", + }, + provider: generator.string(), + } +} + +export function authDetails(user?: User): SSOAuthDetails { + if (!user) { + user = users.user() + } + + const userId = user._id || uuid() + const provider = generator.string() + + const profile = ssoProfile(user) + profile.provider = provider + profile.id = userId + + return { + email: user.email, + oauth2: { + refreshToken: generator.string(), + accessToken: generator.string(), + }, + profile, + provider, + providerType: providerType(), + userId, + } +} + +// OIDC + +export function oidcConfig(): OIDCInnerConfig { + return { + uuid: uuid(), + activated: true, + logo: "", + name: generator.string(), + configUrl: "http://someconfigurl", + clientID: generator.string(), + clientSecret: generator.string(), + } +} + +// response from .well-known/openid-configuration +export function oidcWellKnownConfig(): OIDCWellKnownConfig { + return { + issuer: generator.string(), + authorization_endpoint: generator.url(), + token_endpoint: generator.url(), + userinfo_endpoint: generator.url(), + } +} + +export function jwtClaims(): JwtClaims { + return { + email: email(), + preferred_username: email(), + } +} + +// GOOGLE + +export function googleConfig(): GoogleInnerConfig { + return { + activated: true, + clientID: generator.string(), + clientSecret: generator.string(), + } +} diff --git a/packages/backend-core/tests/utilities/structures/users.ts b/packages/backend-core/tests/utilities/structures/users.ts new file mode 100644 index 0000000000..332c27ca12 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/users.ts @@ -0,0 +1,70 @@ +import { generator } from "../" +import { + AdminUser, + BuilderUser, + SSOAuthDetails, + SSOUser, + User, +} from "@budibase/types" +import { v4 as uuid } from "uuid" +import * as sso from "./sso" + +export const newEmail = () => { + return `${uuid()}@test.com` +} + +export const user = (userProps?: any): User => { + return { + email: newEmail(), + password: "test", + roles: { app_test: "admin" }, + firstName: generator.first(), + lastName: generator.last(), + pictureUrl: "http://test.com", + ...userProps, + } +} + +export const adminUser = (userProps?: any): AdminUser => { + return { + ...user(userProps), + admin: { + global: true, + }, + builder: { + global: true, + }, + } +} + +export const builderUser = (userProps?: any): BuilderUser => { + return { + ...user(userProps), + builder: { + global: true, + }, + } +} + +export function ssoUser( + opts: { user?: any; details?: SSOAuthDetails } = {} +): SSOUser { + const base = user(opts.user) + delete base.password + + if (!opts.details) { + opts.details = sso.authDetails(base) + } + + return { + ...base, + forceResetPassword: false, + oauth2: opts.details?.oauth2, + provider: opts.details?.provider!, + providerType: opts.details?.providerType!, + thirdPartyProfile: { + email: base.email, + picture: base.pictureUrl, + }, + } +} diff --git a/packages/builder/src/pages/builder/portal/_components/UserDropdown.svelte b/packages/builder/src/pages/builder/portal/_components/UserDropdown.svelte index d396edd4e2..935d69812f 100644 --- a/packages/builder/src/pages/builder/portal/_components/UserDropdown.svelte +++ b/packages/builder/src/pages/builder/portal/_components/UserDropdown.svelte @@ -30,9 +30,11 @@ My profile themeModal.show()}>Theme - updatePasswordModal.show()}> - Update password - + {#if !$auth.isSSO} + updatePasswordModal.show()}> + Update password + + {/if} apiKeyModal.show()}> View API key diff --git a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte index b14e3420cc..1c26273a8d 100644 --- a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte @@ -81,6 +81,7 @@ let user let loaded = false + $: isSSO = !!user?.provider $: readonly = !$auth.isAdmin $: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : "" $: privileged = user?.admin?.global || user?.builder?.global @@ -246,9 +247,11 @@ - - Force password reset - + {#if !isSSO} + + Force password reset + + {/if} Delete diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index b10cd05e00..d039bb6eec 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -41,6 +41,7 @@ export function createAuthStore() { initials, isAdmin, isBuilder, + isSSO: !!$store.user?.provider, } }) diff --git a/packages/server/specs/openapi.json b/packages/server/specs/openapi.json index 9a0d69e352..cf4eef75a9 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -817,7 +817,6 @@ "type": "string", "enum": [ "string", - "barcodeqr", "longform", "options", "number", @@ -829,7 +828,8 @@ "formula", "auto", "json", - "internal" + "internal", + "barcodeqr" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, @@ -1021,7 +1021,6 @@ "type": "string", "enum": [ "string", - "barcodeqr", "longform", "options", "number", @@ -1033,7 +1032,8 @@ "formula", "auto", "json", - "internal" + "internal", + "barcodeqr" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, @@ -1236,7 +1236,6 @@ "type": "string", "enum": [ "string", - "barcodeqr", "longform", "options", "number", @@ -1248,7 +1247,8 @@ "formula", "auto", "json", - "internal" + "internal", + "barcodeqr" ], "description": "Defines the type of the column, most explain themselves, a link column is a relationship." }, diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index 69e44d881c..414efe7adb 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -603,7 +603,6 @@ components: type: string enum: - string - - barcodeqr - longform - options - number @@ -616,6 +615,7 @@ components: - auto - json - internal + - barcodeqr description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: @@ -766,7 +766,6 @@ components: type: string enum: - string - - barcodeqr - longform - options - number @@ -779,6 +778,7 @@ components: - auto - json - internal + - barcodeqr description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: @@ -936,7 +936,6 @@ components: type: string enum: - string - - barcodeqr - longform - options - number @@ -949,6 +948,7 @@ components: - auto - json - internal + - barcodeqr description: Defines the type of the column, most explain themselves, a link column is a relationship. constraints: diff --git a/packages/server/src/integrations/tests/couchdb.spec.ts b/packages/server/src/integrations/tests/couchdb.spec.ts index 0d744cd343..66735d7b74 100644 --- a/packages/server/src/integrations/tests/couchdb.spec.ts +++ b/packages/server/src/integrations/tests/couchdb.spec.ts @@ -1,5 +1,3 @@ -import { DatabaseWithConnection } from "@budibase/backend-core/src/db" - jest.mock("@budibase/backend-core", () => { const core = jest.requireActual("@budibase/backend-core") return { diff --git a/packages/types/src/api/account/index.ts b/packages/types/src/api/account/index.ts index 0cbc487bcc..4be610d1b3 100644 --- a/packages/types/src/api/account/index.ts +++ b/packages/types/src/api/account/index.ts @@ -1,2 +1,3 @@ export * from "./user" export * from "./license" +export * from "./status" diff --git a/packages/types/src/api/account/status.ts b/packages/types/src/api/account/status.ts new file mode 100644 index 0000000000..f6a0db7a5e --- /dev/null +++ b/packages/types/src/api/account/status.ts @@ -0,0 +1,7 @@ +export interface HealthStatusResponse { + passing: boolean + checks: { + login: boolean + search: boolean + } +} diff --git a/packages/types/src/api/web/auth.ts b/packages/types/src/api/web/auth.ts new file mode 100644 index 0000000000..e31a151c48 --- /dev/null +++ b/packages/types/src/api/web/auth.ts @@ -0,0 +1,25 @@ +export interface LoginRequest { + username: string + password: string +} + +export interface PasswordResetRequest { + email: string +} + +export interface PasswordResetUpdateRequest { + resetCode: string + password: string +} + +export interface UpdateSelfRequest { + firstName?: string + lastName?: string + password?: string + forceResetPassword?: boolean +} + +export interface UpdateSelfResponse { + _id: string + _rev: string +} diff --git a/packages/types/src/api/web/index.ts b/packages/types/src/api/web/index.ts index 9688a89c7b..8ed5b0aad4 100644 --- a/packages/types/src/api/web/index.ts +++ b/packages/types/src/api/web/index.ts @@ -1,4 +1,5 @@ export * from "./analytics" +export * from "./auth" export * from "./user" export * from "./errors" export * from "./schedule" diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 6acaf6912d..a435808f7e 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -1,6 +1,6 @@ import { User } from "../../documents" -export interface CreateUserResponse { +export interface SaveUserResponse { _id: string _rev: string email: string @@ -58,6 +58,25 @@ export interface CreateAdminUserRequest { tenantId: string } +export interface CreateAdminUserResponse { + _id: string + _rev: string + email: string +} + +export interface AcceptUserInviteRequest { + inviteCode: string + password: string + firstName: string + lastName: string +} + +export interface AcceptUserInviteResponse { + _id: string + _rev: string + email: string +} + export interface SyncUserRequest { previousUser?: User } diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index a8684f8427..cacdc4e9a2 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -79,14 +79,24 @@ export const isSelfHostAccount = (account: Account) => export const isSSOAccount = (account: Account): account is SSOAccount => account.authType === AuthType.SSO -export interface SSOAccount extends Account { - pictureUrl?: string - provider?: string - providerType?: string +export enum AccountSSOProviderType { + GOOGLE = "google", +} + +export enum AccountSSOProvider { + GOOGLE = "google", +} + +export interface AccountSSO { + provider: AccountSSOProvider + providerType: AccountSSOProviderType oauth2?: OAuthTokens + pictureUrl?: string thirdPartyProfile: any // TODO: define what the google profile looks like } +export type SSOAccount = (Account | CloudAccount) & AccountSSO + export enum AuthType { SSO = "sso", PASSWORD = "password", diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts index 9b05069c9a..99dec534b6 100644 --- a/packages/types/src/documents/global/config.ts +++ b/packages/types/src/documents/global/config.ts @@ -27,15 +27,17 @@ export interface SettingsConfig extends Config { } } -export interface GoogleConfig extends Config { - config: { - clientID: string - clientSecret: string - activated: boolean - } +export interface GoogleInnerConfig { + clientID: string + clientSecret: string + activated: boolean } -export interface OIDCConfiguration { +export interface GoogleConfig extends Config { + config: GoogleInnerConfig +} + +export interface OIDCStrategyConfiguration { issuer: string authorizationURL: string tokenURL: string @@ -45,7 +47,7 @@ export interface OIDCConfiguration { callbackURL: string } -export interface OIDCInnerCfg { +export interface OIDCInnerConfig { configUrl: string clientID: string clientSecret: string @@ -57,10 +59,17 @@ export interface OIDCInnerCfg { export interface OIDCConfig extends Config { config: { - configs: OIDCInnerCfg[] + configs: OIDCInnerConfig[] } } +export interface OIDCWellKnownConfig { + issuer: string + authorization_endpoint: string + token_endpoint: string + userinfo_endpoint: string +} + export const isSettingsConfig = (config: Config): config is SettingsConfig => config.type === ConfigType.SETTINGS diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index aef36c3469..2ef13a7412 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -1,37 +1,44 @@ import { Document } from "../document" -export interface SSOProfile { - id: string - name?: { - givenName?: string - familyName?: string - } - _json: { - email: string - picture: string - } - provider?: string +// SSO + +export interface SSOProfileJson { + email?: string + picture?: string } -export interface ThirdPartyUser extends Document { - thirdPartyProfile?: SSOProfile["_json"] - firstName?: string - lastName?: string - pictureUrl?: string - profile?: SSOProfile - oauth2?: any - provider?: string - providerType?: string - email: string - userId?: string - forceResetPassword?: boolean - userGroups?: string[] +export interface OAuth2 { + accessToken: string + refreshToken?: string } -export interface User extends ThirdPartyUser { +export enum SSOProviderType { + OIDC = "oidc", + GOOGLE = "google", +} + +export interface UserSSO { + provider: string // the individual provider e.g. Okta, Auth0, Google + providerType: SSOProviderType + oauth2?: OAuth2 + thirdPartyProfile?: SSOProfileJson +} + +export type SSOUser = User & UserSSO + +export function isSSOUser(user: User): user is SSOUser { + return !!(user as SSOUser).providerType +} + +// USER + +export interface User extends Document { tenantId: string email: string userId?: string + firstName?: string + lastName?: string + pictureUrl?: string forceResetPassword?: boolean roles: UserRoles builder?: { @@ -44,9 +51,7 @@ export interface User extends ThirdPartyUser { status?: string createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now() dayPassRecordedAt?: string - account?: { - authType: string - } + userGroups?: string[] onboardedAt?: string } @@ -54,7 +59,7 @@ export interface UserRoles { [key: string]: string } -// utility types +// UTILITY TYPES export interface BuilderUser extends User { builder: { diff --git a/packages/types/src/sdk/index.ts b/packages/types/src/sdk/index.ts index f8f9d9cb97..be12d45527 100644 --- a/packages/types/src/sdk/index.ts +++ b/packages/types/src/sdk/index.ts @@ -12,3 +12,5 @@ export * from "./db" export * from "./middleware" export * from "./featureFlag" export * from "./environmentVariables" +export * from "./sso" +export * from "./user" diff --git a/packages/types/src/sdk/sso.ts b/packages/types/src/sdk/sso.ts new file mode 100644 index 0000000000..2141204ccb --- /dev/null +++ b/packages/types/src/sdk/sso.ts @@ -0,0 +1,37 @@ +import { + OAuth2, + SSOProfileJson, + SSOProviderType, + SSOUser, + User, +} from "../documents" +import { SaveUserOpts } from "./user" + +export interface JwtClaims { + preferred_username?: string + email?: string +} + +export interface SSOAuthDetails { + oauth2: OAuth2 + provider: string + providerType: SSOProviderType + userId: string + email?: string + profile?: SSOProfile +} + +export interface SSOProfile { + id: string + name?: { + givenName?: string + familyName?: string + } + _json: SSOProfileJson + provider?: string +} + +export type SaveSSOUserFunction = ( + user: SSOUser, + opts: SaveUserOpts +) => Promise diff --git a/packages/types/src/sdk/user.ts b/packages/types/src/sdk/user.ts new file mode 100644 index 0000000000..1602eeb6c8 --- /dev/null +++ b/packages/types/src/sdk/user.ts @@ -0,0 +1,12 @@ +export interface UpdateSelf { + firstName?: string + lastName?: string + password?: string + forceResetPassword?: boolean +} + +export interface SaveUserOpts { + hashPassword?: boolean + requirePassword?: boolean + currentUserId?: string +} diff --git a/packages/worker/scripts/dev/manage.js b/packages/worker/scripts/dev/manage.js index 01cdef08ff..21a9eab9c6 100644 --- a/packages/worker/scripts/dev/manage.js +++ b/packages/worker/scripts/dev/manage.js @@ -29,6 +29,7 @@ async function init() { SERVICE: "worker-service", DEPLOYMENT_ENVIRONMENT: "development", TENANT_FEATURE_FLAGS: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR", + ENABLE_EMAIL_TEST_MODE: 1, } let envFile = "" Object.keys(envFileJson).forEach(key => { diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 738b67c553..948a98cf3a 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -1,49 +1,55 @@ import { - auth, + auth as authCore, constants, context, db as dbCore, events, tenancy, - users as usersCore, - utils, + utils as utilsCore, } from "@budibase/backend-core" -import { EmailTemplatePurpose } from "../../../constants" -import { isEmailConfigured, sendEmail } from "../../../utilities/email" -import { checkResetPasswordCode } from "../../../utilities/redis" +import { + ConfigType, + User, + Ctx, + LoginRequest, + SSOUser, + PasswordResetRequest, + PasswordResetUpdateRequest, +} from "@budibase/types" import env from "../../../environment" -import sdk from "../../../sdk" -import { ConfigType, User } from "@budibase/types" -const { setCookie, getCookie, clearCookie, hash, platformLogout } = utils +import * as authSdk from "../../../sdk/auth" +import * as userSdk from "../../../sdk/users" + const { Cookie, Header } = constants -const { passport, ssoCallbackUrl, google, oidc } = auth +const { passport, ssoCallbackUrl, google, oidc } = authCore +const { setCookie, getCookie, clearCookie } = utilsCore -export async function googleCallbackUrl(config?: { callbackURL?: string }) { - return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.GOOGLE) -} +// LOGIN / LOGOUT -export async function oidcCallbackUrl(config?: { callbackURL?: string }) { - return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.OIDC) -} - -async function authInternal(ctx: any, user: any, err: any = null, info = null) { +async function passportCallback( + ctx: Ctx, + user: User, + err: any = null, + info: { message: string } | null = null +) { if (err) { console.error("Authentication error") console.error(err) console.trace(err) return ctx.throw(403, info ? info : "Unauthorized") } - if (!user) { console.error("Authentication error - no user provided") return ctx.throw(403, info ? info : "Unauthorized") } + const token = await authSdk.loginUser(user) + // set a cookie for browser access - setCookie(ctx, user.token, Cookie.Auth, { sign: false }) + setCookie(ctx, token, Cookie.Auth, { sign: false }) // set the token in a header as well for APIs - ctx.set(Header.TOKEN, user.token) + ctx.set(Header.TOKEN, token) // get rid of any app cookies on login // have to check test because this breaks cypress if (!env.isTest()) { @@ -51,11 +57,18 @@ async function authInternal(ctx: any, user: any, err: any = null, info = null) { } } -export const authenticate = async (ctx: any, next: any) => { +export const login = async (ctx: Ctx, next: any) => { + const email = ctx.request.body.username + + const user = await userSdk.getUserByEmail(email) + if (user && (await userSdk.isPreventSSOPasswords(user))) { + ctx.throw(400, "SSO user cannot login using password") + } + return passport.authenticate( "local", async (err: any, user: User, info: any) => { - await authInternal(ctx, user, err, info) + await passportCallback(ctx, user, err, info) await context.identity.doInUserContext(user, async () => { await events.auth.login("local") }) @@ -64,6 +77,15 @@ export const authenticate = async (ctx: any, next: any) => { )(ctx, next) } +export const logout = async (ctx: any) => { + if (ctx.user && ctx.user._id) { + await authSdk.logout({ ctx, userId: ctx.user._id }) + } + ctx.body = { message: "User logged out." } +} + +// INIT + export const setInitInfo = (ctx: any) => { const initInfo = ctx.request.body setCookie(ctx, initInfo, Cookie.Init) @@ -79,32 +101,16 @@ export const getInitInfo = (ctx: any) => { } } +// PASSWORD MANAGEMENT + /** * Reset the user password, used as part of a forgotten password flow. */ -export const reset = async (ctx: any) => { +export const reset = async (ctx: Ctx) => { const { email } = ctx.request.body - const configured = await isEmailConfigured() - if (!configured) { - ctx.throw( - 400, - "Please contact your platform administrator, SMTP is not configured." - ) - } - try { - const user = (await usersCore.getGlobalUserByEmail(email)) as User - // only if user exists, don't error though if they don't - if (user) { - await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { - user, - subject: "{{ company }} platform password reset", - }) - await events.user.passwordResetRequested(user) - } - } catch (err) { - console.log(err) - // don't throw any kind of error to the user, this might give away something - } + + await authSdk.reset(email) + ctx.body = { message: "Please check your email for a reset link.", } @@ -113,32 +119,21 @@ export const reset = async (ctx: any) => { /** * Perform the user password update if the provided reset code is valid. */ -export const resetUpdate = async (ctx: any) => { +export const resetUpdate = async (ctx: Ctx) => { const { resetCode, password } = ctx.request.body try { - const { userId } = await checkResetPasswordCode(resetCode) - const db = tenancy.getGlobalDB() - const user = await db.get(userId) - user.password = await hash(password) - await db.put(user) + await authSdk.resetUpdate(resetCode, password) ctx.body = { message: "password reset successfully.", } - // remove password from the user before sending events - delete user.password - await events.user.passwordReset(user) } catch (err) { - console.error(err) + console.warn(err) + // hide any details of the error for security ctx.throw(400, "Cannot reset password.") } } -export const logout = async (ctx: any) => { - if (ctx.user && ctx.user._id) { - await platformLogout({ ctx, userId: ctx.user._id }) - } - ctx.body = { message: "User logged out." } -} +// DATASOURCE export const datasourcePreAuth = async (ctx: any, next: any) => { const provider = ctx.params.provider @@ -166,6 +161,12 @@ export const datasourceAuth = async (ctx: any, next: any) => { return handler.postAuth(passport, ctx, next) } +// GOOGLE SSO + +export async function googleCallbackUrl(config?: { callbackURL?: string }) { + return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.GOOGLE) +} + /** * The initial call that google authentication makes to take you to the google login screen. * On a successful login, you will be redirected to the googleAuth callback route. @@ -181,7 +182,7 @@ export const googlePreAuth = async (ctx: any, next: any) => { const strategy = await google.strategyFactory( config, callbackUrl, - sdk.users.save + userSdk.save ) return passport.authenticate(strategy, { @@ -191,7 +192,7 @@ export const googlePreAuth = async (ctx: any, next: any) => { })(ctx, next) } -export const googleAuth = async (ctx: any, next: any) => { +export const googleCallback = async (ctx: any, next: any) => { const db = tenancy.getGlobalDB() const config = await dbCore.getScopedConfig(db, { @@ -202,14 +203,14 @@ export const googleAuth = async (ctx: any, next: any) => { const strategy = await google.strategyFactory( config, callbackUrl, - sdk.users.save + userSdk.save ) return passport.authenticate( strategy, { successRedirect: "/", failureRedirect: "/error" }, - async (err: any, user: User, info: any) => { - await authInternal(ctx, user, err, info) + async (err: any, user: SSOUser, info: any) => { + await passportCallback(ctx, user, err, info) await context.identity.doInUserContext(user, async () => { await events.auth.login("google-internal") }) @@ -218,6 +219,12 @@ export const googleAuth = async (ctx: any, next: any) => { )(ctx, next) } +// OIDC SSO + +export async function oidcCallbackUrl(config?: { callbackURL?: string }) { + return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.OIDC) +} + export const oidcStrategyFactory = async (ctx: any, configId: any) => { const db = tenancy.getGlobalDB() const config = await dbCore.getScopedConfig(db, { @@ -233,7 +240,7 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => { chosenConfig, callbackUrl ) - return oidc.strategyFactory(enrichedConfig, sdk.users.save) + return oidc.strategyFactory(enrichedConfig, userSdk.save) } /** @@ -265,15 +272,15 @@ export const oidcPreAuth = async (ctx: any, next: any) => { })(ctx, next) } -export const oidcAuth = async (ctx: any, next: any) => { +export const oidcCallback = async (ctx: any, next: any) => { const configId = getCookie(ctx, Cookie.OIDC_CONFIG) const strategy = await oidcStrategyFactory(ctx, configId) return passport.authenticate( strategy, { successRedirect: "/", failureRedirect: "/error" }, - async (err: any, user: any, info: any) => { - await authInternal(ctx, user, err, info) + async (err: any, user: SSOUser, info: any) => { + await passportCallback(ctx, user, err, info) await context.identity.doInUserContext(user, async () => { await events.auth.login("oidc") }) diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts index 06906f1e8e..889a3e6a27 100644 --- a/packages/worker/src/api/controllers/global/self.ts +++ b/packages/worker/src/api/controllers/global/self.ts @@ -1,18 +1,22 @@ -import sdk from "../../../sdk" +import * as userSdk from "../../../sdk/users" import { - events, featureFlags, tenancy, constants, db as dbCore, utils, - cache, encryption, + auth as authCore, } from "@budibase/backend-core" import env from "../../../environment" import { groups } from "@budibase/pro" -const { hash, platformLogout, getCookie, clearCookie, newid } = utils -const { user: userCache } = cache +import { + UpdateSelfRequest, + UpdateSelfResponse, + UpdateSelf, + UserCtx, +} from "@budibase/types" +const { getCookie, clearCookie, newid } = utils function newTestApiKey() { return env.ENCRYPTED_TEST_PUBLIC_API_KEY @@ -93,17 +97,6 @@ const addSessionAttributesToUser = (ctx: any) => { ctx.body.csrfToken = ctx.user.csrfToken } -const sanitiseUserUpdate = (ctx: any) => { - const allowed = ["firstName", "lastName", "password", "forceResetPassword"] - const resp: { [key: string]: any } = {} - for (let [key, value] of Object.entries(ctx.request.body)) { - if (allowed.includes(key)) { - resp[key] = value - } - } - return resp -} - export async function getSelf(ctx: any) { if (!ctx.user) { ctx.throw(403, "User not logged in") @@ -116,7 +109,7 @@ export async function getSelf(ctx: any) { checkCurrentApp(ctx) // get the main body of the user - const user = await sdk.users.getUser(userId) + const user = await userSdk.getUser(userId) ctx.body = await groups.enrichUserRolesFromGroups(user) // add the feature flags for this tenant @@ -126,39 +119,30 @@ export async function getSelf(ctx: any) { addSessionAttributesToUser(ctx) } -export async function updateSelf(ctx: any) { - const db = tenancy.getGlobalDB() - const user = await db.get(ctx.user._id) - let passwordChange = false +export async function updateSelf( + ctx: UserCtx +) { + const body = ctx.request.body + const update: UpdateSelf = { + firstName: body.firstName, + lastName: body.lastName, + password: body.password, + forceResetPassword: body.forceResetPassword, + } - const userUpdateObj = sanitiseUserUpdate(ctx) - if (userUpdateObj.password) { - // changing password - passwordChange = true - userUpdateObj.password = await hash(userUpdateObj.password) + const user = await userSdk.updateSelf(ctx.user._id!, update) + + if (update.password) { // Log all other sessions out apart from the current one - await platformLogout({ + await authCore.platformLogout({ ctx, - userId: ctx.user._id, + userId: ctx.user._id!, keepActiveSession: true, }) } - const response = await db.put({ - ...user, - ...userUpdateObj, - }) - await userCache.invalidateUser(user._id) ctx.body = { - _id: response.id, - _rev: response.rev, - } - - // remove the old password from the user before sending events - user._rev = response.rev - delete user.password - await events.user.updated(user) - if (passwordChange) { - await events.user.passwordUpdated(user) + _id: user._id!, + _rev: user._rev!, } } diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 43ec23eade..c722d27faa 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -2,15 +2,21 @@ import { checkInviteCode } from "../../../utilities/redis" import * as userSdk from "../../../sdk/users" import env from "../../../environment" import { + AcceptUserInviteRequest, + AcceptUserInviteResponse, BulkUserRequest, BulkUserResponse, CloudAccount, CreateAdminUserRequest, + CreateAdminUserResponse, + Ctx, InviteUserRequest, InviteUsersRequest, MigrationType, + SaveUserResponse, SearchUsersRequest, User, + UserCtx, } from "@budibase/types" import { accounts, @@ -25,10 +31,18 @@ import { checkAnyUserExists } from "../../../utilities/users" const MAX_USERS_UPLOAD_LIMIT = 1000 -export const save = async (ctx: any) => { +export const save = async (ctx: UserCtx) => { try { const currentUserId = ctx.user._id - ctx.body = await userSdk.save(ctx.request.body, { currentUserId }) + const requestUser = ctx.request.body + + const user = await userSdk.save(requestUser, { currentUserId }) + + ctx.body = { + _id: user._id!, + _rev: user._rev!, + email: user.email, + } } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -71,9 +85,10 @@ const parseBooleanParam = (param: any) => { return !(param && param === "false") } -export const adminUser = async (ctx: any) => { - const { email, password, tenantId } = ctx.request - .body as CreateAdminUserRequest +export const adminUser = async ( + ctx: Ctx +) => { + const { email, password, tenantId } = ctx.request.body if (await platform.tenants.exists(tenantId)) { ctx.throw(403, "Organisation already exists.") @@ -131,7 +146,11 @@ export const adminUser = async (ctx: any) => { } await events.identification.identifyTenantGroup(tenantId, account) - ctx.body = finalUser + ctx.body = { + _id: finalUser._id!, + _rev: finalUser._rev!, + email: finalUser.email, + } } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -236,12 +255,14 @@ export const checkInvite = async (ctx: any) => { } } -export const inviteAccept = async (ctx: any) => { +export const inviteAccept = async ( + ctx: Ctx +) => { const { inviteCode, password, firstName, lastName } = ctx.request.body try { // info is an extension of the user object that was stored by global const { email, info }: any = await checkInviteCode(inviteCode) - ctx.body = await tenancy.doInTenant(info.tenantId, async () => { + const user = await tenancy.doInTenant(info.tenantId, async () => { const saved = await userSdk.save({ firstName, lastName, @@ -254,6 +275,12 @@ export const inviteAccept = async (ctx: any) => { await events.user.inviteAccepted(user) return saved }) + + ctx.body = { + _id: user._id, + _rev: user._rev, + email: user.email, + } } catch (err: any) { if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) { // explicitly re-throw limit exceeded errors diff --git a/packages/worker/src/api/controllers/system/accounts.ts b/packages/worker/src/api/controllers/system/accounts.ts index 0aa5f25785..10b809c0b5 100644 --- a/packages/worker/src/api/controllers/system/accounts.ts +++ b/packages/worker/src/api/controllers/system/accounts.ts @@ -1,21 +1,23 @@ import { Account, AccountMetadata } from "@budibase/types" -import sdk from "../../../sdk" +import * as accounts from "../../../sdk/accounts" export const save = async (ctx: any) => { const account = ctx.request.body as Account let metadata: AccountMetadata = { - _id: sdk.accounts.formatAccountMetadataId(account.accountId), + _id: accounts.metadata.formatAccountMetadataId(account.accountId), email: account.email, } - metadata = await sdk.accounts.saveMetadata(metadata) + metadata = await accounts.metadata.saveMetadata(metadata) ctx.body = metadata ctx.status = 200 } export const destroy = async (ctx: any) => { - const accountId = sdk.accounts.formatAccountMetadataId(ctx.params.accountId) - await sdk.accounts.destroyMetadata(accountId) + const accountId = accounts.metadata.formatAccountMetadataId( + ctx.params.accountId + ) + await accounts.metadata.destroyMetadata(accountId) ctx.status = 204 } diff --git a/packages/worker/src/api/routes/global/auth.ts b/packages/worker/src/api/routes/global/auth.ts index b13cef2fc6..503182a418 100644 --- a/packages/worker/src/api/routes/global/auth.ts +++ b/packages/worker/src/api/routes/global/auth.ts @@ -33,7 +33,7 @@ router .post( "/api/global/auth/:tenantId/login", buildAuthValidation(), - authController.authenticate + authController.login ) .post("/api/global/auth/logout", authController.logout) .post( @@ -68,21 +68,24 @@ router // GOOGLE - MULTI TENANT .get("/api/global/auth/:tenantId/google", authController.googlePreAuth) - .get("/api/global/auth/:tenantId/google/callback", authController.googleAuth) + .get( + "/api/global/auth/:tenantId/google/callback", + authController.googleCallback + ) // GOOGLE - SINGLE TENANT - DEPRECATED - .get("/api/global/auth/google/callback", authController.googleAuth) - .get("/api/admin/auth/google/callback", authController.googleAuth) + .get("/api/global/auth/google/callback", authController.googleCallback) + .get("/api/admin/auth/google/callback", authController.googleCallback) // OIDC - MULTI TENANT .get( "/api/global/auth/:tenantId/oidc/configs/:configId", authController.oidcPreAuth ) - .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth) + .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcCallback) // OIDC - SINGLE TENANT - DEPRECATED - .get("/api/global/auth/oidc/callback", authController.oidcAuth) - .get("/api/admin/auth/oidc/callback", authController.oidcAuth) + .get("/api/global/auth/oidc/callback", authController.oidcCallback) + .get("/api/admin/auth/oidc/callback", authController.oidcCallback) export default router diff --git a/packages/worker/src/api/routes/global/tests/auth.spec.ts b/packages/worker/src/api/routes/global/tests/auth.spec.ts index ee753d49dc..84f8ce1b0a 100644 --- a/packages/worker/src/api/routes/global/tests/auth.spec.ts +++ b/packages/worker/src/api/routes/global/tests/auth.spec.ts @@ -1,13 +1,27 @@ -jest.mock("nodemailer") -import { TestConfiguration, mocks } from "../../../../tests" -const sendMailMock = mocks.email.mock() -import { events, tenancy } from "@budibase/backend-core" -import { structures } from "@budibase/backend-core/tests" +import { CloudAccount, SSOUser, User } from "@budibase/types" -const expectSetAuthCookie = (res: any) => { - expect( - res.get("Set-Cookie").find((c: string) => c.startsWith("budibase:auth")) - ).toBeDefined() +jest.mock("nodemailer") +import { + TestConfiguration, + mocks, + structures, + generator, +} from "../../../../tests" +const sendMailMock = mocks.email.mock() +import { events, constants } from "@budibase/backend-core" +import { Response } from "superagent" + +import * as userSdk from "../../../../sdk/users" + +function getAuthCookie(response: Response) { + return response.headers["set-cookie"] + .find((s: string) => s.startsWith(`${constants.Cookie.Auth}=`)) + .split("=")[1] + .split(";")[0] +} + +const expectSetAuthCookie = (response: Response) => { + expect(getAuthCookie(response).length > 1).toBe(true) } describe("/api/global/auth", () => { @@ -25,60 +39,247 @@ describe("/api/global/auth", () => { jest.clearAllMocks() }) + async function createSSOUser() { + return config.doInTenant(async () => { + return userSdk.save(structures.users.ssoUser(), { + requirePassword: false, + }) + }) + } + describe("password", () => { describe("POST /api/global/auth/:tenantId/login", () => { - it("should login", () => {}) + it("logs in with correct credentials", async () => { + const tenantId = config.tenantId! + const email = config.user?.email! + const password = config.userPassword + + const response = await config.api.auth.login(tenantId, email, password) + + expectSetAuthCookie(response) + expect(events.auth.login).toBeCalledTimes(1) + }) + + it("should return 403 with incorrect credentials", async () => { + const tenantId = config.tenantId! + const email = config.user?.email! + const password = "incorrect" + + const response = await config.api.auth.login( + tenantId, + email, + password, + { status: 403 } + ) + expect(response.body).toEqual({ + message: "Invalid credentials", + status: 403, + }) + }) + + it("should return 403 when user doesn't exist", async () => { + const tenantId = config.tenantId! + const email = "invaliduser@test.com" + const password = "password" + + const response = await config.api.auth.login( + tenantId, + email, + password, + { status: 403 } + ) + expect(response.body).toEqual({ + message: "Invalid credentials", + status: 403, + }) + }) + + describe("sso user", () => { + let user: User + + async function testSSOUser() { + const tenantId = user.tenantId! + const email = user.email + const password = "test" + + const response = await config.api.auth.login( + tenantId, + email, + password, + { status: 400 } + ) + + expect(response.body).toEqual({ + message: "SSO user cannot login using password", + status: 400, + }) + } + + describe("budibase sso user", () => { + it("should prevent user from logging in", async () => { + user = await createSSOUser() + await testSSOUser() + }) + }) + + describe("root account sso user", () => { + it("should prevent user from logging in", async () => { + user = await config.createUser() + const account = structures.accounts.ssoAccount() as CloudAccount + mocks.accounts.getAccount.mockReturnValueOnce( + Promise.resolve(account) + ) + + await testSSOUser() + }) + }) + }) }) describe("POST /api/global/auth/logout", () => { it("should logout", async () => { - await config.api.auth.logout() + const response = await config.api.auth.logout() expect(events.auth.logout).toBeCalledTimes(1) - // TODO: Verify sessions deleted + const authCookie = getAuthCookie(response) + expect(authCookie).toBe("") }) }) describe("POST /api/global/auth/:tenantId/reset", () => { it("should generate password reset email", async () => { - await tenancy.doInTenant(config.tenant1User!.tenantId, async () => { - const userEmail = structures.email() - const { res, code } = await config.api.auth.requestPasswordReset( + const user = await config.createUser() + + const { res, code } = await config.api.auth.requestPasswordReset( + sendMailMock, + user.email + ) + + expect(res.body).toEqual({ + message: "Please check your email for a reset link.", + }) + expect(sendMailMock).toHaveBeenCalled() + expect(code).toBeDefined() + expect(events.user.passwordResetRequested).toBeCalledTimes(1) + expect(events.user.passwordResetRequested).toBeCalledWith(user) + }) + + describe("sso user", () => { + let user: User + + async function testSSOUser() { + const { res } = await config.api.auth.requestPasswordReset( sendMailMock, - userEmail + user.email, + { status: 400 } ) - const user = await config.getUser(userEmail) expect(res.body).toEqual({ - message: "Please check your email for a reset link.", + message: "SSO user cannot reset password", + status: 400, + error: { + code: "http", + type: "generic", + }, }) - expect(sendMailMock).toHaveBeenCalled() + expect(sendMailMock).not.toHaveBeenCalled() + } - expect(code).toBeDefined() - expect(events.user.passwordResetRequested).toBeCalledTimes(1) - expect(events.user.passwordResetRequested).toBeCalledWith(user) + describe("budibase sso user", () => { + it("should prevent user from generating password reset email", async () => { + user = await createSSOUser() + await testSSOUser() + }) + }) + + describe("root account sso user", () => { + it("should prevent user from generating password reset email", async () => { + user = await config.createUser(structures.users.user()) + const account = structures.accounts.ssoAccount() as CloudAccount + mocks.accounts.getAccount.mockReturnValueOnce( + Promise.resolve(account) + ) + + await testSSOUser() + }) }) }) }) describe("POST /api/global/auth/:tenantId/reset/update", () => { it("should reset password", async () => { - await tenancy.doInTenant(config.tenant1User!.tenantId, async () => { - const userEmail = structures.email() - const { code } = await config.api.auth.requestPasswordReset( - sendMailMock, - userEmail + let user = await config.createUser() + const { code } = await config.api.auth.requestPasswordReset( + sendMailMock, + user.email + ) + delete user.password + + const newPassword = "newpassword" + const res = await config.api.auth.updatePassword(code!, newPassword) + + user = await config.getUser(user.email) + delete user.password + + expect(res.body).toEqual({ message: "password reset successfully." }) + expect(events.user.passwordReset).toBeCalledTimes(1) + expect(events.user.passwordReset).toBeCalledWith(user) + + // login using new password + await config.api.auth.login(user.tenantId, user.email, newPassword) + }) + + describe("sso user", () => { + let user: User | SSOUser + + async function testSSOUser(code: string) { + const res = await config.api.auth.updatePassword( + code!, + generator.string(), + { status: 400 } ) - const user = await config.getUser(userEmail) - delete user.password - const res = await config.api.auth.updatePassword(code) + expect(res.body).toEqual({ + message: "Cannot reset password.", + status: 400, + }) + } - expect(res.body).toEqual({ message: "password reset successfully." }) - expect(events.user.passwordReset).toBeCalledTimes(1) - expect(events.user.passwordReset).toBeCalledWith(user) + describe("budibase sso user", () => { + it("should prevent user from generating password reset email", async () => { + user = await config.createUser() + const { code } = await config.api.auth.requestPasswordReset( + sendMailMock, + user.email + ) + + // convert to sso now that password reset has been requested + const ssoUser = user as SSOUser + ssoUser.providerType = structures.sso.providerType() + delete ssoUser.password + await config.doInTenant(() => userSdk.save(ssoUser)) + + await testSSOUser(code!) + }) + }) + + describe("root account sso user", () => { + it("should prevent user from generating password reset email", async () => { + user = await config.createUser() + const { code } = await config.api.auth.requestPasswordReset( + sendMailMock, + user.email + ) + + // convert to account owner now that password has been requested + const account = structures.accounts.ssoAccount() as CloudAccount + mocks.accounts.getAccount.mockReturnValueOnce( + Promise.resolve(account) + ) + + await testSSOUser(code!) + }) }) - // TODO: Login using new password }) }) }) @@ -153,7 +354,7 @@ describe("/api/global/auth", () => { const location: string = res.get("location") expect( location.startsWith( - "http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2Fdefault%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access" + `http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2F${config.tenantId}%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access` ) ).toBe(true) }) diff --git a/packages/worker/src/api/routes/global/tests/self.spec.ts b/packages/worker/src/api/routes/global/tests/self.spec.ts index d253a7f24e..74d24c7c31 100644 --- a/packages/worker/src/api/routes/global/tests/self.spec.ts +++ b/packages/worker/src/api/routes/global/tests/self.spec.ts @@ -30,7 +30,7 @@ describe("/api/global/self", () => { user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString() expect(res.body._id).toBe(user._id) expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.updated).toBeCalledWith(user) + expect(events.user.updated).toBeCalledWith(dbUser) expect(events.user.passwordUpdated).not.toBeCalled() }) @@ -44,12 +44,11 @@ describe("/api/global/self", () => { const dbUser = await config.getUser(user.email) user._rev = dbUser._rev user.dayPassRecordedAt = mocks.date.MOCK_DATE.toISOString() - delete user.password expect(res.body._id).toBe(user._id) expect(events.user.updated).toBeCalledTimes(1) - expect(events.user.updated).toBeCalledWith(user) + expect(events.user.updated).toBeCalledWith(dbUser) expect(events.user.passwordUpdated).toBeCalledTimes(1) - expect(events.user.passwordUpdated).toBeCalledWith(user) + expect(events.user.passwordUpdated).toBeCalledWith(dbUser) }) }) }) diff --git a/packages/worker/src/api/routes/system/tests/accounts.spec.ts b/packages/worker/src/api/routes/system/tests/accounts.spec.ts index fd54dd2b0a..7df2950212 100644 --- a/packages/worker/src/api/routes/system/tests/accounts.spec.ts +++ b/packages/worker/src/api/routes/system/tests/accounts.spec.ts @@ -1,4 +1,4 @@ -import sdk from "../../../../sdk" +import * as accounts from "../../../../sdk/accounts" import { TestConfiguration, structures } from "../../../../tests" import { v4 as uuid } from "uuid" @@ -24,8 +24,8 @@ describe("accounts", () => { const response = await config.api.accounts.saveMetadata(account) - const id = sdk.accounts.formatAccountMetadataId(account.accountId) - const metadata = await sdk.accounts.getMetadata(id) + const id = accounts.metadata.formatAccountMetadataId(account.accountId) + const metadata = await accounts.metadata.getMetadata(id) expect(response).toStrictEqual(metadata) }) }) @@ -37,7 +37,7 @@ describe("accounts", () => { await config.api.accounts.destroyMetadata(account.accountId) - const deleted = await sdk.accounts.getMetadata(account.accountId) + const deleted = await accounts.metadata.getMetadata(account.accountId) expect(deleted).toBe(undefined) }) diff --git a/packages/worker/src/environment.ts b/packages/worker/src/environment.ts index 52fec210bc..71fd89f276 100644 --- a/packages/worker/src/environment.ts +++ b/packages/worker/src/environment.ts @@ -26,6 +26,8 @@ function parseIntSafe(number: any) { } } +const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") + const environment = { // auth MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, @@ -49,7 +51,7 @@ const environment = { CLUSTER_PORT: process.env.CLUSTER_PORT, // flags NODE_ENV: process.env.NODE_ENV, - SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), + SELF_HOSTED: selfHosted, LOG_LEVEL: process.env.LOG_LEVEL, MULTI_TENANCY: process.env.MULTI_TENANCY, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, @@ -65,6 +67,18 @@ const environment = { CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600, SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, ENCRYPTED_TEST_PUBLIC_API_KEY: process.env.ENCRYPTED_TEST_PUBLIC_API_KEY, + /** + * Mock the email service in use - links to ethereal hosted emails are logged instead. + */ + ENABLE_EMAIL_TEST_MODE: process.env.ENABLE_EMAIL_TEST_MODE, + /** + * Enable to allow an admin user to login using a password. + * This can be useful to prevent lockout when configuring SSO. + * However, this should be turned OFF by default for security purposes. + */ + ENABLE_SSO_MAINTENANCE_MODE: selfHosted + ? process.env.ENABLE_SSO_MAINTENANCE_MODE + : false, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 1eff6c06fb..1e3ff3cbdf 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -25,6 +25,12 @@ const koaSession = require("koa-session") const logger = require("koa-pino-logger") import destroyable from "server-destroy" +if (env.ENABLE_SSO_MAINTENANCE_MODE) { + console.warn( + "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress" + ) +} + // this will setup http and https proxies form env variables bootstrap() diff --git a/packages/worker/src/sdk/accounts/index.ts b/packages/worker/src/sdk/accounts/index.ts index f2ae03040e..72db8c3d85 100644 --- a/packages/worker/src/sdk/accounts/index.ts +++ b/packages/worker/src/sdk/accounts/index.ts @@ -1 +1,2 @@ -export * from "./accounts" +export * as metadata from "./metadata" +export { accounts as api } from "@budibase/backend-core" diff --git a/packages/worker/src/sdk/accounts/accounts.ts b/packages/worker/src/sdk/accounts/metadata.ts similarity index 99% rename from packages/worker/src/sdk/accounts/accounts.ts rename to packages/worker/src/sdk/accounts/metadata.ts index e43285087b..64065e8b78 100644 --- a/packages/worker/src/sdk/accounts/accounts.ts +++ b/packages/worker/src/sdk/accounts/metadata.ts @@ -2,7 +2,6 @@ import { AccountMetadata } from "@budibase/types" import { db, StaticDatabases, - HTTPError, DocumentType, SEPARATOR, } from "@budibase/backend-core" diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts new file mode 100644 index 0000000000..15a4f3c7e7 --- /dev/null +++ b/packages/worker/src/sdk/auth/auth.ts @@ -0,0 +1,86 @@ +import { + auth as authCore, + tenancy, + utils as coreUtils, + sessions, + events, + HTTPError, +} from "@budibase/backend-core" +import { PlatformLogoutOpts, User } from "@budibase/types" +import jwt from "jsonwebtoken" +import env from "../../environment" +import * as userSdk from "../users" +import * as emails from "../../utilities/email" +import * as redis from "../../utilities/redis" +import { EmailTemplatePurpose } from "../../constants" + +// LOGIN / LOGOUT + +export async function loginUser(user: User) { + const sessionId = coreUtils.newid() + const tenantId = tenancy.getTenantId() + await sessions.createASession(user._id!, { sessionId, tenantId }) + const token = jwt.sign( + { + userId: user._id, + sessionId, + tenantId, + }, + env.JWT_SECRET! + ) + return token +} + +export async function logout(opts: PlatformLogoutOpts) { + // TODO: This should be moved out of core and into worker only + // account-portal can call worker endpoint + return authCore.platformLogout(opts) +} + +// PASSWORD MANAGEMENT + +/** + * Reset the user password, used as part of a forgotten password flow. + */ +export const reset = async (email: string) => { + const configured = await emails.isEmailConfigured() + if (!configured) { + throw new HTTPError( + "Please contact your platform administrator, SMTP is not configured.", + 400 + ) + } + + const user = await userSdk.core.getGlobalUserByEmail(email) + // exit if user doesn't exist + if (!user) { + return + } + + // exit if user has sso + if (await userSdk.isPreventSSOPasswords(user)) { + throw new HTTPError("SSO user cannot reset password", 400) + } + + // send password reset + await emails.sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { + user, + subject: "{{ company }} platform password reset", + }) + await events.user.passwordResetRequested(user) +} + +/** + * Perform the user password update if the provided reset code is valid. + */ +export const resetUpdate = async (resetCode: string, password: string) => { + const { userId } = await redis.checkResetPasswordCode(resetCode) + + let user = await userSdk.getUser(userId) + user.password = password + user = await userSdk.save(user) + + // remove password from the user before sending events + delete user.password + await events.user.passwordReset(user) +} diff --git a/packages/worker/src/sdk/auth/index.ts b/packages/worker/src/sdk/auth/index.ts new file mode 100644 index 0000000000..306751af96 --- /dev/null +++ b/packages/worker/src/sdk/auth/index.ts @@ -0,0 +1 @@ +export * from "./auth" diff --git a/packages/worker/src/sdk/users/events.ts b/packages/worker/src/sdk/users/events.ts index 17c4748dff..7d86182a3c 100644 --- a/packages/worker/src/sdk/users/events.ts +++ b/packages/worker/src/sdk/users/events.ts @@ -84,6 +84,10 @@ export const handleSaveEvents = async ( ) { await events.user.passwordForceReset(user) } + + if (user.password !== existingUser.password) { + await events.user.passwordUpdated(user) + } } else { await events.user.created(user) } diff --git a/packages/worker/src/sdk/users/index.ts b/packages/worker/src/sdk/users/index.ts index 056d6e5675..2eaa0e68a2 100644 --- a/packages/worker/src/sdk/users/index.ts +++ b/packages/worker/src/sdk/users/index.ts @@ -1 +1,2 @@ export * from "./users" +export { users as core } from "@budibase/backend-core" diff --git a/packages/worker/src/sdk/users/tests/users.spec.ts b/packages/worker/src/sdk/users/tests/users.spec.ts new file mode 100644 index 0000000000..41d9298997 --- /dev/null +++ b/packages/worker/src/sdk/users/tests/users.spec.ts @@ -0,0 +1,52 @@ +import { structures } from "../../../tests" +import * as users from "../users" +import env from "../../../environment" +import { mocks } from "@budibase/backend-core/tests" +import { CloudAccount } from "@budibase/types" + +describe("users", () => { + describe("isPreventSSOPasswords", () => { + it("returns true for sso account user", async () => { + const user = structures.users.user() + mocks.accounts.getAccount.mockReturnValue( + Promise.resolve(structures.accounts.ssoAccount() as CloudAccount) + ) + const result = await users.isPreventSSOPasswords(user) + expect(result).toBe(true) + }) + + it("returns true for sso user", async () => { + const user = structures.users.ssoUser() + const result = await users.isPreventSSOPasswords(user) + expect(result).toBe(true) + }) + + describe("sso maintenance mode", () => { + beforeEach(() => { + env._set("ENABLE_SSO_MAINTENANCE_MODE", true) + }) + + afterEach(() => { + env._set("ENABLE_SSO_MAINTENANCE_MODE", false) + }) + + describe("non-admin user", () => { + it("returns true", async () => { + const user = structures.users.ssoUser() + const result = await users.isPreventSSOPasswords(user) + expect(result).toBe(true) + }) + }) + + describe("admin user", () => { + it("returns false", async () => { + const user = structures.users.ssoUser({ + user: structures.users.adminUser(), + }) + const result = await users.isPreventSSOPasswords(user) + expect(result).toBe(false) + }) + }) + }) + }) +}) diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 1d05f6d84f..7d4a2f04f0 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -6,12 +6,11 @@ import { cache, constants, db as dbUtils, - deprovisioning, events, HTTPError, - migrations, sessions, tenancy, + platform, users as usersCore, utils, ViewName, @@ -21,21 +20,22 @@ import { AllDocsResponse, BulkUserResponse, CloudAccount, - CreateUserResponse, InviteUsersRequest, InviteUsersResponse, - MigrationType, + isSSOAccount, + isSSOUser, PlatformUser, PlatformUserByEmail, RowResponse, SearchUsersRequest, + UpdateSelf, User, - ThirdPartyUser, - isUser, + SaveUserOpts, } from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" import { groups as groupsSdk } from "@budibase/pro" +import * as accountSdk from "../accounts" const PAGE_LIMIT = 8 @@ -94,26 +94,23 @@ export const paginatedUsers = async ({ }) } +export async function getUserByEmail(email: string) { + return usersCore.getGlobalUserByEmail(email) +} + /** * Gets a user by ID from the global database, based on the current tenancy. */ export const getUser = async (userId: string) => { - const db = tenancy.getGlobalDB() - let user = await db.get(userId) + const user = await usersCore.getById(userId) if (user) { delete user.password } return user } -export interface SaveUserOpts { - hashPassword?: boolean - requirePassword?: boolean - currentUserId?: string -} - const buildUser = async ( - user: User | ThirdPartyUser, + user: User, opts: SaveUserOpts = { hashPassword: true, requirePassword: true, @@ -121,11 +118,13 @@ const buildUser = async ( tenantId: string, dbUser?: any ): Promise => { - let fullUser = user as User - let { password, _id } = fullUser + let { password, _id } = user let hashedPassword if (password) { + if (await isPreventSSOPasswords(user)) { + throw new HTTPError("SSO user cannot set password", 400) + } hashedPassword = opts.hashPassword ? await utils.hash(password) : password } else if (dbUser) { hashedPassword = dbUser.password @@ -135,10 +134,10 @@ const buildUser = async ( _id = _id || dbUtils.generateGlobalUserID() - fullUser = { + const fullUser = { createdAt: Date.now(), ...dbUser, - ...fullUser, + ...user, _id, password: hashedPassword, tenantId, @@ -189,10 +188,36 @@ const validateUniqueUser = async (email: string, tenantId: string) => { } } +export async function isPreventSSOPasswords(user: User) { + // when in maintenance mode we allow sso users with the admin role + // to perform any password action - this prevents lockout + if (env.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) { + return false + } + + // Check local sso + if (isSSOUser(user)) { + return true + } + + // Check account sso + const account = await accountSdk.api.getAccount(user.email) + return !!(account && isSSOAccount(account)) +} + +export async function updateSelf(id: string, data: UpdateSelf) { + let user = await getUser(id) + user = { + ...user, + ...data, + } + return save(user) +} + export const save = async ( - user: User | ThirdPartyUser, + user: User, opts: SaveUserOpts = {} -): Promise => { +): Promise => { // default booleans to true if (opts.hashPassword == null) { opts.hashPassword = true @@ -264,7 +289,7 @@ export const save = async ( builtUser._rev = response.rev await eventHelpers.handleSaveEvents(builtUser, dbUser) - await addTenant(tenantId, _id, email) + await platform.users.addUser(tenantId, builtUser._id!, builtUser.email) await cache.user.invalidateUser(response.id) // let server know to sync user @@ -272,11 +297,8 @@ export const save = async ( await Promise.all(groupPromises) - return { - _id: response.id, - _rev: response.rev, - email, - } + // finally returned the saved user from the db + return db.get(builtUser._id!) } catch (err: any) { if (err.status === 409) { throw "User exists already" @@ -286,21 +308,6 @@ export const save = async ( } } -export const addTenant = async ( - tenantId: string, - _id: string, - email: string -) => { - if (env.MULTI_TENANCY) { - const afterCreateTenant = () => - migrations.backPopulateMigrations({ - type: MigrationType.GLOBAL, - tenantId, - }) - await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant) - } -} - const getExistingTenantUsers = async (emails: string[]): Promise => { const lcEmails = emails.map(email => email.toLowerCase()) const params = { @@ -432,7 +439,7 @@ export const bulkCreate = async ( for (const user of usersToBulkSave) { // TODO: Refactor to bulk insert users into the info db // instead of relying on looping tenant creation - await addTenant(tenantId, user._id, user.email) + await platform.users.addUser(tenantId, user._id, user.email) await eventHelpers.handleSaveEvents(user, undefined) await apps.syncUserInApps(user._id) } @@ -566,7 +573,7 @@ export const destroy = async (id: string, currentUser: any) => { } } - await deprovisioning.removeUserFromInfoDB(dbUser) + await platform.users.removeUser(dbUser) await db.remove(userId, dbUser._rev) @@ -579,7 +586,7 @@ export const destroy = async (id: string, currentUser: any) => { const bulkDeleteProcessing = async (dbUser: User) => { const userId = dbUser._id as string - await deprovisioning.removeUserFromInfoDB(dbUser) + await platform.users.removeUser(dbUser) await eventHelpers.handleDeleteEvents(dbUser) await cache.user.invalidateUser(userId) await sessions.invalidateSessions(userId, { reason: "bulk-deletion" }) diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 7d075e7fef..3004d0aed4 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -22,7 +22,7 @@ import { env as coreEnv, } from "@budibase/backend-core" import structures, { CSRF_TOKEN } from "./structures" -import { CreateUserResponse, User, AuthToken } from "@budibase/types" +import { SaveUserResponse, User, AuthToken } from "@budibase/types" import API from "./api" class TestConfiguration { @@ -226,7 +226,7 @@ class TestConfiguration { user = structures.users.user() } const response = await this._req(user, null, controllers.users.save) - const body = response as CreateUserResponse + const body = response as SaveUserResponse return this.getUser(body.email) } diff --git a/packages/worker/src/tests/api/auth.ts b/packages/worker/src/tests/api/auth.ts index ccbf747273..bd0471ca74 100644 --- a/packages/worker/src/tests/api/auth.ts +++ b/packages/worker/src/tests/api/auth.ts @@ -1,21 +1,39 @@ -import structures from "../structures" import TestConfiguration from "../TestConfiguration" -import { TestAPI } from "./base" +import { TestAPI, TestAPIOpts } from "./base" export class AuthAPI extends TestAPI { constructor(config: TestConfiguration) { super(config) } - updatePassword = (code: string) => { + updatePassword = ( + resetCode: string, + password: string, + opts?: TestAPIOpts + ) => { return this.request .post(`/api/global/auth/${this.config.getTenantId()}/reset/update`) .send({ - password: "newpassword", - resetCode: code, + password, + resetCode, }) .expect("Content-Type", /json/) - .expect(200) + .expect(opts?.status ? opts.status : 200) + } + + login = ( + tenantId: string, + email: string, + password: string, + opts?: TestAPIOpts + ) => { + return this.request + .post(`/api/global/auth/${tenantId}/login`) + .send({ + username: email, + password: password, + }) + .expect(opts?.status ? opts.status : 200) } logout = () => { @@ -25,25 +43,31 @@ export class AuthAPI extends TestAPI { .expect(200) } - requestPasswordReset = async (sendMailMock: any, userEmail: string) => { + requestPasswordReset = async ( + sendMailMock: any, + email: string, + opts?: TestAPIOpts + ) => { await this.config.saveSmtpConfig() await this.config.saveSettingsConfig() - await this.config.createUser({ - ...structures.users.user(), - email: userEmail, - }) + const res = await this.request .post(`/api/global/auth/${this.config.getTenantId()}/reset`) .send({ - email: userEmail, + email: email, }) .expect("Content-Type", /json/) - .expect(200) - const emailCall = sendMailMock.mock.calls[0][0] - const parts = emailCall.html.split( - `http://localhost:10000/builder/auth/reset?code=` - ) - const code = parts[1].split('"')[0].split("&")[0] + .expect(opts?.status ? opts.status : 200) + + let code: string | undefined + if (res.status === 200) { + const emailCall = sendMailMock.mock.calls[0][0] + const parts = emailCall.html.split( + `http://localhost:10000/builder/auth/reset?code=` + ) + code = parts[1].split('"')[0].split("&")[0] + } + return { code, res } } } diff --git a/packages/worker/src/tests/structures/index.ts b/packages/worker/src/tests/structures/index.ts index dad055f7a7..bec15df6dd 100644 --- a/packages/worker/src/tests/structures/index.ts +++ b/packages/worker/src/tests/structures/index.ts @@ -1,6 +1,5 @@ import { structures } from "@budibase/backend-core/tests" import * as configs from "./configs" -import * as users from "./users" import * as groups from "./groups" import { v4 as uuid } from "uuid" @@ -11,7 +10,6 @@ const pkg = { ...structures, uuid, configs, - users, TENANT_ID, CSRF_TOKEN, groups, diff --git a/packages/worker/src/tests/structures/users.ts b/packages/worker/src/tests/structures/users.ts deleted file mode 100644 index 3348670b7d..0000000000 --- a/packages/worker/src/tests/structures/users.ts +++ /dev/null @@ -1,37 +0,0 @@ -export const email = "test@test.com" -import { AdminUser, BuilderUser, User } from "@budibase/types" -import { v4 as uuid } from "uuid" - -export const newEmail = () => { - return `${uuid()}@test.com` -} - -export const user = (userProps?: any): User => { - return { - email: newEmail(), - password: "test", - roles: { app_test: "admin" }, - ...userProps, - } -} - -export const adminUser = (userProps?: any): AdminUser => { - return { - ...user(userProps), - admin: { - global: true, - }, - builder: { - global: true, - }, - } -} - -export const builderUser = (userProps?: any): BuilderUser => { - return { - ...user(userProps), - builder: { - global: true, - }, - } -} diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts index 7ec3447707..66e860edcb 100644 --- a/packages/worker/src/utilities/email.ts +++ b/packages/worker/src/utilities/email.ts @@ -26,7 +26,7 @@ type SendEmailOpts = { automation?: boolean } -const TEST_MODE = false +const TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev() const TYPE = TemplateType.EMAIL const FULL_EMAIL_PURPOSES = [ @@ -62,8 +62,8 @@ function createSMTPTransport(config: any) { host: "smtp.ethereal.email", secure: false, auth: { - user: "don.bahringer@ethereal.email", - pass: "yCKSH8rWyUPbnhGYk9", + user: "wyatt.zulauf29@ethereal.email", + pass: "tEwDtHBWWxusVWAPfa", }, } } From bd16f3a55dfe802cc11bc2c7ecac8434494cb0fc Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 21 Feb 2023 09:03:29 +0000 Subject: [PATCH 13/53] Re-add maxWorkers=2 to worker tests --- packages/worker/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/package.json b/packages/worker/package.json index 1ae0327441..0248199341 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -22,7 +22,7 @@ "build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION", "dev:stack:init": "node ./scripts/dev/manage.js init", "dev:builder": "npm run dev:stack:init && nodemon", - "test": "jest --coverage", + "test": "jest --coverage --maxWorkers=2", "test:watch": "jest --watch", "env:multi:enable": "node scripts/multiTenancy.js enable", "env:multi:disable": "node scripts/multiTenancy.js disable", From 3c5d5f4c25ea45663edb2c26afc8ebf1bc6879c9 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Feb 2023 09:42:56 +0000 Subject: [PATCH 14/53] Fix relation get test to test expected behaviour --- packages/server/src/integration-test/postgres.spec.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index ecfb532e7f..32d4204ea1 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -419,14 +419,16 @@ describe("row api - postgres", () => { describe("given a row with relation data", () => { let row: Row + let foreignRow: Row beforeEach(async () => { let [createdRow] = await populatePrimaryRows(1, { createForeignRow: true, }) row = createdRow.row + foreignRow = createdRow.foreignRow! }) - it("foreign key fields are not retrieved", async () => { + it("only foreign keys are retrieved", async () => { const res = await getRow(primaryPostgresTable._id, row.id) expect(res.status).toBe(200) @@ -436,7 +438,12 @@ describe("row api - postgres", () => { _id: expect.any(String), _rev: expect.any(String), }) - expect(res.body.foreignField).toBeUndefined() + expect( + res.body[`fk_${auxPostgresTable.name}_foreignField`] + ).toBeDefined() + expect(res.body[`fk_${auxPostgresTable.name}_foreignField`]).toBe( + foreignRow.id + ) }) }) }) From 483f15a5d6a08337d52c149d566c00fc4c1ef451 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Feb 2023 09:51:07 +0000 Subject: [PATCH 15/53] Add explicit check for the foreign field --- packages/server/src/integration-test/postgres.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index 32d4204ea1..c688600e8d 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -438,6 +438,9 @@ describe("row api - postgres", () => { _id: expect.any(String), _rev: expect.any(String), }) + + expect(res.body.foreignField).toBeUndefined() + expect( res.body[`fk_${auxPostgresTable.name}_foreignField`] ).toBeDefined() From 0c838196e4751ffea9ace440ebb8d357f69393e8 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 21 Feb 2023 10:52:11 +0000 Subject: [PATCH 16/53] Enable mock redis for integration tests --- packages/builder/setup.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/builder/setup.js b/packages/builder/setup.js index 744c20896a..b3fd96877b 100644 --- a/packages/builder/setup.js +++ b/packages/builder/setup.js @@ -19,6 +19,7 @@ process.env.COUCH_DB_USER = "budibase" process.env.COUCH_DB_PASSWORD = "budibase" process.env.INTERNAL_API_KEY = "budibase" process.env.ALLOW_DEV_AUTOMATIONS = 1 +process.env.MOCK_REDIS = 1 // Stop info logs polluting test outputs process.env.LOG_LEVEL = "error" From e6d7c22efa0a82c65020ef37602131508bf51b83 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 11:22:46 +0000 Subject: [PATCH 17/53] v2.3.17-alpha.5 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index dda0b8dfc8..63e9ac45f9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 75c16447ba..ceafebcbc5 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.17-alpha.4", + "@budibase/types": "2.3.17-alpha.5", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index bf2782d843..da9fdbca60 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.17-alpha.4", + "@budibase/string-templates": "2.3.17-alpha.5", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 049b8d2a9f..00fe30b057 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.4", - "@budibase/client": "2.3.17-alpha.4", - "@budibase/frontend-core": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", + "@budibase/bbui": "2.3.17-alpha.5", + "@budibase/client": "2.3.17-alpha.5", + "@budibase/frontend-core": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.5", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1d92be8c84..036120eac5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", - "@budibase/types": "2.3.17-alpha.4", + "@budibase/backend-core": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/types": "2.3.17-alpha.5", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index b40f27f287..97ebc2b2e0 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.4", - "@budibase/frontend-core": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", + "@budibase/bbui": "2.3.17-alpha.5", + "@budibase/frontend-core": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.5", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index c4222bd0e3..7a42ba09db 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.17-alpha.4", + "@budibase/bbui": "2.3.17-alpha.5", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index bd6fa2a9d4..36328c51d8 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 0388465ce4..3888c55f46 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.17-alpha.4", - "@budibase/client": "2.3.17-alpha.4", + "@budibase/backend-core": "2.3.17-alpha.5", + "@budibase/client": "2.3.17-alpha.5", "@budibase/pro": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", - "@budibase/types": "2.3.17-alpha.4", + "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/types": "2.3.17-alpha.5", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index fad3b4d59f..d5eb7e3f04 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index a5441b58a2..0b1ee30b2e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 0248199341..b6bc942add 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.17-alpha.4", + "version": "2.3.17-alpha.5", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.4", + "@budibase/backend-core": "2.3.17-alpha.5", "@budibase/pro": "2.3.17-alpha.4", - "@budibase/string-templates": "2.3.17-alpha.4", - "@budibase/types": "2.3.17-alpha.4", + "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/types": "2.3.17-alpha.5", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From cbddc7ee4f537e78f01450eeb4c62b82e2882c9f Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 11:26:45 +0000 Subject: [PATCH 18/53] Update pro version to 2.3.17-alpha.5 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 3888c55f46..c647378d54 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.17-alpha.5", "@budibase/client": "2.3.17-alpha.5", - "@budibase/pro": "2.3.17-alpha.4", + "@budibase/pro": "2.3.17-alpha.5", "@budibase/string-templates": "2.3.17-alpha.5", "@budibase/types": "2.3.17-alpha.5", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 3fa6a84ad9..2d6ad91005 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.4.tgz#efe13cc2eb9a02223cff46228af8370a5a91db11" - integrity sha512-h9aCz+5uAQOvQiimULShKkXdQgyiwvgcxq3wqEvNHsAxmlslaJJouStWhvxBdsFiOW9K2CyI/8PeUBBRlAU0AQ== +"@budibase/backend-core@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.5.tgz#5bd380c9bf3835694729f1c68c44a77aec7b66dd" + integrity sha512-Nv/BsmItQcdi30oHvNEeRRkDxooMR3shZksb14NDWfdP9gxjVOkUltA2HELujL2ZEg4mismau/0oy9kZy5z3Og== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.4" + "@budibase/types" "2.3.17-alpha.5" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.4.tgz#56e945bd960eca8c8b2a04a3dd606df392bf5792" - integrity sha512-DJtKCc5/XXAnrvI5sS+joVuDYOLgwFmDYwLbssJgSQzGXNqepN/ikGq6eFCKn/99fRYBGREfwXwAlxXveZxPWA== +"@budibase/pro@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.5.tgz#4007ab38a5a88cedc1c0ba48543fcbfdf0c58a2a" + integrity sha512-pR7i7ehpoJ08Pq4Xs5vFJcZLvLkfCDS3AvJV1e0j9IDPD73kqiqCxgOF9rE27NTLf0BvbPK5Vj4Z7RPCxIJv7g== dependencies: - "@budibase/backend-core" "2.3.17-alpha.4" - "@budibase/types" "2.3.17-alpha.4" + "@budibase/backend-core" "2.3.17-alpha.5" + "@budibase/types" "2.3.17-alpha.5" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.4.tgz#63eb756160da7ace5459e303874f5e67425c6988" - integrity sha512-us/gGZPHimHsYNAnJ5yGdxeThT065wofJ2sfg0aD81P8nq3F2mevBoUmci/KRYdrQ9EsgpZ3Ou6CqnhnV8WmfA== +"@budibase/types@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.5.tgz#a8c31f49ac9f8b312317fd6fe547ea76d89d059b" + integrity sha512-qp51pVnOaUf42plQjPlCXZVMTIFKrkkhiJ+XaClnekRmyAYaz7JUIex+QTDbmUF4i0WbZRc0MgUCVQxlKkWotA== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index b6bc942add..c1a1a8a001 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.17-alpha.5", - "@budibase/pro": "2.3.17-alpha.4", + "@budibase/pro": "2.3.17-alpha.5", "@budibase/string-templates": "2.3.17-alpha.5", "@budibase/types": "2.3.17-alpha.5", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 3c12fd53b6..5c534eefe1 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.4.tgz#efe13cc2eb9a02223cff46228af8370a5a91db11" - integrity sha512-h9aCz+5uAQOvQiimULShKkXdQgyiwvgcxq3wqEvNHsAxmlslaJJouStWhvxBdsFiOW9K2CyI/8PeUBBRlAU0AQ== +"@budibase/backend-core@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.5.tgz#5bd380c9bf3835694729f1c68c44a77aec7b66dd" + integrity sha512-Nv/BsmItQcdi30oHvNEeRRkDxooMR3shZksb14NDWfdP9gxjVOkUltA2HELujL2ZEg4mismau/0oy9kZy5z3Og== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.4" + "@budibase/types" "2.3.17-alpha.5" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.4.tgz#56e945bd960eca8c8b2a04a3dd606df392bf5792" - integrity sha512-DJtKCc5/XXAnrvI5sS+joVuDYOLgwFmDYwLbssJgSQzGXNqepN/ikGq6eFCKn/99fRYBGREfwXwAlxXveZxPWA== +"@budibase/pro@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.5.tgz#4007ab38a5a88cedc1c0ba48543fcbfdf0c58a2a" + integrity sha512-pR7i7ehpoJ08Pq4Xs5vFJcZLvLkfCDS3AvJV1e0j9IDPD73kqiqCxgOF9rE27NTLf0BvbPK5Vj4Z7RPCxIJv7g== dependencies: - "@budibase/backend-core" "2.3.17-alpha.4" - "@budibase/types" "2.3.17-alpha.4" + "@budibase/backend-core" "2.3.17-alpha.5" + "@budibase/types" "2.3.17-alpha.5" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.17-alpha.4": - version "2.3.17-alpha.4" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.4.tgz#63eb756160da7ace5459e303874f5e67425c6988" - integrity sha512-us/gGZPHimHsYNAnJ5yGdxeThT065wofJ2sfg0aD81P8nq3F2mevBoUmci/KRYdrQ9EsgpZ3Ou6CqnhnV8WmfA== +"@budibase/types@2.3.17-alpha.5": + version "2.3.17-alpha.5" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.5.tgz#a8c31f49ac9f8b312317fd6fe547ea76d89d059b" + integrity sha512-qp51pVnOaUf42plQjPlCXZVMTIFKrkkhiJ+XaClnekRmyAYaz7JUIex+QTDbmUF4i0WbZRc0MgUCVQxlKkWotA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From 9ed759580f436e598070dc44fbdd3b520491e4f3 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 11:52:45 +0000 Subject: [PATCH 19/53] v2.3.17-alpha.6 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 63e9ac45f9..80460af875 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index ceafebcbc5..68323e05ac 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.17-alpha.5", + "@budibase/types": "2.3.17-alpha.6", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index da9fdbca60..ccaadf77b1 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.6", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 00fe30b057..761f17dd6d 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.5", - "@budibase/client": "2.3.17-alpha.5", - "@budibase/frontend-core": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/bbui": "2.3.17-alpha.6", + "@budibase/client": "2.3.17-alpha.6", + "@budibase/frontend-core": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.6", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 036120eac5..a4c21bef29 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", - "@budibase/types": "2.3.17-alpha.5", + "@budibase/backend-core": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/types": "2.3.17-alpha.6", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index 97ebc2b2e0..8b52e19c87 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.5", - "@budibase/frontend-core": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", + "@budibase/bbui": "2.3.17-alpha.6", + "@budibase/frontend-core": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.6", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 7a42ba09db..0ef09e42e2 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.17-alpha.5", + "@budibase/bbui": "2.3.17-alpha.6", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 36328c51d8..fc95ba8106 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index c647378d54..5c887c1a5b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.17-alpha.5", - "@budibase/client": "2.3.17-alpha.5", + "@budibase/backend-core": "2.3.17-alpha.6", + "@budibase/client": "2.3.17-alpha.6", "@budibase/pro": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", - "@budibase/types": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/types": "2.3.17-alpha.6", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index d5eb7e3f04..b53eaf887e 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 0b1ee30b2e..c913fea299 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index c1a1a8a001..a7b280f203 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.17-alpha.5", + "version": "2.3.17-alpha.6", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.5", + "@budibase/backend-core": "2.3.17-alpha.6", "@budibase/pro": "2.3.17-alpha.5", - "@budibase/string-templates": "2.3.17-alpha.5", - "@budibase/types": "2.3.17-alpha.5", + "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/types": "2.3.17-alpha.6", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From e093ab5ef4992058e4a74181638793ac4b6edd98 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 11:56:26 +0000 Subject: [PATCH 20/53] Update pro version to 2.3.17-alpha.6 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 5c887c1a5b..1d852473f2 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.17-alpha.6", "@budibase/client": "2.3.17-alpha.6", - "@budibase/pro": "2.3.17-alpha.5", + "@budibase/pro": "2.3.17-alpha.6", "@budibase/string-templates": "2.3.17-alpha.6", "@budibase/types": "2.3.17-alpha.6", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 2d6ad91005..72f2bc4db2 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.5.tgz#5bd380c9bf3835694729f1c68c44a77aec7b66dd" - integrity sha512-Nv/BsmItQcdi30oHvNEeRRkDxooMR3shZksb14NDWfdP9gxjVOkUltA2HELujL2ZEg4mismau/0oy9kZy5z3Og== +"@budibase/backend-core@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.6.tgz#b083b3899d435694105d37a1ec817ec6cdc9bb07" + integrity sha512-8ljXZnK6Db3Mexk+MyIsrQBFFu6aV3eRtiw5pwHl9BobxR+6s79YmUgLfjZCfpgzMXqnT73FnV7g8K7KPTNC3g== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.5" + "@budibase/types" "2.3.17-alpha.6" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.5.tgz#4007ab38a5a88cedc1c0ba48543fcbfdf0c58a2a" - integrity sha512-pR7i7ehpoJ08Pq4Xs5vFJcZLvLkfCDS3AvJV1e0j9IDPD73kqiqCxgOF9rE27NTLf0BvbPK5Vj4Z7RPCxIJv7g== +"@budibase/pro@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.6.tgz#2d24afa48003ff5928f667efeff16735bed24e0c" + integrity sha512-yVxKCHiDE4yoARDLZ3IqktUuWgNZoEVlmWo9rS8+bxPcsf5sw7Rj7TXTCMQX7F0cekMVtG+KkpH/8bFrqfR6OA== dependencies: - "@budibase/backend-core" "2.3.17-alpha.5" - "@budibase/types" "2.3.17-alpha.5" + "@budibase/backend-core" "2.3.17-alpha.6" + "@budibase/types" "2.3.17-alpha.6" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.5.tgz#a8c31f49ac9f8b312317fd6fe547ea76d89d059b" - integrity sha512-qp51pVnOaUf42plQjPlCXZVMTIFKrkkhiJ+XaClnekRmyAYaz7JUIex+QTDbmUF4i0WbZRc0MgUCVQxlKkWotA== +"@budibase/types@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.6.tgz#85b148334312a41cfbf01b6a20417b5e20485f3f" + integrity sha512-BgPvLdNQKJSnJmHNo1OfKSHEeVhdTwcNSr2cwHjUpJk394rJiZfsOV7it8M9dLtAtpdsNR3ns7L6biW+pfjYoQ== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index a7b280f203..d595623fa4 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.17-alpha.6", - "@budibase/pro": "2.3.17-alpha.5", + "@budibase/pro": "2.3.17-alpha.6", "@budibase/string-templates": "2.3.17-alpha.6", "@budibase/types": "2.3.17-alpha.6", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 5c534eefe1..3273b78d68 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.5.tgz#5bd380c9bf3835694729f1c68c44a77aec7b66dd" - integrity sha512-Nv/BsmItQcdi30oHvNEeRRkDxooMR3shZksb14NDWfdP9gxjVOkUltA2HELujL2ZEg4mismau/0oy9kZy5z3Og== +"@budibase/backend-core@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.6.tgz#b083b3899d435694105d37a1ec817ec6cdc9bb07" + integrity sha512-8ljXZnK6Db3Mexk+MyIsrQBFFu6aV3eRtiw5pwHl9BobxR+6s79YmUgLfjZCfpgzMXqnT73FnV7g8K7KPTNC3g== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.5" + "@budibase/types" "2.3.17-alpha.6" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.5.tgz#4007ab38a5a88cedc1c0ba48543fcbfdf0c58a2a" - integrity sha512-pR7i7ehpoJ08Pq4Xs5vFJcZLvLkfCDS3AvJV1e0j9IDPD73kqiqCxgOF9rE27NTLf0BvbPK5Vj4Z7RPCxIJv7g== +"@budibase/pro@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.6.tgz#2d24afa48003ff5928f667efeff16735bed24e0c" + integrity sha512-yVxKCHiDE4yoARDLZ3IqktUuWgNZoEVlmWo9rS8+bxPcsf5sw7Rj7TXTCMQX7F0cekMVtG+KkpH/8bFrqfR6OA== dependencies: - "@budibase/backend-core" "2.3.17-alpha.5" - "@budibase/types" "2.3.17-alpha.5" + "@budibase/backend-core" "2.3.17-alpha.6" + "@budibase/types" "2.3.17-alpha.6" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.17-alpha.5": - version "2.3.17-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.5.tgz#a8c31f49ac9f8b312317fd6fe547ea76d89d059b" - integrity sha512-qp51pVnOaUf42plQjPlCXZVMTIFKrkkhiJ+XaClnekRmyAYaz7JUIex+QTDbmUF4i0WbZRc0MgUCVQxlKkWotA== +"@budibase/types@2.3.17-alpha.6": + version "2.3.17-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.6.tgz#85b148334312a41cfbf01b6a20417b5e20485f3f" + integrity sha512-BgPvLdNQKJSnJmHNo1OfKSHEeVhdTwcNSr2cwHjUpJk394rJiZfsOV7it8M9dLtAtpdsNR3ns7L6biW+pfjYoQ== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From a84665c1999a7b683f42794b8fe11c1714ece7d5 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 21 Feb 2023 13:36:14 +0000 Subject: [PATCH 21/53] Re-order deprovisioning sequence to fix platform user removal --- packages/worker/src/sdk/tenants/tenants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/src/sdk/tenants/tenants.ts b/packages/worker/src/sdk/tenants/tenants.ts index 3ba9c3f3a7..829b144c75 100644 --- a/packages/worker/src/sdk/tenants/tenants.ts +++ b/packages/worker/src/sdk/tenants/tenants.ts @@ -5,9 +5,9 @@ import { quotas } from "@budibase/pro" export async function deleteTenant(tenantId: string) { await quotas.bustCache() await platform.tenants.removeTenant(tenantId) - await removeGlobalDB(tenantId) await removeTenantUsers(tenantId) await removeTenantApps(tenantId) + await removeGlobalDB(tenantId) } async function removeGlobalDB(tenantId: string) { From a3dfaf2c3ff6beca2e4a6a86a444cd2461610b7b Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 14:39:55 +0000 Subject: [PATCH 22/53] v2.3.17-alpha.7 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 80460af875..2d1b05887b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 68323e05ac..f991cb07bf 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.17-alpha.6", + "@budibase/types": "2.3.17-alpha.7", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index ccaadf77b1..e925115c81 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.7", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 761f17dd6d..71d498f06e 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.6", - "@budibase/client": "2.3.17-alpha.6", - "@budibase/frontend-core": "2.3.17-alpha.6", - "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/bbui": "2.3.17-alpha.7", + "@budibase/client": "2.3.17-alpha.7", + "@budibase/frontend-core": "2.3.17-alpha.7", + "@budibase/string-templates": "2.3.17-alpha.7", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index a4c21bef29..539612b58e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.6", - "@budibase/string-templates": "2.3.17-alpha.6", - "@budibase/types": "2.3.17-alpha.6", + "@budibase/backend-core": "2.3.17-alpha.7", + "@budibase/string-templates": "2.3.17-alpha.7", + "@budibase/types": "2.3.17-alpha.7", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index 8b52e19c87..deea461e50 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.6", - "@budibase/frontend-core": "2.3.17-alpha.6", - "@budibase/string-templates": "2.3.17-alpha.6", + "@budibase/bbui": "2.3.17-alpha.7", + "@budibase/frontend-core": "2.3.17-alpha.7", + "@budibase/string-templates": "2.3.17-alpha.7", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 0ef09e42e2..662771e3bf 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.17-alpha.6", + "@budibase/bbui": "2.3.17-alpha.7", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index fc95ba8106..18a46b71ed 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 1d852473f2..75b8af44e8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.17-alpha.6", - "@budibase/client": "2.3.17-alpha.6", + "@budibase/backend-core": "2.3.17-alpha.7", + "@budibase/client": "2.3.17-alpha.7", "@budibase/pro": "2.3.17-alpha.6", - "@budibase/string-templates": "2.3.17-alpha.6", - "@budibase/types": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.7", + "@budibase/types": "2.3.17-alpha.7", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index b53eaf887e..8a1024ea0d 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index c913fea299..77b9fba3ca 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index d595623fa4..220bfa239a 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.17-alpha.6", + "version": "2.3.17-alpha.7", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.6", + "@budibase/backend-core": "2.3.17-alpha.7", "@budibase/pro": "2.3.17-alpha.6", - "@budibase/string-templates": "2.3.17-alpha.6", - "@budibase/types": "2.3.17-alpha.6", + "@budibase/string-templates": "2.3.17-alpha.7", + "@budibase/types": "2.3.17-alpha.7", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From 91941a538f926f5b9db5bf8032787855cfa13256 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 14:43:54 +0000 Subject: [PATCH 23/53] Update pro version to 2.3.17-alpha.7 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 75b8af44e8..2a2c12e9ba 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.17-alpha.7", "@budibase/client": "2.3.17-alpha.7", - "@budibase/pro": "2.3.17-alpha.6", + "@budibase/pro": "2.3.17-alpha.7", "@budibase/string-templates": "2.3.17-alpha.7", "@budibase/types": "2.3.17-alpha.7", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 72f2bc4db2..0576daf213 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.6": - version "2.3.17-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.6.tgz#b083b3899d435694105d37a1ec817ec6cdc9bb07" - integrity sha512-8ljXZnK6Db3Mexk+MyIsrQBFFu6aV3eRtiw5pwHl9BobxR+6s79YmUgLfjZCfpgzMXqnT73FnV7g8K7KPTNC3g== +"@budibase/backend-core@2.3.17-alpha.7": + version "2.3.17-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.7.tgz#a7040a54d9efe6f7e377f06ccdd37c39bafb6098" + integrity sha512-LG6/hP7MH9rkCHhW5D9Awst//tm8fn5Fus1b9nJSFVnNsuobRl0TaVs9A1HOrKr+0yHlLg5OrgLyRuFRxWVP6A== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.6" + "@budibase/types" "2.3.17-alpha.7" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.6": - version "2.3.17-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.6.tgz#2d24afa48003ff5928f667efeff16735bed24e0c" - integrity sha512-yVxKCHiDE4yoARDLZ3IqktUuWgNZoEVlmWo9rS8+bxPcsf5sw7Rj7TXTCMQX7F0cekMVtG+KkpH/8bFrqfR6OA== +"@budibase/pro@2.3.17-alpha.7": + version "2.3.17-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.7.tgz#adee25699d0a03f4093256686508c3505fecdeaf" + integrity sha512-mzK3z8v4jJkzUm3v6YeWoMBfFc9lhfk2RXcL3Yn9syCR5eoH+j/b1PVqzb8oHu833MWVHgxEQTguORVciCinig== dependencies: - "@budibase/backend-core" "2.3.17-alpha.6" - "@budibase/types" "2.3.17-alpha.6" + "@budibase/backend-core" "2.3.17-alpha.7" + "@budibase/types" "2.3.17-alpha.7" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.17-alpha.6": - version "2.3.17-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.6.tgz#85b148334312a41cfbf01b6a20417b5e20485f3f" - integrity sha512-BgPvLdNQKJSnJmHNo1OfKSHEeVhdTwcNSr2cwHjUpJk394rJiZfsOV7it8M9dLtAtpdsNR3ns7L6biW+pfjYoQ== +"@budibase/types@2.3.17-alpha.7": + version "2.3.17-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.7.tgz#4bad29002bbb01c5987a30ef5c98b7b075a07339" + integrity sha512-mszQPykRp7dPC6MSGZKTeH59Nx282WeqXnTCGi79Kd+Qdrkex5v7fJCdMoGy/gpScVghC0rEDkMTE2BKk6/jMQ== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index 220bfa239a..f738df4780 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.17-alpha.7", - "@budibase/pro": "2.3.17-alpha.6", + "@budibase/pro": "2.3.17-alpha.7", "@budibase/string-templates": "2.3.17-alpha.7", "@budibase/types": "2.3.17-alpha.7", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 3273b78d68..d80f39be2c 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.6": - version "2.3.17-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.6.tgz#b083b3899d435694105d37a1ec817ec6cdc9bb07" - integrity sha512-8ljXZnK6Db3Mexk+MyIsrQBFFu6aV3eRtiw5pwHl9BobxR+6s79YmUgLfjZCfpgzMXqnT73FnV7g8K7KPTNC3g== +"@budibase/backend-core@2.3.17-alpha.7": + version "2.3.17-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.7.tgz#a7040a54d9efe6f7e377f06ccdd37c39bafb6098" + integrity sha512-LG6/hP7MH9rkCHhW5D9Awst//tm8fn5Fus1b9nJSFVnNsuobRl0TaVs9A1HOrKr+0yHlLg5OrgLyRuFRxWVP6A== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.6" + "@budibase/types" "2.3.17-alpha.7" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.6": - version "2.3.17-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.6.tgz#2d24afa48003ff5928f667efeff16735bed24e0c" - integrity sha512-yVxKCHiDE4yoARDLZ3IqktUuWgNZoEVlmWo9rS8+bxPcsf5sw7Rj7TXTCMQX7F0cekMVtG+KkpH/8bFrqfR6OA== +"@budibase/pro@2.3.17-alpha.7": + version "2.3.17-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.7.tgz#adee25699d0a03f4093256686508c3505fecdeaf" + integrity sha512-mzK3z8v4jJkzUm3v6YeWoMBfFc9lhfk2RXcL3Yn9syCR5eoH+j/b1PVqzb8oHu833MWVHgxEQTguORVciCinig== dependencies: - "@budibase/backend-core" "2.3.17-alpha.6" - "@budibase/types" "2.3.17-alpha.6" + "@budibase/backend-core" "2.3.17-alpha.7" + "@budibase/types" "2.3.17-alpha.7" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.17-alpha.6": - version "2.3.17-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.6.tgz#85b148334312a41cfbf01b6a20417b5e20485f3f" - integrity sha512-BgPvLdNQKJSnJmHNo1OfKSHEeVhdTwcNSr2cwHjUpJk394rJiZfsOV7it8M9dLtAtpdsNR3ns7L6biW+pfjYoQ== +"@budibase/types@2.3.17-alpha.7": + version "2.3.17-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.7.tgz#4bad29002bbb01c5987a30ef5c98b7b075a07339" + integrity sha512-mszQPykRp7dPC6MSGZKTeH59Nx282WeqXnTCGi79Kd+Qdrkex5v7fJCdMoGy/gpScVghC0rEDkMTE2BKk6/jMQ== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From e64e3a9e458c9f27d2d6a73b8f4821f8120d94d5 Mon Sep 17 00:00:00 2001 From: Gerard Burns Date: Tue, 21 Feb 2023 15:03:58 +0000 Subject: [PATCH 24/53] New Onboarding URL Validation (#9507) * New Onboarding URL Validation * linting * PR Feedback --- .../src/components/start/CreateAppModal.svelte | 16 ++++++++++++++-- .../src/components/start/UpdateAppModal.svelte | 15 +++++++++++++-- packages/builder/src/constants/index.js | 2 +- .../builder/src/helpers/validation/yup/app.js | 4 +--- .../apps/onboarding/_components/NamePanel.svelte | 5 +++++ 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte index 9ebc046cdc..e3ce048a89 100644 --- a/packages/builder/src/components/start/CreateAppModal.svelte +++ b/packages/builder/src/components/start/CreateAppModal.svelte @@ -26,7 +26,15 @@ const values = writable({ name: "", url: null }) const validation = createValidationStore() - $: validation.check($values) + + $: { + const { name, url } = $values + + validation.check({ + name, + url: url?.[0] === "/" ? url.substring(1, url.length) : url, + }) + } onMount(async () => { const lastChar = $auth.user?.firstName @@ -87,7 +95,11 @@ appValidation.url(validation, { apps: applications }) appValidation.file(validation, { template }) // init validation - validation.check($values) + const { name, url } = $values + validation.check({ + name, + url: url?.[0] === "/" ? url.substring(1, url.length) : url, + }) } async function createNewApp() { diff --git a/packages/builder/src/components/start/UpdateAppModal.svelte b/packages/builder/src/components/start/UpdateAppModal.svelte index a41ebccaeb..4385175816 100644 --- a/packages/builder/src/components/start/UpdateAppModal.svelte +++ b/packages/builder/src/components/start/UpdateAppModal.svelte @@ -23,14 +23,25 @@ }) const validation = createValidationStore() - $: validation.check($values) + $: { + const { name, url } = $values + + validation.check({ + name, + url: url?.[0] === "/" ? url.substring(1, url.length) : url, + }) + } const setupValidation = async () => { const applications = svelteGet(apps) appValidation.name(validation, { apps: applications, currentApp: app }) appValidation.url(validation, { apps: applications, currentApp: app }) // init validation - validation.check($values) + const { name, url } = $values + validation.check({ + name, + url: url?.[0] === "/" ? url.substring(1, url.length) : url, + }) } async function updateApp() { diff --git a/packages/builder/src/constants/index.js b/packages/builder/src/constants/index.js index 803cafcffb..f68202f81e 100644 --- a/packages/builder/src/constants/index.js +++ b/packages/builder/src/constants/index.js @@ -46,7 +46,7 @@ export const LAYOUT_NAMES = { // one or more word characters and whitespace export const APP_NAME_REGEX = /^[\w\s]+$/ // zero or more non-whitespace characters -export const APP_URL_REGEX = /^\S*$/ +export const APP_URL_REGEX = /^[0-9a-zA-Z-_]+$/ export const DefaultAppTheme = { primaryColor: "var(--spectrum-global-color-blue-600)", diff --git a/packages/builder/src/helpers/validation/yup/app.js b/packages/builder/src/helpers/validation/yup/app.js index 4e41576d46..8498255cc9 100644 --- a/packages/builder/src/helpers/validation/yup/app.js +++ b/packages/builder/src/helpers/validation/yup/app.js @@ -62,11 +62,9 @@ export const url = (validation, { apps, currentApp } = { apps: [] }) => { } // make it clear that this is a url path and cannot be a full url return ( - value.startsWith("/") && !value.includes("http") && !value.includes("www") && - !value.includes(".") && - value.length > 1 // just '/' is not valid + !value.includes(".") ) }) ) diff --git a/packages/builder/src/pages/builder/portal/apps/onboarding/_components/NamePanel.svelte b/packages/builder/src/pages/builder/portal/apps/onboarding/_components/NamePanel.svelte index 730cfbe4a2..1264b63531 100644 --- a/packages/builder/src/pages/builder/portal/apps/onboarding/_components/NamePanel.svelte +++ b/packages/builder/src/pages/builder/portal/apps/onboarding/_components/NamePanel.svelte @@ -1,6 +1,7 @@ From 5e7305b4dd302180b179145c89239778d60b7286 Mon Sep 17 00:00:00 2001 From: Gerard Burns Date: Tue, 21 Feb 2023 15:04:37 +0000 Subject: [PATCH 25/53] Fix Automation Bindings Panel Requiring a Double Click (#9688) * Fix Automation Bindings Panel Requiring a Double Click * PR Feedback --------- Co-authored-by: Rory Powell --- .../builder/src/components/common/bindings/BindingPanel.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 7daf2173a7..3a1c6c4fee 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -183,6 +183,7 @@ bind:this={popover} anchor={popoverAnchor} maxWidth={300} + dismissible={false} >
From 9fadc42a2e62d472aeea4fdd7fd5c141319b9818 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 15:20:48 +0000 Subject: [PATCH 26/53] v2.3.17-alpha.8 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 2d1b05887b..5530ace9f0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index f991cb07bf..18fcb3d696 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.17-alpha.7", + "@budibase/types": "2.3.17-alpha.8", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index e925115c81..3144341c74 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.17-alpha.7", + "@budibase/string-templates": "2.3.17-alpha.8", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 71d498f06e..54b6994eb7 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.7", - "@budibase/client": "2.3.17-alpha.7", - "@budibase/frontend-core": "2.3.17-alpha.7", - "@budibase/string-templates": "2.3.17-alpha.7", + "@budibase/bbui": "2.3.17-alpha.8", + "@budibase/client": "2.3.17-alpha.8", + "@budibase/frontend-core": "2.3.17-alpha.8", + "@budibase/string-templates": "2.3.17-alpha.8", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 539612b58e..83b9440102 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.7", - "@budibase/string-templates": "2.3.17-alpha.7", - "@budibase/types": "2.3.17-alpha.7", + "@budibase/backend-core": "2.3.17-alpha.8", + "@budibase/string-templates": "2.3.17-alpha.8", + "@budibase/types": "2.3.17-alpha.8", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index deea461e50..a0ea947f18 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.17-alpha.7", - "@budibase/frontend-core": "2.3.17-alpha.7", - "@budibase/string-templates": "2.3.17-alpha.7", + "@budibase/bbui": "2.3.17-alpha.8", + "@budibase/frontend-core": "2.3.17-alpha.8", + "@budibase/string-templates": "2.3.17-alpha.8", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 662771e3bf..6ddf874931 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.17-alpha.7", + "@budibase/bbui": "2.3.17-alpha.8", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 18a46b71ed..cd007bac7e 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 2a2c12e9ba..3436419c98 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.17-alpha.7", - "@budibase/client": "2.3.17-alpha.7", + "@budibase/backend-core": "2.3.17-alpha.8", + "@budibase/client": "2.3.17-alpha.8", "@budibase/pro": "2.3.17-alpha.7", - "@budibase/string-templates": "2.3.17-alpha.7", - "@budibase/types": "2.3.17-alpha.7", + "@budibase/string-templates": "2.3.17-alpha.8", + "@budibase/types": "2.3.17-alpha.8", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 8a1024ea0d..e7a72c35e5 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 77b9fba3ca..8db1b0708c 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index f738df4780..ecb62ceeb5 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.17-alpha.7", + "version": "2.3.17-alpha.8", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.17-alpha.7", + "@budibase/backend-core": "2.3.17-alpha.8", "@budibase/pro": "2.3.17-alpha.7", - "@budibase/string-templates": "2.3.17-alpha.7", - "@budibase/types": "2.3.17-alpha.7", + "@budibase/string-templates": "2.3.17-alpha.8", + "@budibase/types": "2.3.17-alpha.8", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From fcab5884ac85d21067c85b15fc8782825bb36838 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 15:24:40 +0000 Subject: [PATCH 27/53] Update pro version to 2.3.17-alpha.8 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 3436419c98..63629b425a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.17-alpha.8", "@budibase/client": "2.3.17-alpha.8", - "@budibase/pro": "2.3.17-alpha.7", + "@budibase/pro": "2.3.17-alpha.8", "@budibase/string-templates": "2.3.17-alpha.8", "@budibase/types": "2.3.17-alpha.8", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 0576daf213..be01941673 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.7": - version "2.3.17-alpha.7" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.7.tgz#a7040a54d9efe6f7e377f06ccdd37c39bafb6098" - integrity sha512-LG6/hP7MH9rkCHhW5D9Awst//tm8fn5Fus1b9nJSFVnNsuobRl0TaVs9A1HOrKr+0yHlLg5OrgLyRuFRxWVP6A== +"@budibase/backend-core@2.3.17-alpha.8": + version "2.3.17-alpha.8" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.8.tgz#c1bd7bb9ac581bd4ea9eb0e7883553219e3bcd9f" + integrity sha512-31zNXAwukBpbcHSvobtdTLBtrZsfIsq0NilxHzFJpaMEeGSq47fpwGPkzvzoekhgn0oGs0X4m4uRNaFKEDifeQ== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.7" + "@budibase/types" "2.3.17-alpha.8" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.7": - version "2.3.17-alpha.7" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.7.tgz#adee25699d0a03f4093256686508c3505fecdeaf" - integrity sha512-mzK3z8v4jJkzUm3v6YeWoMBfFc9lhfk2RXcL3Yn9syCR5eoH+j/b1PVqzb8oHu833MWVHgxEQTguORVciCinig== +"@budibase/pro@2.3.17-alpha.8": + version "2.3.17-alpha.8" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.8.tgz#f88416061c097c12ed133fac59200da408223c1c" + integrity sha512-d8VVdaH6X1e/GrQB4xA09pN5ANk6mxbXJ861KEniex3uEl1YFBJ75JJvoPLf2ynRIVqqsix18AXoO76ug7m9zA== dependencies: - "@budibase/backend-core" "2.3.17-alpha.7" - "@budibase/types" "2.3.17-alpha.7" + "@budibase/backend-core" "2.3.17-alpha.8" + "@budibase/types" "2.3.17-alpha.8" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.17-alpha.7": - version "2.3.17-alpha.7" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.7.tgz#4bad29002bbb01c5987a30ef5c98b7b075a07339" - integrity sha512-mszQPykRp7dPC6MSGZKTeH59Nx282WeqXnTCGi79Kd+Qdrkex5v7fJCdMoGy/gpScVghC0rEDkMTE2BKk6/jMQ== +"@budibase/types@2.3.17-alpha.8": + version "2.3.17-alpha.8" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.8.tgz#3ac692eec686c7b1ca728774a5a7e171644a9388" + integrity sha512-Ubt1vsa2OJY9NYqIxKrrvokAkWNs9snHR69czBkyigYnRrQ8axXijn3s3DoxhXg0KEyaFeMcOnq1yxEyHXPDBg== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index ecb62ceeb5..e7fe5de7f4 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.17-alpha.8", - "@budibase/pro": "2.3.17-alpha.7", + "@budibase/pro": "2.3.17-alpha.8", "@budibase/string-templates": "2.3.17-alpha.8", "@budibase/types": "2.3.17-alpha.8", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index d80f39be2c..fdf89c6b1c 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17-alpha.7": - version "2.3.17-alpha.7" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.7.tgz#a7040a54d9efe6f7e377f06ccdd37c39bafb6098" - integrity sha512-LG6/hP7MH9rkCHhW5D9Awst//tm8fn5Fus1b9nJSFVnNsuobRl0TaVs9A1HOrKr+0yHlLg5OrgLyRuFRxWVP6A== +"@budibase/backend-core@2.3.17-alpha.8": + version "2.3.17-alpha.8" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17-alpha.8.tgz#c1bd7bb9ac581bd4ea9eb0e7883553219e3bcd9f" + integrity sha512-31zNXAwukBpbcHSvobtdTLBtrZsfIsq0NilxHzFJpaMEeGSq47fpwGPkzvzoekhgn0oGs0X4m4uRNaFKEDifeQ== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.17-alpha.7" + "@budibase/types" "2.3.17-alpha.8" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.17-alpha.7": - version "2.3.17-alpha.7" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.7.tgz#adee25699d0a03f4093256686508c3505fecdeaf" - integrity sha512-mzK3z8v4jJkzUm3v6YeWoMBfFc9lhfk2RXcL3Yn9syCR5eoH+j/b1PVqzb8oHu833MWVHgxEQTguORVciCinig== +"@budibase/pro@2.3.17-alpha.8": + version "2.3.17-alpha.8" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17-alpha.8.tgz#f88416061c097c12ed133fac59200da408223c1c" + integrity sha512-d8VVdaH6X1e/GrQB4xA09pN5ANk6mxbXJ861KEniex3uEl1YFBJ75JJvoPLf2ynRIVqqsix18AXoO76ug7m9zA== dependencies: - "@budibase/backend-core" "2.3.17-alpha.7" - "@budibase/types" "2.3.17-alpha.7" + "@budibase/backend-core" "2.3.17-alpha.8" + "@budibase/types" "2.3.17-alpha.8" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.17-alpha.7": - version "2.3.17-alpha.7" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.7.tgz#4bad29002bbb01c5987a30ef5c98b7b075a07339" - integrity sha512-mszQPykRp7dPC6MSGZKTeH59Nx282WeqXnTCGi79Kd+Qdrkex5v7fJCdMoGy/gpScVghC0rEDkMTE2BKk6/jMQ== +"@budibase/types@2.3.17-alpha.8": + version "2.3.17-alpha.8" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17-alpha.8.tgz#3ac692eec686c7b1ca728774a5a7e171644a9388" + integrity sha512-Ubt1vsa2OJY9NYqIxKrrvokAkWNs9snHR69czBkyigYnRrQ8axXijn3s3DoxhXg0KEyaFeMcOnq1yxEyHXPDBg== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From ccc17f55994c05226d8bca3a437494a4201f3a82 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 17:03:45 +0000 Subject: [PATCH 28/53] v2.3.18-alpha.0 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index cfbed56e23..a881722de6 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.17", + "version": "2.3.18-alpha.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 32961a060b..c03600f5da 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "^2.3.17", + "@budibase/types": "2.3.18-alpha.0", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 8c9629b314..9e3aea5fea 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "^2.3.17", + "@budibase/string-templates": "2.3.18-alpha.0", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 5e968c23a3..f8f6ac289d 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "^2.3.17", - "@budibase/client": "^2.3.17", - "@budibase/frontend-core": "^2.3.17", - "@budibase/string-templates": "^2.3.17", + "@budibase/bbui": "2.3.18-alpha.0", + "@budibase/client": "2.3.18-alpha.0", + "@budibase/frontend-core": "2.3.18-alpha.0", + "@budibase/string-templates": "2.3.18-alpha.0", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index fb08c290fc..f2044f9c8b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "^2.3.17", - "@budibase/string-templates": "^2.3.17", - "@budibase/types": "^2.3.17", + "@budibase/backend-core": "2.3.18-alpha.0", + "@budibase/string-templates": "2.3.18-alpha.0", + "@budibase/types": "2.3.18-alpha.0", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index 78df2b67f7..a784cfa6bc 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^2.3.17", - "@budibase/frontend-core": "^2.3.17", - "@budibase/string-templates": "^2.3.17", + "@budibase/bbui": "2.3.18-alpha.0", + "@budibase/frontend-core": "2.3.18-alpha.0", + "@budibase/string-templates": "2.3.18-alpha.0", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 502df0fbd4..b0d39ed450 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "^2.3.17", + "@budibase/bbui": "2.3.18-alpha.0", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d8a5c44c5b..597c3dff84 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 0ddf85f40f..8fe938e72c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "^2.3.17", - "@budibase/client": "^2.3.17", + "@budibase/backend-core": "2.3.18-alpha.0", + "@budibase/client": "2.3.18-alpha.0", "@budibase/pro": "2.3.17", - "@budibase/string-templates": "^2.3.17", - "@budibase/types": "^2.3.17", + "@budibase/string-templates": "2.3.18-alpha.0", + "@budibase/types": "2.3.18-alpha.0", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 87b7c69dbb..edae9518ca 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 8417cadc06..3153bad674 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index cd25468c6b..1584019514 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.17", + "version": "2.3.18-alpha.0", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "^2.3.17", + "@budibase/backend-core": "2.3.18-alpha.0", "@budibase/pro": "2.3.17", - "@budibase/string-templates": "^2.3.17", - "@budibase/types": "^2.3.17", + "@budibase/string-templates": "2.3.18-alpha.0", + "@budibase/types": "2.3.18-alpha.0", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From 1742055c3cee7c91b5b27489066db5e827d8a07b Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 17:07:51 +0000 Subject: [PATCH 29/53] Update pro version to 2.3.18-alpha.0 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 45 ++++++++++++++++++---------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 58 ++++++++++++++++++------------------ 4 files changed, 60 insertions(+), 47 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 8fe938e72c..5812a84717 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.18-alpha.0", "@budibase/client": "2.3.18-alpha.0", - "@budibase/pro": "2.3.17", + "@budibase/pro": "2.3.18-alpha.0", "@budibase/string-templates": "2.3.18-alpha.0", "@budibase/types": "2.3.18-alpha.0", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 95d377f24a..4d31641d3f 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,13 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17": - version "2.3.17" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17.tgz#27c8c2144bfda1533b43da6de7111c0819aea6a5" - integrity sha512-KcmF2OrNLjLbFtNbYD4ZufnsnwmN2Ez/occgWiecvFRAHOhpkm+Hoy6VggpG1YJBp1DG9kLh3WAZbeYI3QoJbw== +"@budibase/backend-core@2.3.18-alpha.0": + version "2.3.18-alpha.0" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.0.tgz#c0a64a150c1fef9cc69f95f0aece4e857d64438d" + integrity sha512-ugD+WMoFwpXm+moSLHUgaBOu4XpX0+5UhmMWcNeRtH0Yd9GpDh2QzwtoN8BtXq8k5gkVEyoNSz+6oxKfNkNVdQ== dependencies: "@budibase/nano" "10.1.1" - "@budibase/types" "^2.3.17" + "@budibase/pouchdb-replication-stream" "1.2.10" + "@budibase/types" "2.3.18-alpha.0" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1309,7 +1310,6 @@ posthog-node "1.3.0" pouchdb "7.3.0" pouchdb-find "7.2.2" - pouchdb-replication-stream "1.2.9" redlock "4.2.0" sanitize-s3-objectkey "0.0.1" semver "7.3.7" @@ -1379,13 +1379,26 @@ qs "^6.11.0" tough-cookie "^4.1.2" -"@budibase/pro@2.3.17": - version "2.3.17" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17.tgz#1a05d3d13195fcfacac410305fcd0943fbbcd5c8" - integrity sha512-sdWuKRDbseu2POkyGfmiqAWp8M9jGmpD0FqaIEWGQmKdezvOKh3sGg0PGT4InoibbXcFf4vVB+HiofBedDFLkA== +"@budibase/pouchdb-replication-stream@1.2.10": + version "1.2.10" + resolved "https://registry.yarnpkg.com/@budibase/pouchdb-replication-stream/-/pouchdb-replication-stream-1.2.10.tgz#4100df2effd7c823edadddcdbdc380f6827eebf5" + integrity sha512-1zeorOwbelZ7HF5vFB+pKE8Mnh31om8k1M6T3AZXVULYTHLsyJrMTozSv5CJ1P8ZfOIJab09HDzCXDh2icFekg== dependencies: - "@budibase/backend-core" "2.3.17" - "@budibase/types" "2.3.17" + argsarray "0.0.1" + inherits "^2.0.3" + lodash.pick "^4.0.0" + ndjson "^1.4.3" + pouch-stream "^0.4.0" + pouchdb-promise "^6.0.4" + through2 "^2.0.0" + +"@budibase/pro@2.3.18-alpha.0": + version "2.3.18-alpha.0" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.0.tgz#e87a2449d9e2453766c0ea77539af359bf5a81ff" + integrity sha512-nKLhCdLxmBX+VY7LF6daH0/AItcHoQTmBB3tc0SP7y4OLcJZfBEYidoWqWJKCgdz6LScWWogLgbDIAC8t+LNzg== + dependencies: + "@budibase/backend-core" "2.3.18-alpha.0" + "@budibase/types" "2.3.18-alpha.0" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1411,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.17", "@budibase/types@^2.3.17": - version "2.3.17" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17.tgz#d97c1de5fb03c91ff7e55d7c8c3901e5e2e95995" - integrity sha512-p/6WgwNjVGfwyNLOofhPEG7S3tt5URxAVs+mPXuLn5bsAqRxxJ5XObvw8chijYXmewhGP0hjONQDkmDJ0FkHuA== +"@budibase/types@2.3.18-alpha.0": + version "2.3.18-alpha.0" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.0.tgz#14480e760c9e7931e884e9e0f8b1d5dd7e5d91c9" + integrity sha512-d+OcW2sNYw7VthMGrOBRY2Bz6iPQVWOnJ94XfYlBRJVIoYwBgudbYkOXPz/vQmHyjSUQFobrvs6UDeZ/3VJTaA== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index 1584019514..9fd2843ae4 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.18-alpha.0", - "@budibase/pro": "2.3.17", + "@budibase/pro": "2.3.18-alpha.0", "@budibase/string-templates": "2.3.18-alpha.0", "@budibase/types": "2.3.18-alpha.0", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 5c73052232..83417a2e84 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,13 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.17": - version "2.3.17" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.17.tgz#27c8c2144bfda1533b43da6de7111c0819aea6a5" - integrity sha512-KcmF2OrNLjLbFtNbYD4ZufnsnwmN2Ez/occgWiecvFRAHOhpkm+Hoy6VggpG1YJBp1DG9kLh3WAZbeYI3QoJbw== +"@budibase/backend-core@2.3.18-alpha.0": + version "2.3.18-alpha.0" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.0.tgz#c0a64a150c1fef9cc69f95f0aece4e857d64438d" + integrity sha512-ugD+WMoFwpXm+moSLHUgaBOu4XpX0+5UhmMWcNeRtH0Yd9GpDh2QzwtoN8BtXq8k5gkVEyoNSz+6oxKfNkNVdQ== dependencies: "@budibase/nano" "10.1.1" - "@budibase/types" "^2.3.17" + "@budibase/pouchdb-replication-stream" "1.2.10" + "@budibase/types" "2.3.18-alpha.0" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -506,7 +507,6 @@ posthog-node "1.3.0" pouchdb "7.3.0" pouchdb-find "7.2.2" - pouchdb-replication-stream "1.2.9" redlock "4.2.0" sanitize-s3-objectkey "0.0.1" semver "7.3.7" @@ -526,13 +526,26 @@ qs "^6.11.0" tough-cookie "^4.1.2" -"@budibase/pro@2.3.17": - version "2.3.17" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.17.tgz#1a05d3d13195fcfacac410305fcd0943fbbcd5c8" - integrity sha512-sdWuKRDbseu2POkyGfmiqAWp8M9jGmpD0FqaIEWGQmKdezvOKh3sGg0PGT4InoibbXcFf4vVB+HiofBedDFLkA== +"@budibase/pouchdb-replication-stream@1.2.10": + version "1.2.10" + resolved "https://registry.yarnpkg.com/@budibase/pouchdb-replication-stream/-/pouchdb-replication-stream-1.2.10.tgz#4100df2effd7c823edadddcdbdc380f6827eebf5" + integrity sha512-1zeorOwbelZ7HF5vFB+pKE8Mnh31om8k1M6T3AZXVULYTHLsyJrMTozSv5CJ1P8ZfOIJab09HDzCXDh2icFekg== dependencies: - "@budibase/backend-core" "2.3.17" - "@budibase/types" "2.3.17" + argsarray "0.0.1" + inherits "^2.0.3" + lodash.pick "^4.0.0" + ndjson "^1.4.3" + pouch-stream "^0.4.0" + pouchdb-promise "^6.0.4" + through2 "^2.0.0" + +"@budibase/pro@2.3.18-alpha.0": + version "2.3.18-alpha.0" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.0.tgz#e87a2449d9e2453766c0ea77539af359bf5a81ff" + integrity sha512-nKLhCdLxmBX+VY7LF6daH0/AItcHoQTmBB3tc0SP7y4OLcJZfBEYidoWqWJKCgdz6LScWWogLgbDIAC8t+LNzg== + dependencies: + "@budibase/backend-core" "2.3.18-alpha.0" + "@budibase/types" "2.3.18-alpha.0" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -540,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.17", "@budibase/types@^2.3.17": - version "2.3.17" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.17.tgz#d97c1de5fb03c91ff7e55d7c8c3901e5e2e95995" - integrity sha512-p/6WgwNjVGfwyNLOofhPEG7S3tt5URxAVs+mPXuLn5bsAqRxxJ5XObvw8chijYXmewhGP0hjONQDkmDJ0FkHuA== +"@budibase/types@2.3.18-alpha.0": + version "2.3.18-alpha.0" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.0.tgz#14480e760c9e7931e884e9e0f8b1d5dd7e5d91c9" + integrity sha512-d+OcW2sNYw7VthMGrOBRY2Bz6iPQVWOnJ94XfYlBRJVIoYwBgudbYkOXPz/vQmHyjSUQFobrvs6UDeZ/3VJTaA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -6780,19 +6793,6 @@ pouchdb-promise@6.4.3, pouchdb-promise@^6.0.4: dependencies: lie "3.1.1" -pouchdb-replication-stream@1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/pouchdb-replication-stream/-/pouchdb-replication-stream-1.2.9.tgz#aa4fa5d8f52df4825392f18e07c7e11acffc650a" - integrity sha512-hM8XRBfamTTUwRhKwLS/jSNouBhn9R/4ugdHNRD1EvJzwV8iImh6sDYbCU9PGuznjyOjXz6vpFRzKeI2KYfwnQ== - dependencies: - argsarray "0.0.1" - inherits "^2.0.3" - lodash.pick "^4.0.0" - ndjson "^1.4.3" - pouch-stream "^0.4.0" - pouchdb-promise "^6.0.4" - through2 "^2.0.0" - pouchdb-selector-core@7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/pouchdb-selector-core/-/pouchdb-selector-core-7.2.2.tgz#264d7436a8c8ac3801f39960e79875ef7f3879a0" From 940de8b6a0a6e013b95852e7d8268b205150f371 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 21 Feb 2023 17:13:24 +0000 Subject: [PATCH 30/53] Run CI steps in parallel (#9760) * Parallel CI * Add build to integration test * Add checkout to top of each run * Revert branch update for ci job * Experiment with --runInBand for CI * Fix intermittent backend-core migration test failure * Fix hanging worker redis connection * Update naming from reset to newTenant --- .github/workflows/budibase_ci.yml | 94 ++++++++------ .husky/pre-commit | 2 - packages/backend-core/jest.config.ts | 10 +- packages/backend-core/package.json | 2 +- .../passport/sso/tests/google.spec.ts | 2 +- .../backend-core/src/migrations/migrations.ts | 116 +++++++++--------- ...x.spec.js.snap => migrations.spec.ts.snap} | 0 .../src/migrations/tests/index.spec.js | 57 --------- .../src/migrations/tests/migrations.spec.ts | 64 ++++++++++ packages/backend-core/src/redis/init.ts | 6 +- packages/backend-core/src/redis/redis.ts | 5 + .../tests/utilities/DBTestConfiguration.ts | 4 + packages/server/jest.config.ts | 21 ++-- packages/server/package.json | 2 +- packages/server/specs/resources/query.js | 2 +- packages/server/specs/resources/table.js | 2 +- .../src/api/routes/tests/static.spec.js | 12 -- .../server/src/api/routes/tests/user.spec.js | 5 +- packages/server/src/utilities/redis.ts | 2 + packages/worker/jest.config.ts | 25 ++-- packages/worker/package.json | 2 +- packages/worker/src/utilities/redis.ts | 2 + 22 files changed, 227 insertions(+), 210 deletions(-) rename packages/backend-core/src/migrations/tests/__snapshots__/{index.spec.js.snap => migrations.spec.ts.snap} (100%) delete mode 100644 packages/backend-core/src/migrations/tests/index.spec.js create mode 100644 packages/backend-core/src/migrations/tests/migrations.spec.ts diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index c07f9b2c28..e0263546ff 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -11,7 +11,6 @@ on: branches: - master - develop - - release workflow_dispatch: env: @@ -20,9 +19,53 @@ env: PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - run: yarn + - run: yarn lint + build: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install Pro + run: yarn install:pro $BRANCH $BASE_BRANCH + - run: yarn + - run: yarn bootstrap + - run: yarn build + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install Pro + run: yarn install:pro $BRANCH $BASE_BRANCH + - run: yarn + - run: yarn bootstrap + - run: yarn test + - uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml + name: codecov-umbrella + verbose: true + + integration-test: + runs-on: ubuntu-latest services: couchdb: image: ibmcom/couchdb3 @@ -31,39 +74,18 @@ jobs: COUCHDB_USER: budibase ports: - 4567:5984 - - strategy: - matrix: - node-version: [14.x] - steps: - - uses: actions/checkout@v2 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - - name: Install Pro - run: yarn install:pro $BRANCH $BASE_BRANCH - - - run: yarn - - run: yarn bootstrap - - run: yarn lint - - run: yarn build - - run: yarn test - env: - CI: true - name: Budibase CI - - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml - name: codecov-umbrella - verbose: true - - - name: QA Core Integration Tests - run: | - cd qa-core - yarn - yarn api:test:ci \ No newline at end of file + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install Pro + run: yarn install:pro $BRANCH $BASE_BRANCH + - run: yarn + - run: yarn bootstrap + - run: yarn build + - run: | + cd qa-core + yarn + yarn api:test:ci diff --git a/.husky/pre-commit b/.husky/pre-commit index 3b614330e0..6700f51282 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,2 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" - -yarn run lint diff --git a/packages/backend-core/jest.config.ts b/packages/backend-core/jest.config.ts index 0483fb073a..1e69797e71 100644 --- a/packages/backend-core/jest.config.ts +++ b/packages/backend-core/jest.config.ts @@ -9,15 +9,9 @@ const baseConfig: Config.InitialProjectOptions = { transform: { "^.+\\.ts?$": "@swc/jest", }, -} - -if (!process.env.CI) { - // use sources when not in CI - baseConfig.moduleNameMapper = { + moduleNameMapper: { "@budibase/types": "/../types/src", - } -} else { - console.log("Running tests with compiled dependency sources") + }, } const config: Config.InitialOptions = { diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index c03600f5da..a32f5fd4dd 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -18,7 +18,7 @@ "build:pro": "../../scripts/pro/build.sh", "postbuild": "yarn run build:pro", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", - "test": "jest --coverage", + "test": "jest --coverage --runInBand", "test:watch": "jest --watchAll" }, "dependencies": { diff --git a/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts index eb8ffc9b71..d0689a1f0a 100644 --- a/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts +++ b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts @@ -19,7 +19,7 @@ describe("google", () => { const callbackUrl = generator.url() it("should create successfully create a google strategy", async () => { - await google.strategyFactory(googleConfig, callbackUrl) + await google.strategyFactory(googleConfig, callbackUrl, mockSaveUserFn) const expectedOptions = { clientID: googleConfig.clientID, diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts index 79c7eb55ea..2e3524775f 100644 --- a/packages/backend-core/src/migrations/migrations.ts +++ b/packages/backend-core/src/migrations/migrations.ts @@ -4,7 +4,7 @@ import { StaticDatabases, getAllApps, getGlobalDBName, - doWithDB, + getDB, } from "../db" import environment from "../environment" import * as platform from "../platform" @@ -86,66 +86,65 @@ export const runMigration = async ( count++ const lengthStatement = length > 1 ? `[${count}/${length}]` : "" - await doWithDB(dbName, async (db: any) => { - try { - const doc = await getMigrationsDoc(db) + const db = getDB(dbName) + try { + const doc = await getMigrationsDoc(db) - // the migration has already been run - if (doc[migrationName]) { - // check for force - if ( - options.force && - options.force[migrationType] && - options.force[migrationType].includes(migrationName) - ) { - log( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing` - ) - } else { - // no force, exit - return - } - } - - // check if the migration is not a no-op - if (!options.noOp) { + // the migration has already been run + if (doc[migrationName]) { + // check for force + if ( + options.force && + options.force[migrationType] && + options.force[migrationType].includes(migrationName) + ) { log( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}` - ) - - if (migration.preventRetry) { - // eagerly set the completion date - // so that we never run this migration twice even upon failure - doc[migrationName] = Date.now() - const response = await db.put(doc) - doc._rev = response.rev - } - - // run the migration - if (migrationType === MigrationType.APP) { - await context.doInAppContext(db.name, async () => { - await migration.fn(db) - }) - } else { - await migration.fn(db) - } - - log( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete` + `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing` ) + } else { + // no force, exit + return } - - // mark as complete - doc[migrationName] = Date.now() - await db.put(doc) - } catch (err) { - console.error( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `, - err - ) - throw err } - }) + + // check if the migration is not a no-op + if (!options.noOp) { + log( + `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}` + ) + + if (migration.preventRetry) { + // eagerly set the completion date + // so that we never run this migration twice even upon failure + doc[migrationName] = Date.now() + const response = await db.put(doc) + doc._rev = response.rev + } + + // run the migration + if (migrationType === MigrationType.APP) { + await context.doInAppContext(db.name, async () => { + await migration.fn(db) + }) + } else { + await migration.fn(db) + } + + log( + `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete` + ) + } + + // mark as complete + doc[migrationName] = Date.now() + await db.put(doc) + } catch (err) { + console.error( + `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `, + err + ) + throw err + } } } @@ -185,7 +184,10 @@ export const runMigrations = async ( // for all migrations for (const migration of migrations) { // run the migration - await context.doInTenant(tenantId, () => runMigration(migration, options)) + await context.doInTenant( + tenantId, + async () => await runMigration(migration, options) + ) } } console.log("Migrations complete") diff --git a/packages/backend-core/src/migrations/tests/__snapshots__/index.spec.js.snap b/packages/backend-core/src/migrations/tests/__snapshots__/migrations.spec.ts.snap similarity index 100% rename from packages/backend-core/src/migrations/tests/__snapshots__/index.spec.js.snap rename to packages/backend-core/src/migrations/tests/__snapshots__/migrations.spec.ts.snap diff --git a/packages/backend-core/src/migrations/tests/index.spec.js b/packages/backend-core/src/migrations/tests/index.spec.js deleted file mode 100644 index c1915510c3..0000000000 --- a/packages/backend-core/src/migrations/tests/index.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -require("../../../tests") -const { runMigrations, getMigrationsDoc } = require("../index") -const { getGlobalDBName, getDB } = require("../../db") - -const { structures, testEnv } = require("../../../tests") -testEnv.multiTenant() - -let db - -describe("migrations", () => { - - const migrationFunction = jest.fn() - - const MIGRATIONS = [{ - type: "global", - name: "test", - fn: migrationFunction - }] - - let tenantId - - beforeEach(() => { - tenantId = structures.tenant.id() - db = getDB(getGlobalDBName(tenantId)) - }) - - afterEach(async () => { - jest.clearAllMocks() - await db.destroy() - }) - - const migrate = () => { - return runMigrations(MIGRATIONS, { tenantIds: [tenantId]}) - } - - it("should run a new migration", async () => { - await migrate() - expect(migrationFunction).toHaveBeenCalled() - const doc = await getMigrationsDoc(db) - expect(doc.test).toBeDefined() - }) - - it("should match snapshot", async () => { - await migrate() - const doc = await getMigrationsDoc(db) - expect(doc).toMatchSnapshot() - }) - - it("should skip a previously run migration", async () => { - await migrate() - const previousMigrationTime = await getMigrationsDoc(db).test - await migrate() - const currentMigrationTime = await getMigrationsDoc(db).test - expect(migrationFunction).toHaveBeenCalledTimes(1) - expect(currentMigrationTime).toBe(previousMigrationTime) - }) -}) \ No newline at end of file diff --git a/packages/backend-core/src/migrations/tests/migrations.spec.ts b/packages/backend-core/src/migrations/tests/migrations.spec.ts new file mode 100644 index 0000000000..c74ab816c1 --- /dev/null +++ b/packages/backend-core/src/migrations/tests/migrations.spec.ts @@ -0,0 +1,64 @@ +import { testEnv, DBTestConfiguration } from "../../../tests" +import * as migrations from "../index" +import * as context from "../../context" +import { MigrationType } from "@budibase/types" + +testEnv.multiTenant() + +describe("migrations", () => { + const config = new DBTestConfiguration() + + const migrationFunction = jest.fn() + + const MIGRATIONS = [ + { + type: MigrationType.GLOBAL, + name: "test" as any, + fn: migrationFunction, + }, + ] + + beforeEach(() => { + config.newTenant() + }) + + afterEach(async () => { + jest.clearAllMocks() + }) + + const migrate = () => { + return migrations.runMigrations(MIGRATIONS, { + tenantIds: [config.tenantId], + }) + } + + it("should run a new migration", async () => { + await config.doInTenant(async () => { + await migrate() + expect(migrationFunction).toHaveBeenCalled() + const db = context.getGlobalDB() + const doc = await migrations.getMigrationsDoc(db) + expect(doc.test).toBeDefined() + }) + }) + + it("should match snapshot", async () => { + await config.doInTenant(async () => { + await migrate() + const doc = await migrations.getMigrationsDoc(context.getGlobalDB()) + expect(doc).toMatchSnapshot() + }) + }) + + it("should skip a previously run migration", async () => { + await config.doInTenant(async () => { + const db = context.getGlobalDB() + await migrate() + const previousDoc = await migrations.getMigrationsDoc(db) + await migrate() + const currentDoc = await migrations.getMigrationsDoc(db) + expect(migrationFunction).toHaveBeenCalledTimes(1) + expect(currentDoc.test).toBe(previousDoc.test) + }) + }) +}) diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index 00329ffb84..485268edad 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -20,13 +20,17 @@ async function init() { ).init() } -process.on("exit", async () => { +export async function shutdown() { if (userClient) await userClient.finish() if (sessionClient) await sessionClient.finish() if (appClient) await appClient.finish() if (cacheClient) await cacheClient.finish() if (writethroughClient) await writethroughClient.finish() if (lockClient) await lockClient.finish() +} + +process.on("exit", async () => { + await shutdown() }) export async function getUserClient() { diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 2669cd816a..951369496a 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -91,6 +91,11 @@ function init(selectDb = DEFAULT_SELECT_DB) { } // attach handlers client.on("end", (err: Error) => { + if (env.isTest()) { + // don't try to re-connect in test env + // allow the process to exit + return + } connectionError(selectDb, timeout, err) }) client.on("error", (err: Error) => { diff --git a/packages/backend-core/tests/utilities/DBTestConfiguration.ts b/packages/backend-core/tests/utilities/DBTestConfiguration.ts index cad62e2979..e5e57a99a3 100644 --- a/packages/backend-core/tests/utilities/DBTestConfiguration.ts +++ b/packages/backend-core/tests/utilities/DBTestConfiguration.ts @@ -12,6 +12,10 @@ class DBTestConfiguration { this.tenantId = structures.tenant.id() } + newTenant() { + this.tenantId = structures.tenant.id() + } + // TENANCY doInTenant(task: any) { diff --git a/packages/server/jest.config.ts b/packages/server/jest.config.ts index 41558d4c8e..331912aa19 100644 --- a/packages/server/jest.config.ts +++ b/packages/server/jest.config.ts @@ -11,22 +11,17 @@ const baseConfig: Config.InitialProjectOptions = { transform: { "^.+\\.ts?$": "@swc/jest", }, -} - -if (!process.env.CI) { - // use sources when not in CI - baseConfig.moduleNameMapper = { + moduleNameMapper: { "@budibase/backend-core/(.*)": "/../backend-core/$1", "@budibase/backend-core": "/../backend-core/src", "@budibase/types": "/../types/src", - } - // add pro sources if they exist - if (fs.existsSync("../../../budibase-pro")) { - baseConfig.moduleNameMapper["@budibase/pro"] = - "/../../../budibase-pro/packages/pro/src" - } -} else { - console.log("Running tests with compiled dependency sources") + }, +} + +// add pro sources if they exist +if (fs.existsSync("../../../budibase-pro")) { + baseConfig.moduleNameMapper["@budibase/pro"] = + "/../../../budibase-pro/packages/pro/src" } const config: Config.InitialOptions = { diff --git a/packages/server/package.json b/packages/server/package.json index 5812a84717..3eb133e272 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -14,7 +14,7 @@ "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js", "postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/", - "test": "jest --coverage --maxWorkers=2", + "test": "jest --coverage --runInBand", "test:watch": "jest --watch", "predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client", "build:docker": "yarn run predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION", diff --git a/packages/server/specs/resources/query.js b/packages/server/specs/resources/query.js index 10544ee7eb..1442e46a04 100644 --- a/packages/server/specs/resources/query.js +++ b/packages/server/specs/resources/query.js @@ -1,6 +1,6 @@ const Resource = require("./utils/Resource") const { object } = require("./utils") -const { BaseQueryVerbs } = require("../../dist/constants") +const { BaseQueryVerbs } = require("../../src/constants") const query = { _id: "query_datasource_plus_4d8be0c506b9465daf4bf84d890fdab6_454854487c574d45bc4029b1e153219e", diff --git a/packages/server/specs/resources/table.js b/packages/server/specs/resources/table.js index 9bc57daf42..523a3a9dfd 100644 --- a/packages/server/specs/resources/table.js +++ b/packages/server/specs/resources/table.js @@ -2,7 +2,7 @@ const { FieldTypes, RelationshipTypes, FormulaTypes, -} = require("../../dist/constants") +} = require("../../src/constants") const { object } = require("./utils") const Resource = require("./utils/Resource") diff --git a/packages/server/src/api/routes/tests/static.spec.js b/packages/server/src/api/routes/tests/static.spec.js index a0532f12fb..13d963d057 100644 --- a/packages/server/src/api/routes/tests/static.spec.js +++ b/packages/server/src/api/routes/tests/static.spec.js @@ -13,18 +13,6 @@ describe("/static", () => { app = await config.init() }) - describe("/builder", () => { - it("should serve the builder", async () => { - const res = await request - .get("/builder/portal") - .set(config.defaultHeaders()) - .expect("Content-Type", /text\/html/) - .expect(200) - - expect(res.text).toContain("Budibase") - }) - }) - describe("/app", () => { beforeEach(() => { jest.clearAllMocks() diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index bae784cf3d..6b674a8479 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -1,4 +1,4 @@ -const { roles, utils } = require("@budibase/backend-core") +const { roles } = require("@budibase/backend-core") const { checkPermissionsEndpoint } = require("./utilities/TestFunctions") const setup = require("./utilities") const { BUILTIN_ROLE_IDS } = roles @@ -21,8 +21,7 @@ describe("/users", () => { afterAll(setup.afterAll) - // For some reason this cannot be a beforeAll or the test "should be able to update the user" fail - beforeEach(async () => { + beforeAll(async () => { await config.init() }) diff --git a/packages/server/src/utilities/redis.ts b/packages/server/src/utilities/redis.ts index 1b7a3ce64c..dc37baae58 100644 --- a/packages/server/src/utilities/redis.ts +++ b/packages/server/src/utilities/redis.ts @@ -21,6 +21,8 @@ export async function shutdown() { if (devAppClient) await devAppClient.finish() if (debounceClient) await debounceClient.finish() if (flagClient) await flagClient.finish() + // shutdown core clients + await redis.clients.shutdown() console.log("Redis shutdown") } diff --git a/packages/worker/jest.config.ts b/packages/worker/jest.config.ts index 8b0514211b..cdacfa411a 100644 --- a/packages/worker/jest.config.ts +++ b/packages/worker/jest.config.ts @@ -12,24 +12,19 @@ const config: Config.InitialOptions = { transform: { "^.+\\.ts?$": "@swc/jest", }, -} - -if (!process.env.CI) { - // use sources when not in CI - config.moduleNameMapper = { + moduleNameMapper: { "@budibase/backend-core/(.*)": "/../backend-core/$1", "@budibase/backend-core": "/../backend-core/src", "@budibase/types": "/../types/src", - } - // add pro sources if they exist - if (fs.existsSync("../../../budibase-pro")) { - config.moduleNameMapper["@budibase/pro/(.*)"] = - "/../../../budibase-pro/packages/pro/$1" - config.moduleNameMapper["@budibase/pro"] = - "/../../../budibase-pro/packages/pro/src" - } -} else { - console.log("Running tests with compiled dependency sources") + }, +} + +// add pro sources if they exist +if (fs.existsSync("../../../budibase-pro")) { + config.moduleNameMapper["@budibase/pro/(.*)"] = + "/../../../budibase-pro/packages/pro/$1" + config.moduleNameMapper["@budibase/pro"] = + "/../../../budibase-pro/packages/pro/src" } export default config diff --git a/packages/worker/package.json b/packages/worker/package.json index 9fd2843ae4..0fb3abe53b 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -22,7 +22,7 @@ "build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION", "dev:stack:init": "node ./scripts/dev/manage.js init", "dev:builder": "npm run dev:stack:init && nodemon", - "test": "jest --coverage --maxWorkers=2", + "test": "jest --coverage --runInBand", "test:watch": "jest --watch", "env:multi:enable": "node scripts/multiTenancy.js enable", "env:multi:disable": "node scripts/multiTenancy.js disable", diff --git a/packages/worker/src/utilities/redis.ts b/packages/worker/src/utilities/redis.ts index 893ec9f0a8..9171fe97ee 100644 --- a/packages/worker/src/utilities/redis.ts +++ b/packages/worker/src/utilities/redis.ts @@ -54,6 +54,8 @@ export async function init() { export async function shutdown() { if (pwResetClient) await pwResetClient.finish() if (invitationClient) await invitationClient.finish() + // shutdown core clients + await redis.clients.shutdown() console.log("Redis shutdown") } From deb76ca7f38ffccebc0d8c7a343f9cbcb7247f1e Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 17:33:47 +0000 Subject: [PATCH 31/53] v2.3.18-alpha.1 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index a881722de6..64c2e16b71 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index a32f5fd4dd..a5603730de 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.18-alpha.0", + "@budibase/types": "2.3.18-alpha.1", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 9e3aea5fea..d45ce99270 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.18-alpha.0", + "@budibase/string-templates": "2.3.18-alpha.1", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index f8f6ac289d..b8b9c2e79c 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.0", - "@budibase/client": "2.3.18-alpha.0", - "@budibase/frontend-core": "2.3.18-alpha.0", - "@budibase/string-templates": "2.3.18-alpha.0", + "@budibase/bbui": "2.3.18-alpha.1", + "@budibase/client": "2.3.18-alpha.1", + "@budibase/frontend-core": "2.3.18-alpha.1", + "@budibase/string-templates": "2.3.18-alpha.1", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index f2044f9c8b..1b675c1f37 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.0", - "@budibase/string-templates": "2.3.18-alpha.0", - "@budibase/types": "2.3.18-alpha.0", + "@budibase/backend-core": "2.3.18-alpha.1", + "@budibase/string-templates": "2.3.18-alpha.1", + "@budibase/types": "2.3.18-alpha.1", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index a784cfa6bc..b3f8734aff 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.0", - "@budibase/frontend-core": "2.3.18-alpha.0", - "@budibase/string-templates": "2.3.18-alpha.0", + "@budibase/bbui": "2.3.18-alpha.1", + "@budibase/frontend-core": "2.3.18-alpha.1", + "@budibase/string-templates": "2.3.18-alpha.1", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index b0d39ed450..1753a0ecbd 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.18-alpha.0", + "@budibase/bbui": "2.3.18-alpha.1", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 597c3dff84..8b58c801fb 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 3eb133e272..d88c41f056 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.18-alpha.0", - "@budibase/client": "2.3.18-alpha.0", + "@budibase/backend-core": "2.3.18-alpha.1", + "@budibase/client": "2.3.18-alpha.1", "@budibase/pro": "2.3.18-alpha.0", - "@budibase/string-templates": "2.3.18-alpha.0", - "@budibase/types": "2.3.18-alpha.0", + "@budibase/string-templates": "2.3.18-alpha.1", + "@budibase/types": "2.3.18-alpha.1", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index edae9518ca..c2e06902e2 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index 3153bad674..d378335af8 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 0fb3abe53b..b2c7d93f94 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.18-alpha.0", + "version": "2.3.18-alpha.1", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.0", + "@budibase/backend-core": "2.3.18-alpha.1", "@budibase/pro": "2.3.18-alpha.0", - "@budibase/string-templates": "2.3.18-alpha.0", - "@budibase/types": "2.3.18-alpha.0", + "@budibase/string-templates": "2.3.18-alpha.1", + "@budibase/types": "2.3.18-alpha.1", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From 1e0e3ce19ef74e7f62afd78c0986e7ade013d38a Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 21 Feb 2023 17:37:00 +0000 Subject: [PATCH 32/53] Update pro version to 2.3.18-alpha.1 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index d88c41f056..c8c48044ab 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.18-alpha.1", "@budibase/client": "2.3.18-alpha.1", - "@budibase/pro": "2.3.18-alpha.0", + "@budibase/pro": "2.3.18-alpha.1", "@budibase/string-templates": "2.3.18-alpha.1", "@budibase/types": "2.3.18-alpha.1", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 4d31641d3f..125ca007dd 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.0": - version "2.3.18-alpha.0" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.0.tgz#c0a64a150c1fef9cc69f95f0aece4e857d64438d" - integrity sha512-ugD+WMoFwpXm+moSLHUgaBOu4XpX0+5UhmMWcNeRtH0Yd9GpDh2QzwtoN8BtXq8k5gkVEyoNSz+6oxKfNkNVdQ== +"@budibase/backend-core@2.3.18-alpha.1": + version "2.3.18-alpha.1" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.1.tgz#af5110dd16dd6a1bf378e4fadd312f920afadd3e" + integrity sha512-p5oNTWYjHMj2HdKrAP2vrVWCiU8tTQ978jgyvwgtBC8TfElowHN5Ly7HLu67v8IeSGK9BGZ7sz6kcS6ApcZa8Q== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.0" + "@budibase/types" "2.3.18-alpha.1" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.0": - version "2.3.18-alpha.0" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.0.tgz#e87a2449d9e2453766c0ea77539af359bf5a81ff" - integrity sha512-nKLhCdLxmBX+VY7LF6daH0/AItcHoQTmBB3tc0SP7y4OLcJZfBEYidoWqWJKCgdz6LScWWogLgbDIAC8t+LNzg== +"@budibase/pro@2.3.18-alpha.1": + version "2.3.18-alpha.1" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.1.tgz#de1b7b271102fc3c62138dfdd130bf9af9f4c53f" + integrity sha512-mqyqsE0jg0kBbNrACti69iMfaVO/GgfHYsRizrN9xMwNRUamiwYROLvWYWouyCJYJAkTCTjll6Wex8CpinllCA== dependencies: - "@budibase/backend-core" "2.3.18-alpha.0" - "@budibase/types" "2.3.18-alpha.0" + "@budibase/backend-core" "2.3.18-alpha.1" + "@budibase/types" "2.3.18-alpha.1" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.18-alpha.0": - version "2.3.18-alpha.0" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.0.tgz#14480e760c9e7931e884e9e0f8b1d5dd7e5d91c9" - integrity sha512-d+OcW2sNYw7VthMGrOBRY2Bz6iPQVWOnJ94XfYlBRJVIoYwBgudbYkOXPz/vQmHyjSUQFobrvs6UDeZ/3VJTaA== +"@budibase/types@2.3.18-alpha.1": + version "2.3.18-alpha.1" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.1.tgz#d917e512d8644d94d71d0c8cb0d2e9cacecee720" + integrity sha512-35cX1NJfDwRb8DX3RkWVx46yT9+0fMndWSpOX+183ps0BTMyQ0UJBb80pNSsKMGjlgE+loQEXf0LymZ6vr9ucQ== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index b2c7d93f94..90c7a2773d 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.18-alpha.1", - "@budibase/pro": "2.3.18-alpha.0", + "@budibase/pro": "2.3.18-alpha.1", "@budibase/string-templates": "2.3.18-alpha.1", "@budibase/types": "2.3.18-alpha.1", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 83417a2e84..358d122930 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.0": - version "2.3.18-alpha.0" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.0.tgz#c0a64a150c1fef9cc69f95f0aece4e857d64438d" - integrity sha512-ugD+WMoFwpXm+moSLHUgaBOu4XpX0+5UhmMWcNeRtH0Yd9GpDh2QzwtoN8BtXq8k5gkVEyoNSz+6oxKfNkNVdQ== +"@budibase/backend-core@2.3.18-alpha.1": + version "2.3.18-alpha.1" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.1.tgz#af5110dd16dd6a1bf378e4fadd312f920afadd3e" + integrity sha512-p5oNTWYjHMj2HdKrAP2vrVWCiU8tTQ978jgyvwgtBC8TfElowHN5Ly7HLu67v8IeSGK9BGZ7sz6kcS6ApcZa8Q== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.0" + "@budibase/types" "2.3.18-alpha.1" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.0": - version "2.3.18-alpha.0" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.0.tgz#e87a2449d9e2453766c0ea77539af359bf5a81ff" - integrity sha512-nKLhCdLxmBX+VY7LF6daH0/AItcHoQTmBB3tc0SP7y4OLcJZfBEYidoWqWJKCgdz6LScWWogLgbDIAC8t+LNzg== +"@budibase/pro@2.3.18-alpha.1": + version "2.3.18-alpha.1" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.1.tgz#de1b7b271102fc3c62138dfdd130bf9af9f4c53f" + integrity sha512-mqyqsE0jg0kBbNrACti69iMfaVO/GgfHYsRizrN9xMwNRUamiwYROLvWYWouyCJYJAkTCTjll6Wex8CpinllCA== dependencies: - "@budibase/backend-core" "2.3.18-alpha.0" - "@budibase/types" "2.3.18-alpha.0" + "@budibase/backend-core" "2.3.18-alpha.1" + "@budibase/types" "2.3.18-alpha.1" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.18-alpha.0": - version "2.3.18-alpha.0" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.0.tgz#14480e760c9e7931e884e9e0f8b1d5dd7e5d91c9" - integrity sha512-d+OcW2sNYw7VthMGrOBRY2Bz6iPQVWOnJ94XfYlBRJVIoYwBgudbYkOXPz/vQmHyjSUQFobrvs6UDeZ/3VJTaA== +"@budibase/types@2.3.18-alpha.1": + version "2.3.18-alpha.1" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.1.tgz#d917e512d8644d94d71d0c8cb0d2e9cacecee720" + integrity sha512-35cX1NJfDwRb8DX3RkWVx46yT9+0fMndWSpOX+183ps0BTMyQ0UJBb80pNSsKMGjlgE+loQEXf0LymZ6vr9ucQ== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From 6a88cfc32db9b4211d96e4450ea7b21529794c55 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Wed, 22 Feb 2023 08:32:03 +0000 Subject: [PATCH 33/53] Update locks error logging (#9768) * Fix intermittent backend-core migration test failure * Update lock logging --- packages/backend-core/src/redis/redlock.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/redis/redlock.ts b/packages/backend-core/src/redis/redlock.ts index 2021da2b56..136d7f5d33 100644 --- a/packages/backend-core/src/redis/redlock.ts +++ b/packages/backend-core/src/redis/redlock.ts @@ -85,17 +85,20 @@ export const doWithLock = async (opts: LockOptions, task: any) => { const result = await task() return result } catch (e: any) { - console.log("lock error") + console.warn("lock error") // lock limit exceeded if (e.name === "LockError") { if (opts.type === LockType.TRY_ONCE) { // don't throw for try-once locks, they will always error // due to retry count (0) exceeded + console.warn(e) return } else { + console.error(e) throw e } } else { + console.error(e) throw e } } finally { From 288f853208f748b4a80d59f65c1e45c85e1146ab Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 22 Feb 2023 08:40:11 +0000 Subject: [PATCH 34/53] v2.3.18-alpha.2 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 64c2e16b71..fe2aecb942 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index a5603730de..03bf98ed05 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.18-alpha.1", + "@budibase/types": "2.3.18-alpha.2", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index d45ce99270..f223d0fecd 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.18-alpha.1", + "@budibase/string-templates": "2.3.18-alpha.2", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index b8b9c2e79c..f6252a4928 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.1", - "@budibase/client": "2.3.18-alpha.1", - "@budibase/frontend-core": "2.3.18-alpha.1", - "@budibase/string-templates": "2.3.18-alpha.1", + "@budibase/bbui": "2.3.18-alpha.2", + "@budibase/client": "2.3.18-alpha.2", + "@budibase/frontend-core": "2.3.18-alpha.2", + "@budibase/string-templates": "2.3.18-alpha.2", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1b675c1f37..0595259f56 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.1", - "@budibase/string-templates": "2.3.18-alpha.1", - "@budibase/types": "2.3.18-alpha.1", + "@budibase/backend-core": "2.3.18-alpha.2", + "@budibase/string-templates": "2.3.18-alpha.2", + "@budibase/types": "2.3.18-alpha.2", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index b3f8734aff..7154228c2c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.1", - "@budibase/frontend-core": "2.3.18-alpha.1", - "@budibase/string-templates": "2.3.18-alpha.1", + "@budibase/bbui": "2.3.18-alpha.2", + "@budibase/frontend-core": "2.3.18-alpha.2", + "@budibase/string-templates": "2.3.18-alpha.2", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 1753a0ecbd..30a202dd19 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.18-alpha.1", + "@budibase/bbui": "2.3.18-alpha.2", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8b58c801fb..a20fd61d9b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index c8c48044ab..caf3d09f76 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.18-alpha.1", - "@budibase/client": "2.3.18-alpha.1", + "@budibase/backend-core": "2.3.18-alpha.2", + "@budibase/client": "2.3.18-alpha.2", "@budibase/pro": "2.3.18-alpha.1", - "@budibase/string-templates": "2.3.18-alpha.1", - "@budibase/types": "2.3.18-alpha.1", + "@budibase/string-templates": "2.3.18-alpha.2", + "@budibase/types": "2.3.18-alpha.2", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index c2e06902e2..810eff7712 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index d378335af8..a5cfd06cbd 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 90c7a2773d..fdd5edcfc5 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.18-alpha.1", + "version": "2.3.18-alpha.2", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.1", + "@budibase/backend-core": "2.3.18-alpha.2", "@budibase/pro": "2.3.18-alpha.1", - "@budibase/string-templates": "2.3.18-alpha.1", - "@budibase/types": "2.3.18-alpha.1", + "@budibase/string-templates": "2.3.18-alpha.2", + "@budibase/types": "2.3.18-alpha.2", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From faaf01cd5355f09ce7141592d33a15d82e2a9935 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 22 Feb 2023 08:43:41 +0000 Subject: [PATCH 35/53] Update pro version to 2.3.18-alpha.2 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index caf3d09f76..e550a9ca38 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.18-alpha.2", "@budibase/client": "2.3.18-alpha.2", - "@budibase/pro": "2.3.18-alpha.1", + "@budibase/pro": "2.3.18-alpha.2", "@budibase/string-templates": "2.3.18-alpha.2", "@budibase/types": "2.3.18-alpha.2", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 125ca007dd..67db7d2d28 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.1": - version "2.3.18-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.1.tgz#af5110dd16dd6a1bf378e4fadd312f920afadd3e" - integrity sha512-p5oNTWYjHMj2HdKrAP2vrVWCiU8tTQ978jgyvwgtBC8TfElowHN5Ly7HLu67v8IeSGK9BGZ7sz6kcS6ApcZa8Q== +"@budibase/backend-core@2.3.18-alpha.2": + version "2.3.18-alpha.2" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.2.tgz#a18b075234d184961b03ca89c7ae4b8229cbf279" + integrity sha512-rWI/GGphASPRu3HdPDVPpzbvBBLdFWnD5FyGVJ1/lZ5s0A/npegXl7noaFAgVjrAPGOKWDsDv43sscMzl8ZWLw== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.1" + "@budibase/types" "2.3.18-alpha.2" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.1": - version "2.3.18-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.1.tgz#de1b7b271102fc3c62138dfdd130bf9af9f4c53f" - integrity sha512-mqyqsE0jg0kBbNrACti69iMfaVO/GgfHYsRizrN9xMwNRUamiwYROLvWYWouyCJYJAkTCTjll6Wex8CpinllCA== +"@budibase/pro@2.3.18-alpha.2": + version "2.3.18-alpha.2" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.2.tgz#50456af8151b4e2832c3f518ad10e381f2f4e70f" + integrity sha512-x0X4LjFsqgi4OMm+Mlk3NAVHFA3bakVTvOSoKz/7LLa5XN9DaD1AOj7Vwh6iNVuoMmarIS9lj9NlripFzMs1EQ== dependencies: - "@budibase/backend-core" "2.3.18-alpha.1" - "@budibase/types" "2.3.18-alpha.1" + "@budibase/backend-core" "2.3.18-alpha.2" + "@budibase/types" "2.3.18-alpha.2" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.18-alpha.1": - version "2.3.18-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.1.tgz#d917e512d8644d94d71d0c8cb0d2e9cacecee720" - integrity sha512-35cX1NJfDwRb8DX3RkWVx46yT9+0fMndWSpOX+183ps0BTMyQ0UJBb80pNSsKMGjlgE+loQEXf0LymZ6vr9ucQ== +"@budibase/types@2.3.18-alpha.2": + version "2.3.18-alpha.2" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.2.tgz#06ac9e16d0aff7eced989cd2133a26cee9d540ee" + integrity sha512-vNSu+E98jmBrQ0hx0Mza2whKScmW5O6mRmvyuJor6R/AhMHYn6k8ChuZsbLmx5C8oRdFT8rquaQxcbENl7I4eA== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index fdd5edcfc5..97f4444361 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.18-alpha.2", - "@budibase/pro": "2.3.18-alpha.1", + "@budibase/pro": "2.3.18-alpha.2", "@budibase/string-templates": "2.3.18-alpha.2", "@budibase/types": "2.3.18-alpha.2", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 358d122930..5949e5e5f8 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.1": - version "2.3.18-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.1.tgz#af5110dd16dd6a1bf378e4fadd312f920afadd3e" - integrity sha512-p5oNTWYjHMj2HdKrAP2vrVWCiU8tTQ978jgyvwgtBC8TfElowHN5Ly7HLu67v8IeSGK9BGZ7sz6kcS6ApcZa8Q== +"@budibase/backend-core@2.3.18-alpha.2": + version "2.3.18-alpha.2" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.2.tgz#a18b075234d184961b03ca89c7ae4b8229cbf279" + integrity sha512-rWI/GGphASPRu3HdPDVPpzbvBBLdFWnD5FyGVJ1/lZ5s0A/npegXl7noaFAgVjrAPGOKWDsDv43sscMzl8ZWLw== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.1" + "@budibase/types" "2.3.18-alpha.2" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.1": - version "2.3.18-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.1.tgz#de1b7b271102fc3c62138dfdd130bf9af9f4c53f" - integrity sha512-mqyqsE0jg0kBbNrACti69iMfaVO/GgfHYsRizrN9xMwNRUamiwYROLvWYWouyCJYJAkTCTjll6Wex8CpinllCA== +"@budibase/pro@2.3.18-alpha.2": + version "2.3.18-alpha.2" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.2.tgz#50456af8151b4e2832c3f518ad10e381f2f4e70f" + integrity sha512-x0X4LjFsqgi4OMm+Mlk3NAVHFA3bakVTvOSoKz/7LLa5XN9DaD1AOj7Vwh6iNVuoMmarIS9lj9NlripFzMs1EQ== dependencies: - "@budibase/backend-core" "2.3.18-alpha.1" - "@budibase/types" "2.3.18-alpha.1" + "@budibase/backend-core" "2.3.18-alpha.2" + "@budibase/types" "2.3.18-alpha.2" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.18-alpha.1": - version "2.3.18-alpha.1" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.1.tgz#d917e512d8644d94d71d0c8cb0d2e9cacecee720" - integrity sha512-35cX1NJfDwRb8DX3RkWVx46yT9+0fMndWSpOX+183ps0BTMyQ0UJBb80pNSsKMGjlgE+loQEXf0LymZ6vr9ucQ== +"@budibase/types@2.3.18-alpha.2": + version "2.3.18-alpha.2" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.2.tgz#06ac9e16d0aff7eced989cd2133a26cee9d540ee" + integrity sha512-vNSu+E98jmBrQ0hx0Mza2whKScmW5O6mRmvyuJor6R/AhMHYn6k8ChuZsbLmx5C8oRdFT8rquaQxcbENl7I4eA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From e7f8a8a8015e2d66a21c92da227b8f114a3019c1 Mon Sep 17 00:00:00 2001 From: Gerard Burns Date: Wed, 22 Feb 2023 10:03:11 +0000 Subject: [PATCH 36/53] Backups UI Changes (#9726) * Backups UI Changes * PR Feedback --------- Co-authored-by: Rory Powell --- packages/builder/src/helpers/userInitials.js | 13 +++++ .../_components/ActionsRenderer.svelte | 41 ++------------ .../_components/CreateBackupModal.svelte | 22 -------- .../backups/_components/NameRenderer.svelte | 8 --- .../_components/TimeAgoRenderer.svelte | 10 ++++ .../backups/_components/UserRenderer.svelte | 15 ++--- .../overview/[appId]/backups/index.svelte | 55 ++++++++++--------- packages/builder/src/stores/portal/auth.js | 12 +--- packages/builder/src/stores/portal/backups.js | 4 +- packages/frontend-core/src/api/backups.js | 3 +- 10 files changed, 67 insertions(+), 116 deletions(-) create mode 100644 packages/builder/src/helpers/userInitials.js delete mode 100644 packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/CreateBackupModal.svelte delete mode 100644 packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/NameRenderer.svelte create mode 100644 packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/TimeAgoRenderer.svelte diff --git a/packages/builder/src/helpers/userInitials.js b/packages/builder/src/helpers/userInitials.js new file mode 100644 index 0000000000..c87d38c494 --- /dev/null +++ b/packages/builder/src/helpers/userInitials.js @@ -0,0 +1,13 @@ +const getUserInitials = user => { + if (user.firstName && user.lastName) { + return user.firstName[0] + user.lastName[0] + } else if (user.firstName) { + return user.firstName[0] + } else if (user.email) { + return user.email[0] + } + + return "U" +} + +export default getUserInitials diff --git a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/ActionsRenderer.svelte b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/ActionsRenderer.svelte index 7dc302186a..4787112760 100644 --- a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/ActionsRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/ActionsRenderer.svelte @@ -3,31 +3,26 @@ ActionMenu, MenuItem, Icon, - Input, Heading, Body, Modal, } from "@budibase/bbui" import ConfirmDialog from "components/common/ConfirmDialog.svelte" import CreateRestoreModal from "./CreateRestoreModal.svelte" - import { createEventDispatcher, onMount } from "svelte" + import { createEventDispatcher } from "svelte" export let row let deleteDialog let restoreDialog - let updateDialog - let name let restoreBackupModal const dispatch = createEventDispatcher() - const onClickRestore = name => { + const onClickRestore = () => { dispatch("buttonclick", { type: "backupRestore", - name, backupId: row._id, - restoreBackupName: name, }) } @@ -38,21 +33,9 @@ }) } - const onClickUpdate = () => { - dispatch("buttonclick", { - type: "backupUpdate", - backupId: row._id, - name, - }) - } - async function downloadExport() { window.open(`/api/apps/${row.appId}/backups/${row._id}/file`, "_blank") } - - onMount(() => { - name = row.name - })
@@ -66,12 +49,11 @@ Delete Download {/if} - Rename
- onClickRestore(name)} /> + - Are you sure you wish to delete the backup - {row.name}? - This action cannot be undone. + Are you sure you wish to delete this backup? This action cannot be undone. - {row.name || "Backup"} + Backup {new Date(row.timestamp).toLocaleString()} - - - - diff --git a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/NameRenderer.svelte b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/NameRenderer.svelte deleted file mode 100644 index 93eda410fe..0000000000 --- a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/NameRenderer.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -{truncatedValue} diff --git a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/TimeAgoRenderer.svelte b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/TimeAgoRenderer.svelte new file mode 100644 index 0000000000..fd67b4010b --- /dev/null +++ b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/TimeAgoRenderer.svelte @@ -0,0 +1,10 @@ + + +{dayjs(value).fromNow()} diff --git a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/UserRenderer.svelte b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/UserRenderer.svelte index abab314d05..a9aabae857 100644 --- a/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/UserRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/overview/[appId]/backups/_components/UserRenderer.svelte @@ -1,17 +1,14 @@ -
- {#if value != null} -
{username}
- {/if} +
+
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/blankScreenPreview.png b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/blankScreenPreview.png new file mode 100644 index 0000000000000000000000000000000000000000..0511e2f4fe2293df2cd32157a6b54bdeedc8959f GIT binary patch literal 71063 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 literal 0 HcmV?d00001 From 910bac322cee72552ad462ce87a9b7f032ed37f5 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 23 Feb 2023 12:14:21 +0000 Subject: [PATCH 49/53] v2.3.18-alpha.7 --- lerna.json | 2 +- packages/backend-core/package.json | 4 ++-- packages/bbui/package.json | 4 ++-- packages/builder/package.json | 10 +++++----- packages/cli/package.json | 8 ++++---- packages/client/package.json | 8 ++++---- packages/frontend-core/package.json | 4 ++-- packages/sdk/package.json | 2 +- packages/server/package.json | 10 +++++----- packages/string-templates/package.json | 2 +- packages/types/package.json | 2 +- packages/worker/package.json | 8 ++++---- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lerna.json b/lerna.json index 0a3d923d6c..ebcd234dac 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 479a54bd94..398dbfcd0f 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.1", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.3.18-alpha.6", + "@budibase/types": "2.3.18-alpha.7", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 6cfeb44a7b..47778ca6e7 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.3.18-alpha.6", + "@budibase/string-templates": "2.3.18-alpha.7", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 77b09fdbf3..3f4b4fb274 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.6", - "@budibase/client": "2.3.18-alpha.6", - "@budibase/frontend-core": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", + "@budibase/bbui": "2.3.18-alpha.7", + "@budibase/client": "2.3.18-alpha.7", + "@budibase/frontend-core": "2.3.18-alpha.7", + "@budibase/string-templates": "2.3.18-alpha.7", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 49f7a6ab6b..54ccf350a5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", - "@budibase/types": "2.3.18-alpha.6", + "@budibase/backend-core": "2.3.18-alpha.7", + "@budibase/string-templates": "2.3.18-alpha.7", + "@budibase/types": "2.3.18-alpha.7", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index b848ae691e..685afed15b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.3.18-alpha.6", - "@budibase/frontend-core": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", + "@budibase/bbui": "2.3.18-alpha.7", + "@budibase/frontend-core": "2.3.18-alpha.7", + "@budibase/string-templates": "2.3.18-alpha.7", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 8328865966..52d51f9c3e 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.3.18-alpha.6", + "@budibase/bbui": "2.3.18-alpha.7", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a44a61274d..0deea4e791 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index c183a808b8..2db558f3d0 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.3.18-alpha.6", - "@budibase/client": "2.3.18-alpha.6", + "@budibase/backend-core": "2.3.18-alpha.7", + "@budibase/client": "2.3.18-alpha.7", "@budibase/pro": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", - "@budibase/types": "2.3.18-alpha.6", + "@budibase/string-templates": "2.3.18-alpha.7", + "@budibase/types": "2.3.18-alpha.7", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 82b97419ef..c11f5ce7f4 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index f1a9adfa2f..052ba91c3a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index 2854f35441..15085f7718 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.3.18-alpha.6", + "version": "2.3.18-alpha.7", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.3.18-alpha.6", + "@budibase/backend-core": "2.3.18-alpha.7", "@budibase/pro": "2.3.18-alpha.6", - "@budibase/string-templates": "2.3.18-alpha.6", - "@budibase/types": "2.3.18-alpha.6", + "@budibase/string-templates": "2.3.18-alpha.7", + "@budibase/types": "2.3.18-alpha.7", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", From 351ea232f7923a0546e6b1fbaadd49dcac99a493 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 23 Feb 2023 12:17:13 +0000 Subject: [PATCH 50/53] Update pro version to 2.3.18-alpha.7 --- packages/server/package.json | 2 +- packages/server/yarn.lock | 30 +++++++++++++++--------------- packages/worker/package.json | 2 +- packages/worker/yarn.lock | 30 +++++++++++++++--------------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index 2db558f3d0..07b9187cd5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -45,7 +45,7 @@ "@apidevtools/swagger-parser": "10.0.3", "@budibase/backend-core": "2.3.18-alpha.7", "@budibase/client": "2.3.18-alpha.7", - "@budibase/pro": "2.3.18-alpha.6", + "@budibase/pro": "2.3.18-alpha.7", "@budibase/string-templates": "2.3.18-alpha.7", "@budibase/types": "2.3.18-alpha.7", "@bull-board/api": "3.7.0", diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 04d1b7759f..0d838af3a2 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.6.tgz#e6b3304e96b9469f3ca0f4fcfda8cf234c37e2d7" - integrity sha512-To0kFbB9nZ6p0UO4ScS4PJ0gbqI1PrMWRXJLTv/6GU3PxnsqvH1tbpcleLMz2zeE04e5xdwt6W1oPViELom2gg== +"@budibase/backend-core@2.3.18-alpha.7": + version "2.3.18-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.7.tgz#3407df3d6744b5d1e9fe03e1557102fe4fb0f914" + integrity sha512-satGbpr+giJQFUmgjNEzHvqXxEHa3M89y/w2nKfYboPrjq4odHrnQI1N/fX/tcjmWIUwU0/dfgVm4EJuCvWCRw== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.6" + "@budibase/types" "2.3.18-alpha.7" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1392,13 +1392,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.6.tgz#7c5c221da7da79af79605a00aacac5c60f209bf6" - integrity sha512-YWPxmZn+z3tm5GZ+2UZSkOAhamlue/dmki+FCML5pIp3dCw8KsXnpzYXHRc1F8yXMTmA/8KBb/YkjQ2WK3Rk7A== +"@budibase/pro@2.3.18-alpha.7": + version "2.3.18-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.7.tgz#c0746d624665a81b6c1ea55ff95789e67fe4aa81" + integrity sha512-h3bMjoLkpl6n8xtkEHuaLu/SVN4QmzetyAug6iKC5tIGsz59XMrevMLD4xvsoa/2lr8Qp2WZSyRpk47cG+fflg== dependencies: - "@budibase/backend-core" "2.3.18-alpha.6" - "@budibase/types" "2.3.18-alpha.6" + "@budibase/backend-core" "2.3.18-alpha.7" + "@budibase/types" "2.3.18-alpha.7" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1424,10 +1424,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.6.tgz#9438ee64008668bbcb3d688b189cc649e03dfd60" - integrity sha512-16YtXwSODS8UDhdxCP2piGDWELP05EZuPbwLsOUFLX3Gt0+Wwkme+XWw4pTPE+GoK/mTVkDxzSc4cvuXWtfxxA== +"@budibase/types@2.3.18-alpha.7": + version "2.3.18-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.7.tgz#f98414de18db9948938f74322ca26ef4af6c3100" + integrity sha512-b5vqHFPs//PymAK1AFX+CEFts3F4YdCo/gsgo1wkCJfVpd2/EBpe6KD/PHiabS1XbA3DYvlDWL/lTrYbdnpyyw== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/worker/package.json b/packages/worker/package.json index 15085f7718..b4c4595d71 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -37,7 +37,7 @@ "license": "GPL-3.0", "dependencies": { "@budibase/backend-core": "2.3.18-alpha.7", - "@budibase/pro": "2.3.18-alpha.6", + "@budibase/pro": "2.3.18-alpha.7", "@budibase/string-templates": "2.3.18-alpha.7", "@budibase/types": "2.3.18-alpha.7", "@koa/router": "8.0.8", diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 7d296fe01b..6faf55532f 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.6.tgz#e6b3304e96b9469f3ca0f4fcfda8cf234c37e2d7" - integrity sha512-To0kFbB9nZ6p0UO4ScS4PJ0gbqI1PrMWRXJLTv/6GU3PxnsqvH1tbpcleLMz2zeE04e5xdwt6W1oPViELom2gg== +"@budibase/backend-core@2.3.18-alpha.7": + version "2.3.18-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.3.18-alpha.7.tgz#3407df3d6744b5d1e9fe03e1557102fe4fb0f914" + integrity sha512-satGbpr+giJQFUmgjNEzHvqXxEHa3M89y/w2nKfYboPrjq4odHrnQI1N/fX/tcjmWIUwU0/dfgVm4EJuCvWCRw== dependencies: "@budibase/nano" "10.1.1" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.3.18-alpha.6" + "@budibase/types" "2.3.18-alpha.7" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -539,13 +539,13 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.6.tgz#7c5c221da7da79af79605a00aacac5c60f209bf6" - integrity sha512-YWPxmZn+z3tm5GZ+2UZSkOAhamlue/dmki+FCML5pIp3dCw8KsXnpzYXHRc1F8yXMTmA/8KBb/YkjQ2WK3Rk7A== +"@budibase/pro@2.3.18-alpha.7": + version "2.3.18-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.3.18-alpha.7.tgz#c0746d624665a81b6c1ea55ff95789e67fe4aa81" + integrity sha512-h3bMjoLkpl6n8xtkEHuaLu/SVN4QmzetyAug6iKC5tIGsz59XMrevMLD4xvsoa/2lr8Qp2WZSyRpk47cG+fflg== dependencies: - "@budibase/backend-core" "2.3.18-alpha.6" - "@budibase/types" "2.3.18-alpha.6" + "@budibase/backend-core" "2.3.18-alpha.7" + "@budibase/types" "2.3.18-alpha.7" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -553,10 +553,10 @@ lru-cache "^7.14.1" node-fetch "^2.6.1" -"@budibase/types@2.3.18-alpha.6": - version "2.3.18-alpha.6" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.6.tgz#9438ee64008668bbcb3d688b189cc649e03dfd60" - integrity sha512-16YtXwSODS8UDhdxCP2piGDWELP05EZuPbwLsOUFLX3Gt0+Wwkme+XWw4pTPE+GoK/mTVkDxzSc4cvuXWtfxxA== +"@budibase/types@2.3.18-alpha.7": + version "2.3.18-alpha.7" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.3.18-alpha.7.tgz#f98414de18db9948938f74322ca26ef4af6c3100" + integrity sha512-b5vqHFPs//PymAK1AFX+CEFts3F4YdCo/gsgo1wkCJfVpd2/EBpe6KD/PHiabS1XbA3DYvlDWL/lTrYbdnpyyw== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" From 8cd7ba1fdfccec709c7c35a5e392ec1f1c311c3a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 23 Feb 2023 13:55:18 +0000 Subject: [PATCH 51/53] Undo/Redo for Design and Automate sections + automations refactor (#9714) * Add full undo/redo support for screens * Add loading states to disable spamming undo/redo * Add keyboard shortcuts for undo and redo * Fix modals not closing in design section when escape is pressed * Remove log * Add smart metadata saving to undo/redo * Add error handling to undo/redo * Add active state to hoverable icons * Fix screen deletion * Always attempt to get latest doc version before deleting in case rev has changed * Move undo listener top level, hide controls when on certain tabs, and improve selection state * Add tooltips to undo/redo control * Update automation section nav to match other sections * Fix automation list padding * Fix some styles in create automation modal * Improve automation section styles and add undo/redo * Update styles in add action modal * Fix button size when creating admin user * Fix styles in add automation step modal * Fix issue selecting disabled automation steps * Reset automation history store when changing app * Reduce spammy unnecessary API calls when editing cron trigger * WIP automation refactor * Rewrite most automation state * Rewrite most of the rest of automation state * Finish refactor of automation state * Fix selection state when selecting new doc after history recreates it * Prune nullish or empty block inputs from automations and avoid sending API requests when no changes have been made * Fix animation issues with automations * Sort automations and refetch list when adding or deleting * Fix formatting * Add back in ability to swap between values and bindings for block inputs * Lint * Format * Fix potential issue in design section when selected screen is unset * Fix automation arrow directions everywhere, tidy up logic and fix crash when using invalid looping * Lint * Fix more cases of automation errors * Fix implicity any TS error * Respect _id specified when creating automations * Fix crash in history store when reverting a change on a doc whose ID has changed * Lint * Ensure cloneDeep helper doesn't crash when a nullish value is passed in * Remove deprecated frontend automation test --------- Co-authored-by: Rory Powell --- packages/bbui/src/Icon/Icon.svelte | 3 + .../bbui/src/InlineAlert/InlineAlert.svelte | 2 + packages/bbui/src/Label/Label.svelte | 2 +- packages/bbui/src/Modal/Modal.svelte | 11 +- packages/bbui/src/helpers.js | 3 + packages/builder/package.json | 1 + packages/builder/src/builderStore/index.js | 45 +++ .../store/automation/Automation.js | 69 ---- .../builderStore/store/automation/index.js | 212 +++++++----- .../store/automation/tests/Automation.spec.js | 48 --- .../store/automation/tests/testAutomation.js | 78 ----- .../src/builderStore/store/frontend.js | 17 +- .../builder/src/builderStore/store/history.js | 319 ++++++++++++++++++ .../AutomationBuilder.svelte | 10 +- .../FlowChart/ActionModal.svelte | 93 ++--- .../FlowChart/FlowChart.svelte | 88 ++--- .../FlowChart/FlowItem.svelte | 139 +++----- .../FlowChart/FlowItemHeader.svelte | 23 +- .../FlowChart/TestDataModal.svelte | 23 +- .../AutomationBuilder/TestDisplay.svelte | 13 +- .../AutomationBuilder/TestPanel.svelte | 20 +- .../AutomationPanel/AutomationList.svelte | 13 +- .../AutomationPanel/AutomationPanel.svelte | 38 +-- .../CreateAutomationModal.svelte | 38 +-- .../EditAutomationPopover.svelte | 3 - .../UpdateAutomationModal.svelte | 6 +- .../SetupPanel/AutomationBlockSetup.svelte | 62 ++-- .../automation/SetupPanel/CronBuilder.svelte | 11 +- .../Shared/CreateWebhookModal.svelte | 5 +- .../components/common/UndoRedoControl.svelte | 57 ++++ .../ButtonActionEditor.svelte | 21 +- .../src/pages/builder/admin/index.svelte | 1 + .../_layout.svelte | 0 .../index.svelte | 0 .../app/[application]/automate/_layout.svelte | 50 ++- .../app/[application]/automate/index.svelte | 15 +- .../[screenId]/_components/AppPanel.svelte | 10 +- .../navigation/ComponentKeyHandler.svelte | 59 ++-- .../_components/ScreenSettingsPanel.svelte | 10 +- .../design/[screenId]/screens/index.svelte | 8 +- packages/builder/yarn.lock | 5 + .../server/src/api/controllers/automation.ts | 15 +- packages/server/src/api/routes/automation.ts | 2 +- 43 files changed, 926 insertions(+), 722 deletions(-) delete mode 100644 packages/builder/src/builderStore/store/automation/Automation.js delete mode 100644 packages/builder/src/builderStore/store/automation/tests/Automation.spec.js delete mode 100644 packages/builder/src/builderStore/store/automation/tests/testAutomation.js create mode 100644 packages/builder/src/builderStore/store/history.js create mode 100644 packages/builder/src/components/common/UndoRedoControl.svelte rename packages/builder/src/pages/builder/app/[application]/automate/{[automation] => [automationId]}/_layout.svelte (100%) rename packages/builder/src/pages/builder/app/[application]/automate/{[automation] => [automationId]}/index.svelte (100%) diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index 8290acd7cc..452a8c74a1 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -67,6 +67,9 @@ color: var(--spectrum-alias-icon-color-selected-hover) !important; cursor: pointer; } + svg.hoverable:active { + color: var(--spectrum-global-color-blue-400) !important; + } svg.disabled { color: var(--spectrum-global-color-gray-500) !important; diff --git a/packages/bbui/src/InlineAlert/InlineAlert.svelte b/packages/bbui/src/InlineAlert/InlineAlert.svelte index 57e7296234..bd873042b3 100644 --- a/packages/bbui/src/InlineAlert/InlineAlert.svelte +++ b/packages/bbui/src/InlineAlert/InlineAlert.svelte @@ -57,5 +57,7 @@ --spectrum-semantic-negative-icon-color: #e34850; min-width: 100px; margin: 0; + border-color: var(--spectrum-global-color-gray-400); + border-width: 1px; } diff --git a/packages/bbui/src/Label/Label.svelte b/packages/bbui/src/Label/Label.svelte index ee6d9adf76..261ca946ea 100644 --- a/packages/bbui/src/Label/Label.svelte +++ b/packages/bbui/src/Label/Label.svelte @@ -21,7 +21,7 @@ label { padding: 0; white-space: nowrap; - color: var(--spectrum-global-color-gray-600); + color: var(--spectrum-global-color-gray-700); } .muted { diff --git a/packages/bbui/src/Modal/Modal.svelte b/packages/bbui/src/Modal/Modal.svelte index 47420444a2..45081356c1 100644 --- a/packages/bbui/src/Modal/Modal.svelte +++ b/packages/bbui/src/Modal/Modal.svelte @@ -1,7 +1,7 @@ - + onMount(() => { + document.addEventListener("keydown", handleKey) + return () => { + document.removeEventListener("keydown", handleKey) + } + }) + {#if inline} {#if visible} diff --git a/packages/bbui/src/helpers.js b/packages/bbui/src/helpers.js index b02783e0bd..f2246fbb49 100644 --- a/packages/bbui/src/helpers.js +++ b/packages/bbui/src/helpers.js @@ -104,6 +104,9 @@ export const deepSet = (obj, key, value) => { * @param obj the object to clone */ export const cloneDeep = obj => { + if (!obj) { + return obj + } return JSON.parse(JSON.stringify(obj)) } diff --git a/packages/builder/package.json b/packages/builder/package.json index 3f4b4fb274..b87cd3924b 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -72,6 +72,7 @@ "codemirror": "^5.59.0", "dayjs": "^1.11.2", "downloadjs": "1.4.7", + "fast-json-patch": "^3.1.1", "lodash": "4.17.21", "posthog-js": "^1.36.0", "remixicon": "2.5.0", diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 69bca7eac3..d15cdb6e98 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -5,12 +5,47 @@ import { getThemeStore } from "./store/theme" import { derived } from "svelte/store" import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" +import { createHistoryStore } from "builderStore/store/history" +import { get } from "svelte/store" export const store = getFrontendStore() export const automationStore = getAutomationStore() export const themeStore = getThemeStore() export const temporalStore = getTemporalStore() +// Setup history for screens +export const screenHistoryStore = createHistoryStore({ + getDoc: id => get(store).screens?.find(screen => screen._id === id), + selectDoc: store.actions.screens.select, + afterAction: () => { + // Ensure a valid component is selected + if (!get(selectedComponent)) { + store.update(state => ({ + ...state, + selectedComponentId: get(selectedScreen)?.props._id, + })) + } + }, +}) +store.actions.screens.save = screenHistoryStore.wrapSaveDoc( + store.actions.screens.save +) +store.actions.screens.delete = screenHistoryStore.wrapDeleteDoc( + store.actions.screens.delete +) + +// Setup history for automations +export const automationHistoryStore = createHistoryStore({ + getDoc: automationStore.actions.getDefinition, + selectDoc: automationStore.actions.select, +}) +automationStore.actions.save = automationHistoryStore.wrapSaveDoc( + automationStore.actions.save +) +automationStore.actions.delete = automationHistoryStore.wrapDeleteDoc( + automationStore.actions.delete +) + export const selectedScreen = derived(store, $store => { return $store.screens.find(screen => screen._id === $store.selectedScreenId) }) @@ -71,3 +106,13 @@ export const selectedComponentPath = derived( ).map(component => component._id) } ) + +// Derived automation state +export const selectedAutomation = derived(automationStore, $automationStore => { + if (!$automationStore.selectedAutomationId) { + return null + } + return $automationStore.automations?.find( + x => x._id === $automationStore.selectedAutomationId + ) +}) diff --git a/packages/builder/src/builderStore/store/automation/Automation.js b/packages/builder/src/builderStore/store/automation/Automation.js deleted file mode 100644 index af0c03cb5a..0000000000 --- a/packages/builder/src/builderStore/store/automation/Automation.js +++ /dev/null @@ -1,69 +0,0 @@ -import { generate } from "shortid" - -/** - * Class responsible for the traversing of the automation definition. - * Automation definitions are stored in linked lists. - */ -export default class Automation { - constructor(automation) { - this.automation = automation - } - - hasTrigger() { - return this.automation.definition.trigger - } - - addTestData(data) { - this.automation.testData = { ...this.automation.testData, ...data } - } - - addBlock(block, idx) { - // Make sure to add trigger if doesn't exist - if (!this.hasTrigger() && block.type === "TRIGGER") { - const trigger = { id: generate(), ...block } - this.automation.definition.trigger = trigger - return trigger - } - - const newBlock = { id: generate(), ...block } - this.automation.definition.steps.splice(idx, 0, newBlock) - return newBlock - } - - updateBlock(updatedBlock, id) { - const { steps, trigger } = this.automation.definition - - if (trigger && trigger.id === id) { - this.automation.definition.trigger = updatedBlock - return - } - - const stepIdx = steps.findIndex(step => step.id === id) - if (stepIdx < 0) throw new Error("Block not found.") - steps.splice(stepIdx, 1, updatedBlock) - this.automation.definition.steps = steps - } - - deleteBlock(id) { - const { steps, trigger } = this.automation.definition - - if (trigger && trigger.id === id) { - this.automation.definition.trigger = null - return - } - - const stepIdx = steps.findIndex(step => step.id === id) - if (stepIdx < 0) throw new Error("Block not found.") - steps.splice(stepIdx, 1) - this.automation.definition.steps = steps - } - - constructBlock(type, stepId, blockDefinition) { - return { - ...blockDefinition, - inputs: blockDefinition.inputs || {}, - stepId, - type, - } - } -} diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index af102ab694..dc1e2a2cc1 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -1,16 +1,18 @@ -import { writable } from "svelte/store" +import { writable, get } from "svelte/store" import { API } from "api" -import Automation from "./Automation" import { cloneDeep } from "lodash/fp" +import { generate } from "shortid" +import { selectedAutomation } from "builderStore" const initialAutomationState = { automations: [], + testResults: null, showTestPanel: false, blockDefinitions: { TRIGGER: [], ACTION: [], }, - selectedAutomation: null, + selectedAutomationId: null, } export const getAutomationStore = () => { @@ -37,49 +39,41 @@ const automationActions = store => ({ API.getAutomationDefinitions(), ]) store.update(state => { - let selected = state.selectedAutomation?.automation state.automations = responses[0] + state.automations.sort((a, b) => { + return a.name < b.name ? -1 : 1 + }) state.blockDefinitions = { TRIGGER: responses[1].trigger, ACTION: responses[1].action, } - // If previously selected find the new obj and select it - if (selected) { - selected = responses[0].filter( - automation => automation._id === selected._id - ) - state.selectedAutomation = new Automation(selected[0]) - } return state }) }, - create: async ({ name }) => { + create: async (name, trigger) => { const automation = { name, type: "automation", definition: { steps: [], + trigger, }, } - const response = await API.createAutomation(automation) - store.update(state => { - state.automations = [...state.automations, response.automation] - store.actions.select(response.automation) - return state - }) + const response = await store.actions.save(automation) + await store.actions.fetch() + store.actions.select(response._id) + return response }, duplicate: async automation => { - const response = await API.createAutomation({ + const response = await store.actions.save({ ...automation, name: `${automation.name} - copy`, _id: undefined, _ref: undefined, }) - store.update(state => { - state.automations = [...state.automations, response.automation] - store.actions.select(response.automation) - return state - }) + await store.actions.fetch() + store.actions.select(response._id) + return response }, save: async automation => { const response = await API.updateAutomation(automation) @@ -90,11 +84,13 @@ const automationActions = store => ({ ) if (existingIdx !== -1) { state.automations.splice(existingIdx, 1, updatedAutomation) - state.automations = [...state.automations] - store.actions.select(updatedAutomation) return state + } else { + state.automations = [...state.automations, updatedAutomation] } + return state }) + return response.automation }, delete: async automation => { await API.deleteAutomation({ @@ -102,34 +98,83 @@ const automationActions = store => ({ automationRev: automation?._rev, }) store.update(state => { - const existingIdx = state.automations.findIndex( - existing => existing._id === automation?._id + // Remove the automation + state.automations = state.automations.filter( + x => x._id !== automation._id ) - state.automations.splice(existingIdx, 1) - state.automations = [...state.automations] - state.selectedAutomation = null - state.selectedBlock = null + // Select a new automation if required + if (automation._id === state.selectedAutomationId) { + store.actions.select(state.automations[0]?._id) + } return state }) + await store.actions.fetch() + }, + updateBlockInputs: async (block, data) => { + // Create new modified block + let newBlock = { + ...block, + inputs: { + ...block.inputs, + ...data, + }, + } + + // Remove any nullish or empty string values + Object.keys(newBlock.inputs).forEach(key => { + const val = newBlock.inputs[key] + if (val == null || val === "") { + delete newBlock.inputs[key] + } + }) + + // Create new modified automation + const automation = get(selectedAutomation) + const newAutomation = store.actions.getUpdatedDefinition( + automation, + newBlock + ) + + // Don't save if no changes were made + if (JSON.stringify(newAutomation) === JSON.stringify(automation)) { + return + } + await store.actions.save(newAutomation) }, test: async (automation, testData) => { - store.update(state => { - state.selectedAutomation.testResults = null - return state - }) const result = await API.testAutomation({ automationId: automation?._id, testData, }) + if (!result?.trigger && !result?.steps?.length) { + throw "Something went wrong testing your automation" + } store.update(state => { - state.selectedAutomation.testResults = result + state.testResults = result return state }) }, - select: automation => { + getDefinition: id => { + return get(store).automations?.find(x => x._id === id) + }, + getUpdatedDefinition: (automation, block) => { + let newAutomation = cloneDeep(automation) + if (automation.definition.trigger?.id === block.id) { + newAutomation.definition.trigger = block + } else { + const idx = automation.definition.steps.findIndex(x => x.id === block.id) + newAutomation.definition.steps.splice(idx, 1, block) + } + return newAutomation + }, + select: id => { + if (!id || id === get(store).selectedAutomationId) { + return + } store.update(state => { - state.selectedAutomation = new Automation(cloneDeep(automation)) - state.selectedBlock = null + state.selectedAutomationId = id + state.testResults = null + state.showTestPanel = false return state }) }, @@ -147,48 +192,57 @@ const automationActions = store => ({ appId, }) }, - addTestDataToAutomation: data => { - store.update(state => { - state.selectedAutomation.addTestData(data) - return state - }) + addTestDataToAutomation: async data => { + let newAutomation = cloneDeep(get(selectedAutomation)) + newAutomation.testData = { + ...newAutomation.testData, + ...data, + } + await store.actions.save(newAutomation) }, - addBlockToAutomation: (block, blockIdx) => { - store.update(state => { - state.selectedBlock = state.selectedAutomation.addBlock( - cloneDeep(block), - blockIdx - ) - return state - }) + constructBlock(type, stepId, blockDefinition) { + return { + ...blockDefinition, + inputs: blockDefinition.inputs || {}, + stepId, + type, + id: generate(), + } }, - toggleFieldControl: value => { - store.update(state => { - state.selectedBlock.rowControl = value - return state - }) + addBlockToAutomation: async (block, blockIdx) => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) + if (!automation) { + return + } + newAutomation.definition.steps.splice(blockIdx, 0, block) + await store.actions.save(newAutomation) }, - deleteAutomationBlock: block => { - store.update(state => { - const idx = - state.selectedAutomation.automation.definition.steps.findIndex( - x => x.id === block.id - ) - state.selectedAutomation.deleteBlock(block.id) + /** + * "rowControl" appears to be the name of the flag used to determine whether + * a certain automation block uses values or bindings as inputs + */ + toggleRowControl: async (block, rowControl) => { + const newBlock = { ...block, rowControl } + const newAutomation = store.actions.getUpdatedDefinition( + get(selectedAutomation), + newBlock + ) + await store.actions.save(newAutomation) + }, + deleteAutomationBlock: async block => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) - // Select next closest step - const steps = state.selectedAutomation.automation.definition.steps - let nextSelectedBlock - if (steps[idx] != null) { - nextSelectedBlock = steps[idx] - } else if (steps[idx - 1] != null) { - nextSelectedBlock = steps[idx - 1] - } else { - nextSelectedBlock = - state.selectedAutomation.automation.definition.trigger || null - } - state.selectedBlock = nextSelectedBlock - return state - }) + // Delete trigger if required + if (newAutomation.definition.trigger?.id === block.id) { + delete newAutomation.definition.trigger + } else { + // Otherwise remove step + newAutomation.definition.steps = newAutomation.definition.steps.filter( + step => step.id !== block.id + ) + } + await store.actions.save(newAutomation) }, }) diff --git a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js b/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js deleted file mode 100644 index 8378310c2e..0000000000 --- a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import Automation from "../Automation" -import TEST_AUTOMATION from "./testAutomation" - -const TEST_BLOCK = { - id: "AUXJQGZY7", - name: "Delay", - icon: "ri-time-fill", - tagline: "Delay for {{time}} milliseconds", - description: "Delay the automation until an amount of time has passed.", - params: { time: "number" }, - type: "LOGIC", - args: { time: "5000" }, - stepId: "DELAY", -} - -describe("Automation Data Object", () => { - let automation - - beforeEach(() => { - automation = new Automation({ ...TEST_AUTOMATION }) - }) - - it("adds a automation block to the automation", () => { - automation.addBlock(TEST_BLOCK) - expect(automation.automation.definition) - }) - - it("updates a automation block with new attributes", () => { - const firstBlock = automation.automation.definition.steps[0] - const updatedBlock = { - ...firstBlock, - name: "UPDATED", - } - automation.updateBlock(updatedBlock, firstBlock.id) - expect(automation.automation.definition.steps[0]).toEqual(updatedBlock) - }) - - it("deletes a automation block successfully", () => { - const { steps } = automation.automation.definition - const originalLength = steps.length - - const lastBlock = steps[steps.length - 1] - automation.deleteBlock(lastBlock.id) - expect(automation.automation.definition.steps.length).toBeLessThan( - originalLength - ) - }) -}) diff --git a/packages/builder/src/builderStore/store/automation/tests/testAutomation.js b/packages/builder/src/builderStore/store/automation/tests/testAutomation.js deleted file mode 100644 index 3fafbaf1d0..0000000000 --- a/packages/builder/src/builderStore/store/automation/tests/testAutomation.js +++ /dev/null @@ -1,78 +0,0 @@ -export default { - name: "Test automation", - definition: { - steps: [ - { - id: "ANBDINAPS", - description: "Send an email.", - tagline: "Send email to {{to}}", - icon: "ri-mail-open-fill", - name: "Send Email", - params: { - to: "string", - from: "string", - subject: "longText", - text: "longText", - }, - type: "ACTION", - args: { - text: "A user was created!", - subject: "New Budibase User", - from: "budimaster@budibase.com", - to: "test@test.com", - }, - stepId: "SEND_EMAIL", - }, - ], - trigger: { - id: "iRzYMOqND", - name: "Row Saved", - event: "row:save", - icon: "ri-save-line", - tagline: "Row is added to {{table.name}}", - description: "Fired when a row is saved to your database.", - params: { table: "table" }, - type: "TRIGGER", - args: { - table: { - type: "table", - views: {}, - name: "users", - schema: { - name: { - type: "string", - constraints: { - type: "string", - length: { maximum: 123 }, - presence: { allowEmpty: false }, - }, - name: "name", - }, - age: { - type: "number", - constraints: { - type: "number", - presence: { allowEmpty: false }, - numericality: { - greaterThanOrEqualTo: "", - lessThanOrEqualTo: "", - }, - }, - name: "age", - }, - }, - _id: "c6b4e610cd984b588837bca27188a451", - _rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff", - }, - }, - stepId: "ROW_SAVED", - }, - }, - type: "automation", - ok: true, - id: "b384f861f4754e1693835324a7fcca62", - rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37", - live: false, - _id: "b384f861f4754e1693835324a7fcca62", - _rev: "108-4116829ec375e0481d0ecab9e83a2caf", -} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 56b8a599f0..d58a2d5b9e 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -1,6 +1,11 @@ import { get, writable } from "svelte/store" import { cloneDeep } from "lodash/fp" -import { selectedScreen, selectedComponent } from "builderStore" +import { + selectedScreen, + selectedComponent, + screenHistoryStore, + automationHistoryStore, +} from "builderStore" import { datasources, integrations, @@ -122,6 +127,8 @@ export const getFrontendStore = () => { navigation: application.navigation || {}, usedPlugins: application.usedPlugins || [], })) + screenHistoryStore.reset() + automationHistoryStore.reset() // Initialise backend stores database.set(application.instance) @@ -179,10 +186,7 @@ export const getFrontendStore = () => { } // Check screen isn't already selected - if ( - state.selectedScreenId === screen._id && - state.selectedComponentId === screen.props?._id - ) { + if (state.selectedScreenId === screen._id) { return } @@ -256,7 +260,7 @@ export const getFrontendStore = () => { } }, save: async screen => { - /* + /* Temporarily disabled to accomodate migration issues. store.actions.screens.validate(screen) */ @@ -347,6 +351,7 @@ export const getFrontendStore = () => { return state }) + return null }, updateSetting: async (screen, name, value) => { if (!screen || !name) { diff --git a/packages/builder/src/builderStore/store/history.js b/packages/builder/src/builderStore/store/history.js new file mode 100644 index 0000000000..0f21085c6a --- /dev/null +++ b/packages/builder/src/builderStore/store/history.js @@ -0,0 +1,319 @@ +import * as jsonpatch from "fast-json-patch/index.mjs" +import { writable, derived, get } from "svelte/store" + +const Operations = { + Add: "Add", + Delete: "Delete", + Change: "Change", +} + +const initialState = { + history: [], + position: 0, + loading: false, +} + +export const createHistoryStore = ({ + getDoc, + selectDoc, + beforeAction, + afterAction, +}) => { + // Use a derived store to check if we are able to undo or redo any operations + const store = writable(initialState) + const derivedStore = derived(store, $store => { + return { + ...$store, + canUndo: $store.position > 0, + canRedo: $store.position < $store.history.length, + } + }) + + // Wrapped versions of essential functions which we call ourselves when using + // undo and redo + let saveFn + let deleteFn + + /** + * Internal util to set the loading flag + */ + const startLoading = () => { + store.update(state => { + state.loading = true + return state + }) + } + + /** + * Internal util to unset the loading flag + */ + const stopLoading = () => { + store.update(state => { + state.loading = false + return state + }) + } + + /** + * Resets history state + */ + const reset = () => { + store.set(initialState) + } + + /** + * Adds or updates an operation in history. + * For internal use only. + * @param operation the operation to save + */ + const saveOperation = operation => { + store.update(state => { + // Update history + let history = state.history + let position = state.position + if (!operation.id) { + // Every time a new operation occurs we discard any redo potential + operation.id = Math.random() + history = [...history.slice(0, state.position), operation] + position += 1 + } else { + // If this is a redo/undo of an existing operation, just update history + // to replace the doc object as revisions may have changed + const idx = history.findIndex(op => op.id === operation.id) + history[idx].doc = operation.doc + } + return { history, position } + }) + } + + /** + * Wraps the save function, which asynchronously updates a doc. + * The returned function is an enriched version of the real save function so + * that we can control history. + * @param fn the save function + * @returns {function} a wrapped version of the save function + */ + const wrapSaveDoc = fn => { + saveFn = async (doc, operationId) => { + // Only works on a single doc at a time + if (!doc || Array.isArray(doc)) { + return + } + startLoading() + try { + const oldDoc = getDoc(doc._id) + const newDoc = jsonpatch.deepClone(await fn(doc)) + + // Store the change + if (!oldDoc) { + // If no old doc, this is an add operation + saveOperation({ + type: Operations.Add, + doc: newDoc, + id: operationId, + }) + } else { + // Otherwise this is a change operation + saveOperation({ + type: Operations.Change, + forwardPatch: jsonpatch.compare(oldDoc, doc), + backwardsPatch: jsonpatch.compare(doc, oldDoc), + doc: newDoc, + id: operationId, + }) + } + stopLoading() + return newDoc + } catch (error) { + // We want to allow errors to propagate up to normal handlers, but we + // want to stop loading first + stopLoading() + throw error + } + } + return saveFn + } + + /** + * Wraps the delete function, which asynchronously deletes a doc. + * The returned function is an enriched version of the real delete function so + * that we can control history. + * @param fn the delete function + * @returns {function} a wrapped version of the delete function + */ + const wrapDeleteDoc = fn => { + deleteFn = async (doc, operationId) => { + // Only works on a single doc at a time + if (!doc || Array.isArray(doc)) { + return + } + startLoading() + try { + const oldDoc = jsonpatch.deepClone(doc) + await fn(doc) + saveOperation({ + type: Operations.Delete, + doc: oldDoc, + id: operationId, + }) + stopLoading() + } catch (error) { + // We want to allow errors to propagate up to normal handlers, but we + // want to stop loading first + stopLoading() + throw error + } + } + return deleteFn + } + + /** + * Asynchronously undoes the previous operation. + * Optionally selects the changed document so that changes are visible. + * @returns {Promise} + */ + const undo = async () => { + // Sanity checks + const { canUndo, history, position, loading } = get(derivedStore) + if (!canUndo || loading) { + return + } + const operation = history[position - 1] + if (!operation) { + return + } + startLoading() + + // Before hook + await beforeAction?.(operation) + + // Update state immediately to prevent further clicks and to prevent bad + // history in the event of an update failing + store.update(state => { + return { + ...state, + position: state.position - 1, + } + }) + + // Undo the operation + try { + // Undo ADD + if (operation.type === Operations.Add) { + // Try to get the latest doc version to delete + const latestDoc = getDoc(operation.doc._id) + const doc = latestDoc || operation.doc + await deleteFn(doc, operation.id) + } + + // Undo DELETE + else if (operation.type === Operations.Delete) { + // Delete the _rev from the deleted doc so that we can save it as a new + // doc again without conflicts + let doc = jsonpatch.deepClone(operation.doc) + delete doc._rev + const created = await saveFn(doc, operation.id) + selectDoc?.(created?._id || doc._id) + } + + // Undo CHANGE + else { + // Get the current doc and apply the backwards patch on top of it + let doc = jsonpatch.deepClone(getDoc(operation.doc._id)) + if (doc) { + jsonpatch.applyPatch( + doc, + jsonpatch.deepClone(operation.backwardsPatch) + ) + await saveFn(doc, operation.id) + selectDoc?.(doc._id) + } + } + stopLoading() + } catch (error) { + stopLoading() + throw error + } + + // After hook + await afterAction?.(operation) + } + + /** + * Asynchronously redoes the previous undo. + * Optionally selects the changed document so that changes are visible. + * @returns {Promise} + */ + const redo = async () => { + // Sanity checks + const { canRedo, history, position, loading } = get(derivedStore) + if (!canRedo || loading) { + return + } + const operation = history[position] + if (!operation) { + return + } + startLoading() + + // Before hook + await beforeAction?.(operation) + + // Update state immediately to prevent further clicks and to prevent bad + // history in the event of an update failing + store.update(state => { + return { + ...state, + position: state.position + 1, + } + }) + + // Redo the operation + try { + // Redo ADD + if (operation.type === Operations.Add) { + // Delete the _rev from the deleted doc so that we can save it as a new + // doc again without conflicts + let doc = jsonpatch.deepClone(operation.doc) + delete doc._rev + const created = await saveFn(doc, operation.id) + selectDoc?.(created?._id || doc._id) + } + + // Redo DELETE + else if (operation.type === Operations.Delete) { + // Try to get the latest doc version to delete + const latestDoc = getDoc(operation.doc._id) + const doc = latestDoc || operation.doc + await deleteFn(doc, operation.id) + } + + // Redo CHANGE + else { + // Get the current doc and apply the forwards patch on top of it + let doc = jsonpatch.deepClone(getDoc(operation.doc._id)) + if (doc) { + jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch)) + await saveFn(doc, operation.id) + selectDoc?.(doc._id) + } + } + stopLoading() + } catch (error) { + stopLoading() + throw error + } + + // After hook + await afterAction?.(operation) + } + + return { + subscribe: derivedStore.subscribe, + wrapSaveDoc, + wrapDeleteDoc, + reset, + undo, + redo, + } +} diff --git a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte index e852ee1a0d..b80ba45086 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte @@ -1,10 +1,10 @@ -{#if automation} - +{#if $selectedAutomation} + {#key $selectedAutomation._id} + + {/key} {/if} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index caf8835b86..f30b49eb39 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -5,7 +5,6 @@ Detail, Body, Icon, - Tooltip, notifications, } from "@budibase/bbui" import { automationStore } from "builderStore" @@ -13,7 +12,6 @@ import { externalActions } from "./ExternalActions" export let blockIdx - export let blockComplete const disabled = { SEND_EMAIL_SMTP: { @@ -50,15 +48,12 @@ async function addBlockToAutomation() { try { - const newBlock = $automationStore.selectedAutomation.constructBlock( + const newBlock = automationStore.actions.constructBlock( "ACTION", actionVal.stepId, actionVal ) - automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1) - await automationStore.actions.save( - $automationStore.selectedAutomation?.automation - ) + await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1) } catch (error) { notifications.error("Error saving automation") } @@ -66,20 +61,14 @@ { - blockComplete = true - addBlockToAutomation() - }} + onConfirm={addBlockToAutomation} > - Select an app or event. - - - Apps - + + Apps

{#each Object.entries(external) as [idx, action]}
- {idx.charAt(0).toUpperCase() + idx.slice(1)} + + {idx.charAt(0).toUpperCase() + idx.slice(1)} + +
{/each}
+ + Actions -
{#each Object.entries(internal) as [idx, action]} - {#if disabled[idx] && disabled[idx].disabled} - -
selectAction(action)} - > -
- - - {action.name} -
-
-
- {:else} -
selectAction(action)} - > -
- - - {action.name} -
+ {@const isDisabled = disabled[idx] && disabled[idx].disabled} +
selectAction(action)} + > +
+ + {action.name} + {#if isDisabled} + + {/if}
- {/if} +
{/each}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 4b01616b54..63a3478ef3 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -1,5 +1,5 @@
-
+
{automation.name} -
-
-
-
-
- -
+
+ + +
{ testDataModal.show() @@ -62,15 +60,13 @@ icon="MultipleCheck" size="M">Run test -
- { - $automationStore.showTestPanel = true - }} - size="M">Test Details -
+ { + $automationStore.showTestPanel = true + }} + size="M">Test Details
@@ -80,7 +76,7 @@
{#if block.stepId !== ActionStepID.LOOP} @@ -105,6 +101,9 @@ diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index d6e5fcb36d..7484a60502 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -1,5 +1,5 @@
{}}> - {#if loopingSelected} + {#if loopBlock}
{ @@ -174,13 +142,8 @@
-
{ - onSelect(block) - }} - > - +
{}}> +
@@ -198,9 +161,7 @@ $automationStore.blockDefinitions.ACTION.LOOP.schema.inputs .properties )} - block={$automationStore.selectedAutomation?.automation.definition.steps.find( - x => x.blockToLoop === block.id - )} + block={loopBlock} {webhookModal} /> @@ -209,22 +170,28 @@ {/if} {/if} - - {#if !blockComplete} + (open = !open)} + /> + {#if open}
{#if !isTrigger}
- {#if !loopingSelected} - addLooping()} icon="Reuse" - >Add Looping + {#if !loopBlock} + addLooping()} icon="Reuse"> + Add Looping + {/if} {#if showBindingPicker}