diff --git a/.eslintignore b/.eslintignore index f2c53c2fdc..94984a446f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,4 +12,5 @@ packages/sdk/sdk packages/account-portal/packages/server/build packages/account-portal/packages/ui/.routify packages/account-portal/packages/ui/build -**/*.ivm.bundle.js \ No newline at end of file +**/*.ivm.bundle.js +packages/server/build/oldClientVersions/**/** diff --git a/.eslintrc.json b/.eslintrc.json index ae9512152f..2a40c6cc29 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -34,18 +34,43 @@ }, { "files": ["**/*.ts"], + "excludedFiles": ["qa-core/**"], "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], "extends": ["eslint:recommended"], + "globals": { + "NodeJS": true + }, "rules": { "no-unused-vars": "off", - "no-inner-declarations": "off", - "no-case-declarations": "off", - "no-useless-escape": "off", - "no-undef": "off", - "no-prototype-builtins": "off", - "local-rules/no-budibase-imports": "error", + "@typescript-eslint/no-unused-vars": "error", + "local-rules/no-budibase-imports": "error" + } + }, + { + "files": ["**/*.spec.ts"], + "excludedFiles": ["qa-core/**"], + "parser": "@typescript-eslint/parser", + "plugins": ["jest", "@typescript-eslint"], + "extends": ["eslint:recommended", "plugin:jest/recommended"], + "env": { + "jest/globals": true + }, + "globals": { + "NodeJS": true + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error", "local-rules/no-test-com": "error", - "local-rules/email-domain-example-com": "error" + "local-rules/email-domain-example-com": "error", + "no-console": "warn", + // We have a lot of tests that don't have assertions, they use our test + // API client that does the assertions for them + "jest/expect-expect": "off", + // We do this in some tests where the behaviour of internal tables + // differs to external, but the API is broadly the same + "jest/no-conditional-expect": "off" } }, { diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 5c474aa826..030ad6578e 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -66,7 +66,8 @@ jobs: # Run build all the projects - name: Build run: | - yarn build + yarn build:oss + yarn build:account-portal # Check the types of the projects built via esbuild - name: Check types run: | @@ -138,6 +139,8 @@ jobs: test-server: runs-on: ubuntu-latest + env: + DEBUG: testcontainers,testcontainers:exec,testcontainers:build,testcontainers:pull steps: - name: Checkout repo uses: actions/checkout@v4 @@ -151,7 +154,19 @@ jobs: with: node-version: 20.x cache: yarn + + - name: Pull testcontainers images + run: | + docker pull mcr.microsoft.com/mssql/server:2022-latest + docker pull mysql:8.3 + docker pull postgres:16.1-bullseye + docker pull mongo:7.0-jammy + docker pull mariadb:lts + docker pull testcontainers/ryuk:0.5.1 + docker pull budibase/couchdb + - run: yarn --frozen-lockfile + - name: Test server run: | if ${{ env.USE_NX_AFFECTED }}; then @@ -217,27 +232,34 @@ jobs: echo "pro_commit=$pro_commit" echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT" echo "base_commit=$base_commit" - echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT" + + base_commit_excluding_merges=$(git log --no-merges -n 1 --format=format:%H $base_commit) + echo "base_commit_excluding_merges=$base_commit_excluding_merges" + echo "base_commit_excluding_merges=$base_commit_excluding_merges" >> "$GITHUB_OUTPUT" else echo "Nothing to do - branch to branch merge." fi - - name: Check submodule merged to base branch - if: ${{ steps.get_pro_commits.outputs.base_commit != '' }} - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const submoduleCommit = '${{ steps.get_pro_commits.outputs.pro_commit }}'; - const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}'; + - name: Check submodule merged and latest on base branch + if: ${{ steps.get_pro_commits.outputs.base_commit_excluding_merges != '' }} + run: | + cd packages/pro + base_commit_excluding_merges='${{ steps.get_pro_commits.outputs.base_commit_excluding_merges }}' + pro_commit='${{ steps.get_pro_commits.outputs.pro_commit }}' - if (submoduleCommit !== baseCommit) { - console.error('Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.'); - console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md') - process.exit(1); - } else { - console.log('All good, the submodule had been merged and setup correctly!') - } + any_commit=$(git log --no-merges $base_commit_excluding_merges...$pro_commit) + + if [ -n "$any_commit" ]; then + echo $any_commit + + echo "An error occurred: " + echo 'Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.' + echo 'Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md' + + exit 1 + else + echo 'All good, the submodule had been merged and setup correctly!' + fi check-accountportal-submodule: runs-on: ubuntu-latest @@ -250,7 +272,15 @@ jobs: token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} fetch-depth: 0 - - name: Check account portal commit + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + src: + - packages/account-portal/** + + - if: steps.changes.outputs.src == 'true' + name: Check account portal commit id: get_accountportal_commits run: | cd packages/account-portal diff --git a/.gitignore b/.gitignore index 3eb705dbbf..661c60e95e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ packages/server/runtime_apps/ bb-airgapped.tar.gz *.iml +packages/server/build/oldClientVersions/**/* +packages/builder/src/components/deploy/clientVersions.json + # Logs logs *.log @@ -107,3 +110,4 @@ budibase-component budibase-datasource *.iml +.nx \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index cfd8d7b155..2fda61345b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,4 +1,3 @@ - { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. @@ -20,6 +19,13 @@ "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], "args": ["${workspaceFolder}/packages/worker/src/index.ts"], "cwd": "${workspaceFolder}/packages/worker" + }, + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:10000", + "webRoot": "${workspaceFolder}" } ], "compounds": [ diff --git a/charts/budibase/README.md b/charts/budibase/README.md index 342011bdb1..dea7d1dbae 100644 --- a/charts/budibase/README.md +++ b/charts/budibase/README.md @@ -140,7 +140,7 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml | ingress.className | string | `""` | What ingress class to use. | | ingress.enabled | bool | `true` | Whether to create an Ingress resource pointing to the Budibase proxy. | | ingress.hosts | list | `[]` | Standard hosts block for the Ingress resource. Defaults to pointing to the Budibase proxy. | -| nameOverride | string | `""` | Override the name of the deploymen. Defaults to {{ .Chart.Name }}. | +| nameOverride | string | `""` | Override the name of the deployment. Defaults to {{ .Chart.Name }}. | | service.port | int | `10000` | Port to expose on the service. | | service.type | string | `"ClusterIP"` | Service type for the service that points to the main Budibase proxy pod. | | serviceAccount.annotations | object | `{}` | Annotations to add to the service account | diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 09262df463..19b6c22d6c 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -1,6 +1,6 @@ # -- Passed to all pods created by this chart. Should not ordinarily need to be changed. imagePullSecrets: [] -# -- Override the name of the deploymen. Defaults to {{ .Chart.Name }}. +# -- Override the name of the deployment. Defaults to {{ .Chart.Name }}. nameOverride: "" serviceAccount: diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js index 202e52e70e..a4866bc1f8 100644 --- a/eslint-local-rules/index.js +++ b/eslint-local-rules/index.js @@ -25,11 +25,9 @@ module.exports = { docs: { description: "disallow the use of 'test.com' in strings and replace it with 'example.com'", - category: "Possible Errors", - recommended: false, }, - schema: [], // no options - fixable: "code", // Indicates that this rule supports automatic fixing + schema: [], + fixable: "code", }, create: function (context) { return { @@ -58,8 +56,6 @@ module.exports = { docs: { description: "enforce using the example.com domain for generator.email calls", - category: "Possible Errors", - recommended: false, }, fixable: "code", schema: [], diff --git a/globalSetup.ts b/globalSetup.ts new file mode 100644 index 0000000000..4cb542a3c3 --- /dev/null +++ b/globalSetup.ts @@ -0,0 +1,25 @@ +import { GenericContainer, Wait } from "testcontainers" + +export default async function setup() { + await new GenericContainer("budibase/couchdb") + .withExposedPorts(5984) + .withEnvironment({ + COUCHDB_PASSWORD: "budibase", + COUCHDB_USER: "budibase", + }) + .withCopyContentToContainer([ + { + content: ` + [log] + level = warn + `, + target: "/opt/couchdb/etc/local.d/test-couchdb.ini", + }, + ]) + .withWaitStrategy( + Wait.forSuccessfulCommand( + "curl http://budibase:budibase@localhost:5984/_up" + ).withStartupTimeout(20000) + ) + .start() +} diff --git a/jestTestcontainersConfigGenerator.js b/jestTestcontainersConfigGenerator.js deleted file mode 100644 index 1e39ed771f..0000000000 --- a/jestTestcontainersConfigGenerator.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = () => { - return { - couchdb: { - image: "budibase/couchdb", - ports: [5984], - env: { - COUCHDB_PASSWORD: "budibase", - COUCHDB_USER: "budibase", - }, - wait: { - type: "ports", - timeout: 20000, - } - } - } -} diff --git a/lerna.json b/lerna.json index d191854fac..bacdcb782f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.22.1", + "version": "2.22.13", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/package.json b/package.json index 0a20f01d52..32693a0b6f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "esbuild-node-externals": "^1.8.0", "eslint": "^8.52.0", "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jest": "^27.9.0", "eslint-plugin-local-rules": "^2.0.0", "eslint-plugin-svelte": "^2.34.0", "husky": "^8.0.3", @@ -25,12 +26,16 @@ "svelte": "^4.2.10", "svelte-eslint-parser": "^0.33.1", "typescript": "5.2.2", + "typescript-eslint": "^7.3.1", "yargs": "^17.7.2" }, "scripts": { "preinstall": "node scripts/syncProPackage.js", + "get-past-client-version": "node scripts/getPastClientVersion.js", "setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev", "build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream", + "build:oss": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui", + "build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --scope @budibase/account-portal --scope @budibase/account-portal-server --scope @budibase/account-portal-ui", "build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput", "check:types": "lerna run check:types", "build:sdk": "lerna run --stream build:sdk", diff --git a/packages/account-portal b/packages/account-portal index 23a1219732..63ce32bca8 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 23a1219732bd778654c0bcc4f49910c511e2d51f +Subproject commit 63ce32bca871f0a752323f5f7ebb5ec16bbbacc3 diff --git a/packages/backend-core/jest-testcontainers-config.js b/packages/backend-core/jest-testcontainers-config.js deleted file mode 100644 index 8ac0f0cd9d..0000000000 --- a/packages/backend-core/jest-testcontainers-config.js +++ /dev/null @@ -1,8 +0,0 @@ -const { join } = require("path") -require("dotenv").config({ - path: join(__dirname, "..", "..", "hosting", ".env"), -}) - -const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator") - -module.exports = jestTestcontainersConfigGenerator() diff --git a/packages/backend-core/jest.config.ts b/packages/backend-core/jest.config.ts index 3f1065ead2..c944b0d7e1 100644 --- a/packages/backend-core/jest.config.ts +++ b/packages/backend-core/jest.config.ts @@ -1,8 +1,8 @@ import { Config } from "@jest/types" const baseConfig: Config.InitialProjectOptions = { - preset: "@trendyol/jest-testcontainers", setupFiles: ["./tests/jestEnv.ts"], + globalSetup: "./../../globalSetup.ts", setupFilesAfterEnv: ["./tests/jestSetup.ts"], transform: { "^.+\\.ts?$": "@swc/jest", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index fe56780982..030fec8728 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -60,7 +60,6 @@ "@shopify/jest-koa-mocks": "5.1.1", "@swc/core": "1.3.71", "@swc/jest": "0.2.27", - "@trendyol/jest-testcontainers": "^2.1.1", "@types/chance": "1.1.3", "@types/cookies": "0.7.8", "@types/jest": "29.5.5", diff --git a/packages/backend-core/scripts/test.sh b/packages/backend-core/scripts/test.sh index 7d19ec96cc..b9937e3a4a 100644 --- a/packages/backend-core/scripts/test.sh +++ b/packages/backend-core/scripts/test.sh @@ -4,10 +4,10 @@ set -e if [[ -n $CI ]] then # --runInBand performs better in ci where resources are limited - echo "jest --coverage --runInBand --forceExit" - jest --coverage --runInBand --forceExit + echo "jest --coverage --runInBand --forceExit $@" + jest --coverage --runInBand --forceExit $@ else # --maxWorkers performs better in development - echo "jest --coverage --detectOpenHandles" - jest --coverage --detectOpenHandles + echo "jest --coverage --forceExit --detectOpenHandles $@" + jest --coverage --forceExit --detectOpenHandles $@ fi \ No newline at end of file diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index 1951c7986c..87ac46cf1c 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -133,7 +133,7 @@ export async function refreshOAuthToken( configId?: string ): Promise { switch (providerType) { - case SSOProviderType.OIDC: + case SSOProviderType.OIDC: { if (!configId) { return { err: { data: "OIDC config id not provided" } } } @@ -142,12 +142,14 @@ export async function refreshOAuthToken( return { err: { data: "OIDC configuration not found" } } } return refreshOIDCAccessToken(oidcConfig, refreshToken) - case SSOProviderType.GOOGLE: + } + case SSOProviderType.GOOGLE: { let googleConfig = await configs.getGoogleConfig() if (!googleConfig) { return { err: { data: "Google configuration not found" } } } return refreshGoogleAccessToken(googleConfig, refreshToken) + } } } diff --git a/packages/backend-core/src/auth/tests/auth.spec.ts b/packages/backend-core/src/auth/tests/auth.spec.ts index 3ae691be58..a80e1ea739 100644 --- a/packages/backend-core/src/auth/tests/auth.spec.ts +++ b/packages/backend-core/src/auth/tests/auth.spec.ts @@ -8,7 +8,7 @@ describe("platformLogout", () => { await testEnv.withTenant(async () => { const ctx = structures.koa.newContext() await auth.platformLogout({ ctx, userId: "test" }) - expect(events.auth.logout).toBeCalledTimes(1) + expect(events.auth.logout).toHaveBeenCalledTimes(1) }) }) }) diff --git a/packages/backend-core/src/cache/base/index.ts b/packages/backend-core/src/cache/base/index.ts index 74da4fe0d2..433941b5c7 100644 --- a/packages/backend-core/src/cache/base/index.ts +++ b/packages/backend-core/src/cache/base/index.ts @@ -129,7 +129,7 @@ export default class BaseCache { } } - async bustCache(key: string, opts = { client: null }) { + async bustCache(key: string) { const client = await this.getClient() try { await client.delete(generateTenantKey(key)) diff --git a/packages/backend-core/src/cache/docWritethrough.ts b/packages/backend-core/src/cache/docWritethrough.ts index 1b129bb26a..05f13a0d91 100644 --- a/packages/backend-core/src/cache/docWritethrough.ts +++ b/packages/backend-core/src/cache/docWritethrough.ts @@ -1,6 +1,6 @@ import { AnyDocument, Database } from "@budibase/types" -import { JobQueue, createQueue } from "../queue" +import { JobQueue, Queue, createQueue } from "../queue" import * as dbUtils from "../db" interface ProcessDocMessage { @@ -12,18 +12,26 @@ interface ProcessDocMessage { const PERSIST_MAX_ATTEMPTS = 100 let processor: DocWritethroughProcessor | undefined -export const docWritethroughProcessorQueue = createQueue( - JobQueue.DOC_WRITETHROUGH_QUEUE, - { - jobOptions: { - attempts: PERSIST_MAX_ATTEMPTS, - }, - } -) +export class DocWritethroughProcessor { + private static _queue: Queue + + public static get queue() { + if (!DocWritethroughProcessor._queue) { + DocWritethroughProcessor._queue = createQueue( + JobQueue.DOC_WRITETHROUGH_QUEUE, + { + jobOptions: { + attempts: PERSIST_MAX_ATTEMPTS, + }, + } + ) + } + + return DocWritethroughProcessor._queue + } -class DocWritethroughProcessor { init() { - docWritethroughProcessorQueue.process(async message => { + DocWritethroughProcessor.queue.process(async message => { try { await this.persistToDb(message.data) } catch (err: any) { @@ -76,7 +84,7 @@ export class DocWritethrough { } async patch(data: Record) { - await docWritethroughProcessorQueue.add({ + await DocWritethroughProcessor.queue.add({ dbName: this.db.name, docId: this.docId, data, diff --git a/packages/backend-core/src/cache/invite.ts b/packages/backend-core/src/cache/invite.ts index e43ebc4aa8..e3d698bcc6 100644 --- a/packages/backend-core/src/cache/invite.ts +++ b/packages/backend-core/src/cache/invite.ts @@ -1,5 +1,5 @@ import * as utils from "../utils" -import { Duration, DurationType } from "../utils" +import { Duration } from "../utils" import env from "../environment" import { getTenantId } from "../context" import * as redis from "../redis/init" diff --git a/packages/backend-core/src/cache/tests/docWritethrough.spec.ts b/packages/backend-core/src/cache/tests/docWritethrough.spec.ts index 1ae85cfd0b..47b3f0672f 100644 --- a/packages/backend-core/src/cache/tests/docWritethrough.spec.ts +++ b/packages/backend-core/src/cache/tests/docWritethrough.spec.ts @@ -6,7 +6,7 @@ import { getDB } from "../../db" import { DocWritethrough, - docWritethroughProcessorQueue, + DocWritethroughProcessor, init, } from "../docWritethrough" @@ -15,7 +15,7 @@ import InMemoryQueue from "../../queue/inMemoryQueue" const initialTime = Date.now() async function waitForQueueCompletion() { - const queue: InMemoryQueue = docWritethroughProcessorQueue as never + const queue: InMemoryQueue = DocWritethroughProcessor.queue as never await queue.waitForCompletion() } @@ -235,11 +235,11 @@ describe("docWritethrough", () => { return acc }, {}) } - const queueMessageSpy = jest.spyOn(docWritethroughProcessorQueue, "add") + const queueMessageSpy = jest.spyOn(DocWritethroughProcessor.queue, "add") await config.doInTenant(async () => { let patches = await parallelPatch(5) - expect(queueMessageSpy).toBeCalledTimes(5) + expect(queueMessageSpy).toHaveBeenCalledTimes(5) await waitForQueueCompletion() expect(await db.get(documentId)).toEqual( @@ -247,7 +247,7 @@ describe("docWritethrough", () => { ) patches = { ...patches, ...(await parallelPatch(40)) } - expect(queueMessageSpy).toBeCalledTimes(45) + expect(queueMessageSpy).toHaveBeenCalledTimes(45) await waitForQueueCompletion() expect(await db.get(documentId)).toEqual( @@ -255,7 +255,7 @@ describe("docWritethrough", () => { ) patches = { ...patches, ...(await parallelPatch(10)) } - expect(queueMessageSpy).toBeCalledTimes(55) + expect(queueMessageSpy).toHaveBeenCalledTimes(55) await waitForQueueCompletion() expect(await db.get(documentId)).toEqual( @@ -265,6 +265,7 @@ describe("docWritethrough", () => { }) // This is not yet supported + // eslint-disable-next-line jest/no-disabled-tests it.skip("patches will execute in order", async () => { let incrementalValue = 0 const keyToOverride = generator.word() diff --git a/packages/backend-core/src/cache/tests/user.spec.ts b/packages/backend-core/src/cache/tests/user.spec.ts index 80e5bc3063..49a8d51c16 100644 --- a/packages/backend-core/src/cache/tests/user.spec.ts +++ b/packages/backend-core/src/cache/tests/user.spec.ts @@ -55,8 +55,8 @@ describe("user cache", () => { })), }) - expect(UserDB.bulkGet).toBeCalledTimes(1) - expect(UserDB.bulkGet).toBeCalledWith(userIdsToRequest) + expect(UserDB.bulkGet).toHaveBeenCalledTimes(1) + expect(UserDB.bulkGet).toHaveBeenCalledWith(userIdsToRequest) }) it("on a second all, all of them are retrieved from cache", async () => { @@ -82,7 +82,7 @@ describe("user cache", () => { ), }) - expect(UserDB.bulkGet).toBeCalledTimes(1) + expect(UserDB.bulkGet).toHaveBeenCalledTimes(1) }) it("when some users are cached, only the missing ones are retrieved from db", async () => { @@ -110,8 +110,8 @@ describe("user cache", () => { ), }) - expect(UserDB.bulkGet).toBeCalledTimes(1) - expect(UserDB.bulkGet).toBeCalledWith([ + expect(UserDB.bulkGet).toHaveBeenCalledTimes(1) + expect(UserDB.bulkGet).toHaveBeenCalledWith([ userIdsToRequest[1], userIdsToRequest[2], userIdsToRequest[4], diff --git a/packages/backend-core/src/cache/writethrough.ts b/packages/backend-core/src/cache/writethrough.ts index 5cafe418d7..cd7409ca15 100644 --- a/packages/backend-core/src/cache/writethrough.ts +++ b/packages/backend-core/src/cache/writethrough.ts @@ -8,7 +8,7 @@ const DEFAULT_WRITE_RATE_MS = 10000 let CACHE: BaseCache | null = null interface CacheItem { - doc: any + doc: T lastWrite: number } diff --git a/packages/backend-core/src/context/tests/index.spec.ts b/packages/backend-core/src/context/tests/index.spec.ts index cfc820e169..2d89131549 100644 --- a/packages/backend-core/src/context/tests/index.spec.ts +++ b/packages/backend-core/src/context/tests/index.spec.ts @@ -246,7 +246,7 @@ describe("context", () => { context.doInAppMigrationContext(db.generateAppID(), async () => { await otherContextCall() }) - ).rejects.toThrowError( + ).rejects.toThrow( "The context cannot be changed, a migration is currently running" ) } diff --git a/packages/backend-core/src/db/Replication.ts b/packages/backend-core/src/db/Replication.ts index 9c960d76dd..735c2fa86e 100644 --- a/packages/backend-core/src/db/Replication.ts +++ b/packages/backend-core/src/db/Replication.ts @@ -27,7 +27,7 @@ class Replication { return resolve(info) }) .on("error", function (err) { - throw new Error(`Replication Error: ${err}`) + throw err }) }) } diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index f982ee67d0..987d750d45 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -10,10 +10,6 @@ interface SearchResponse { totalRows: number } -interface PaginatedSearchResponse extends SearchResponse { - hasNextPage: boolean -} - export type SearchParams = { tableId?: string sort?: string @@ -247,7 +243,7 @@ export class QueryBuilder { } // Escape characters if (!this.#noEscaping && escape && originalType === "string") { - value = `${value}`.replace(/[ \/#+\-&|!(){}\]^"~*?:\\]/g, "\\$&") + value = `${value}`.replace(/[ /#+\-&|!(){}\]^"~*?:\\]/g, "\\$&") } // Wrap in quotes diff --git a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts index b953e3516e..8742d405f2 100644 --- a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts +++ b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts @@ -34,12 +34,12 @@ export async function createUserIndex() { } let idxKey = prev != null ? `${prev}.${key}` : key if (typeof input[key] === "string") { + // @ts-expect-error index is available in a CouchDB map function // eslint-disable-next-line no-undef - // @ts-ignore index(idxKey, input[key].toLowerCase(), { facet: true }) } else if (typeof input[key] !== "object") { + // @ts-expect-error index is available in a CouchDB map function // eslint-disable-next-line no-undef - // @ts-ignore index(idxKey, input[key], { facet: true }) } else { idx(input[key], idxKey) diff --git a/packages/backend-core/src/docUpdates/index.ts b/packages/backend-core/src/docUpdates/index.ts index 3971f8de12..bd34f4f0cd 100644 --- a/packages/backend-core/src/docUpdates/index.ts +++ b/packages/backend-core/src/docUpdates/index.ts @@ -17,13 +17,8 @@ export function init(processors: ProcessorMap) { // if not processing in this instance, kick it off if (!processingPromise) { processingPromise = asyncEventQueue.process(async job => { - const { event, identity, properties, timestamp } = job.data - await documentProcessor.processEvent( - event, - identity, - properties, - timestamp - ) + const { event, identity, properties } = job.data + await documentProcessor.processEvent(event, identity, properties) }) } } diff --git a/packages/backend-core/src/events/events.ts b/packages/backend-core/src/events/events.ts index f02b9fdf32..92b81553b0 100644 --- a/packages/backend-core/src/events/events.ts +++ b/packages/backend-core/src/events/events.ts @@ -1,4 +1,4 @@ -import { Event } from "@budibase/types" +import { Event, Identity } from "@budibase/types" import { processors } from "./processors" import identification from "./identification" import * as backfill from "./backfill" @@ -7,12 +7,19 @@ import { publishAsyncEvent } from "./asyncEvents" export const publishEvent = async ( event: Event, properties: any, - timestamp?: string | number + timestamp?: string | number, + identityOverride?: Identity ) => { // in future this should use async events via a distributed queue. - const identity = await identification.getCurrentIdentity() + const identity = + identityOverride || (await identification.getCurrentIdentity()) + + // Backfilling is get from the user cache, but when we override the identity cache is not available. Overrides are + // normally performed in automatic actions or operations in async flows (BPM) where the user session is not available. + const backfilling = identityOverride + ? false + : await backfill.isBackfillingEvent(event) - const backfilling = await backfill.isBackfillingEvent(event) // no backfill - send the event and exit if (!backfilling) { // send off async events if required diff --git a/packages/backend-core/src/events/processors/AuditLogsProcessor.ts b/packages/backend-core/src/events/processors/AuditLogsProcessor.ts index 94b4e1b09f..3dd2ab9d10 100644 --- a/packages/backend-core/src/events/processors/AuditLogsProcessor.ts +++ b/packages/backend-core/src/events/processors/AuditLogsProcessor.ts @@ -1,7 +1,6 @@ import { Event, Identity, - Group, IdentityType, AuditLogQueueEvent, AuditLogFn, @@ -79,11 +78,11 @@ export default class AuditLogsProcessor implements EventProcessor { } } - async identify(identity: Identity, timestamp?: string | number) { + async identify() { // no-op } - async identifyGroup(group: Group, timestamp?: string | number) { + async identifyGroup() { // no-op } diff --git a/packages/backend-core/src/events/processors/LoggingProcessor.ts b/packages/backend-core/src/events/processors/LoggingProcessor.ts index 0f4d02b99c..9f2dc5a473 100644 --- a/packages/backend-core/src/events/processors/LoggingProcessor.ts +++ b/packages/backend-core/src/events/processors/LoggingProcessor.ts @@ -8,8 +8,7 @@ export default class LoggingProcessor implements EventProcessor { async processEvent( event: Event, identity: Identity, - properties: any, - timestamp?: string + properties: any ): Promise { if (skipLogging) { return @@ -17,14 +16,14 @@ export default class LoggingProcessor implements EventProcessor { console.log(`[audit] [identityType=${identity.type}] ${event}`, properties) } - async identify(identity: Identity, timestamp?: string | number) { + async identify(identity: Identity) { if (skipLogging) { return } console.log(`[audit] identified`, identity) } - async identifyGroup(group: Group, timestamp?: string | number) { + async identifyGroup(group: Group) { if (skipLogging) { return } diff --git a/packages/backend-core/src/events/processors/async/DocumentUpdateProcessor.ts b/packages/backend-core/src/events/processors/async/DocumentUpdateProcessor.ts index 54304ee21b..92afcdc637 100644 --- a/packages/backend-core/src/events/processors/async/DocumentUpdateProcessor.ts +++ b/packages/backend-core/src/events/processors/async/DocumentUpdateProcessor.ts @@ -14,12 +14,7 @@ export default class DocumentUpdateProcessor implements EventProcessor { this.processors = processors } - async processEvent( - event: Event, - identity: Identity, - properties: any, - timestamp?: string | number - ) { + async processEvent(event: Event, identity: Identity, properties: any) { const tenantId = identity.realTenantId const docId = getDocumentId(event, properties) if (!tenantId || !docId) { diff --git a/packages/backend-core/src/events/publishers/account.ts b/packages/backend-core/src/events/publishers/account.ts index d337e404ef..99767962dd 100644 --- a/packages/backend-core/src/events/publishers/account.ts +++ b/packages/backend-core/src/events/publishers/account.ts @@ -5,13 +5,19 @@ import { AccountCreatedEvent, AccountDeletedEvent, AccountVerifiedEvent, + Identity, } from "@budibase/types" -async function created(account: Account) { +async function created(account: Account, identityOverride?: Identity) { const properties: AccountCreatedEvent = { tenantId: account.tenantId, } - await publishEvent(Event.ACCOUNT_CREATED, properties) + await publishEvent( + Event.ACCOUNT_CREATED, + properties, + undefined, + identityOverride + ) } async function deleted(account: Account) { diff --git a/packages/backend-core/src/logging/pino/logger.ts b/packages/backend-core/src/logging/pino/logger.ts index 7a051e7f12..0a8470a453 100644 --- a/packages/backend-core/src/logging/pino/logger.ts +++ b/packages/backend-core/src/logging/pino/logger.ts @@ -10,6 +10,18 @@ import { formats } from "dd-trace/ext" import { localFileDestination } from "../system" +function isPlainObject(obj: any) { + return typeof obj === "object" && obj !== null && !(obj instanceof Error) +} + +function isError(obj: any) { + return obj instanceof Error +} + +function isMessage(obj: any) { + return typeof obj === "string" +} + // LOGGER let pinoInstance: pino.Logger | undefined @@ -71,23 +83,11 @@ if (!env.DISABLE_PINO_LOGGER) { err?: Error } - function isPlainObject(obj: any) { - return typeof obj === "object" && obj !== null && !(obj instanceof Error) - } - - function isError(obj: any) { - return obj instanceof Error - } - - function isMessage(obj: any) { - return typeof obj === "string" - } - /** * Backwards compatibility between console logging statements * and pino logging requirements. */ - function getLogParams(args: any[]): [MergingObject, string] { + const getLogParams = (args: any[]): [MergingObject, string] => { let error = undefined let objects: any[] = [] let message = "" diff --git a/packages/backend-core/src/middleware/matchers.ts b/packages/backend-core/src/middleware/matchers.ts index efbdec2dbe..757d93a60d 100644 --- a/packages/backend-core/src/middleware/matchers.ts +++ b/packages/backend-core/src/middleware/matchers.ts @@ -11,7 +11,6 @@ export const buildMatcherRegex = ( return patterns.map(pattern => { let route = pattern.route const method = pattern.method - const strict = pattern.strict ? pattern.strict : false // if there is a param in the route // use a wildcard pattern @@ -24,24 +23,17 @@ export const buildMatcherRegex = ( } } - return { regex: new RegExp(route), method, strict, route } + return { regex: new RegExp(route), method, route } }) } export const matches = (ctx: BBContext, options: RegexMatcher[]) => { - return options.find(({ regex, method, strict, route }) => { - let urlMatch - if (strict) { - urlMatch = ctx.request.url === route - } else { - urlMatch = regex.test(ctx.request.url) - } - + return options.find(({ regex, method }) => { + const urlMatch = regex.test(ctx.request.url) const methodMatch = method === "ALL" ? true : ctx.request.method.toLowerCase() === method.toLowerCase() - return urlMatch && methodMatch }) } diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index ab4ffee9d2..7f768f1623 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -3,7 +3,7 @@ import { Cookie } from "../../../constants" import * as configs from "../../../configs" import * as cache from "../../../cache" import * as utils from "../../../utils" -import { UserCtx, SSOProfile, DatasourceAuthCookie } from "@budibase/types" +import { UserCtx, SSOProfile } from "@budibase/types" import { ssoSaveUserNoOp } from "../sso/sso" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy diff --git a/packages/backend-core/src/middleware/passport/sso/sso.ts b/packages/backend-core/src/middleware/passport/sso/sso.ts index 2fc1184722..ee84f03dae 100644 --- a/packages/backend-core/src/middleware/passport/sso/sso.ts +++ b/packages/backend-core/src/middleware/passport/sso/sso.ts @@ -5,7 +5,6 @@ import * as context from "../../../context" import fetch from "node-fetch" import { SaveSSOUserFunction, - SaveUserOpts, SSOAuthDetails, SSOUser, User, @@ -14,10 +13,8 @@ import { // 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) +export const ssoSaveUserNoOp: SaveSSOUserFunction = (user: SSOUser) => + Promise.resolve(user) /** * Common authentication logic for third parties. e.g. OAuth, OIDC. 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 index d3486a5b14..ea9584c284 100644 --- a/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts +++ b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts @@ -114,11 +114,11 @@ describe("sso", () => { // tenant id added ssoUser.tenantId = context.getTenantId() - expect(mockSaveUser).toBeCalledWith(ssoUser, { + expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, { hashPassword: false, requirePassword: false, }) - expect(mockDone).toBeCalledWith(null, ssoUser) + expect(mockDone).toHaveBeenCalledWith(null, ssoUser) }) }) }) @@ -159,11 +159,11 @@ describe("sso", () => { // existing id preserved ssoUser._id = existingUser._id - expect(mockSaveUser).toBeCalledWith(ssoUser, { + expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, { hashPassword: false, requirePassword: false, }) - expect(mockDone).toBeCalledWith(null, ssoUser) + expect(mockDone).toHaveBeenCalledWith(null, ssoUser) }) }) @@ -187,11 +187,11 @@ describe("sso", () => { // existing id preserved ssoUser._id = existingUser._id - expect(mockSaveUser).toBeCalledWith(ssoUser, { + expect(mockSaveUser).toHaveBeenCalledWith(ssoUser, { hashPassword: false, requirePassword: false, }) - expect(mockDone).toBeCalledWith(null, ssoUser) + expect(mockDone).toHaveBeenCalledWith(null, ssoUser) }) }) }) diff --git a/packages/backend-core/src/middleware/tests/builder.spec.ts b/packages/backend-core/src/middleware/tests/builder.spec.ts index 0514dc13f0..0f35b0b833 100644 --- a/packages/backend-core/src/middleware/tests/builder.spec.ts +++ b/packages/backend-core/src/middleware/tests/builder.spec.ts @@ -24,13 +24,13 @@ function buildUserCtx(user: ContextUser) { } function passed(throwFn: jest.Func, nextFn: jest.Func) { - expect(throwFn).not.toBeCalled() - expect(nextFn).toBeCalled() + expect(throwFn).not.toHaveBeenCalled() + expect(nextFn).toHaveBeenCalled() } function threw(throwFn: jest.Func) { // cant check next, the throw function doesn't actually throw - so it still continues - expect(throwFn).toBeCalled() + expect(throwFn).toHaveBeenCalled() } describe("adminOnly middleware", () => { diff --git a/packages/backend-core/src/middleware/tests/matchers.spec.ts b/packages/backend-core/src/middleware/tests/matchers.spec.ts index c39bbb6dd3..1b79db2e68 100644 --- a/packages/backend-core/src/middleware/tests/matchers.spec.ts +++ b/packages/backend-core/src/middleware/tests/matchers.spec.ts @@ -34,23 +34,6 @@ describe("matchers", () => { expect(!!matchers.matches(ctx, built)).toBe(true) }) - it("doesn't wildcard path with strict", () => { - const pattern = [ - { - route: "/api/tests", - method: "POST", - strict: true, - }, - ] - const ctx = structures.koa.newContext() - ctx.request.url = "/api/tests/id/something/else" - ctx.request.method = "POST" - - const built = matchers.buildMatcherRegex(pattern) - - expect(!!matchers.matches(ctx, built)).toBe(false) - }) - it("matches with param", () => { const pattern = [ { @@ -67,23 +50,6 @@ describe("matchers", () => { expect(!!matchers.matches(ctx, built)).toBe(true) }) - // TODO: Support the below behaviour - // Strict does not work when a param is present - // it("matches with param with strict", () => { - // const pattern = [{ - // route: "/api/tests/:testId", - // method: "GET", - // strict: true - // }] - // const ctx = structures.koa.newContext() - // ctx.request.url = "/api/tests/id" - // ctx.request.method = "GET" - // - // const built = matchers.buildMatcherRegex(pattern) - // - // expect(!!matchers.matches(ctx, built)).toBe(true) - // }) - it("doesn't match by path", () => { const pattern = [ { diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts index c750bc4882..3f033b8cdb 100644 --- a/packages/backend-core/src/migrations/migrations.ts +++ b/packages/backend-core/src/migrations/migrations.ts @@ -45,10 +45,6 @@ export const runMigration = async ( options: MigrationOptions = {} ) => { const migrationType = migration.type - let tenantId: string | undefined - if (migrationType !== MigrationType.INSTALLATION) { - tenantId = context.getTenantId() - } const migrationName = migration.name const silent = migration.silent diff --git a/packages/backend-core/src/objectStore/buckets/tests/app.spec.ts b/packages/backend-core/src/objectStore/buckets/tests/app.spec.ts index cbbbee6255..4a132ce54d 100644 --- a/packages/backend-core/src/objectStore/buckets/tests/app.spec.ts +++ b/packages/backend-core/src/objectStore/buckets/tests/app.spec.ts @@ -126,7 +126,7 @@ describe("app", () => { it("gets url with embedded minio", async () => { testEnv.withMinio() - await testEnv.withTenant(tenantId => { + await testEnv.withTenant(() => { const url = getAppFileUrl() expect(url).toBe( "/files/signed/prod-budi-app-assets/app_123/attachments/image.jpeg" @@ -136,7 +136,7 @@ describe("app", () => { it("gets url with custom S3", async () => { testEnv.withS3() - await testEnv.withTenant(tenantId => { + await testEnv.withTenant(() => { const url = getAppFileUrl() expect(url).toBe( "http://s3.example.com/prod-budi-app-assets/app_123/attachments/image.jpeg" @@ -146,7 +146,7 @@ describe("app", () => { it("gets url with cloudfront + s3", async () => { testEnv.withCloudfront() - await testEnv.withTenant(tenantId => { + await testEnv.withTenant(() => { const url = getAppFileUrl() // omit rest of signed params expect( diff --git a/packages/backend-core/src/platform/tests/tenants.spec.ts b/packages/backend-core/src/platform/tests/tenants.spec.ts index b2ab75c954..e22003fd45 100644 --- a/packages/backend-core/src/platform/tests/tenants.spec.ts +++ b/packages/backend-core/src/platform/tests/tenants.spec.ts @@ -3,7 +3,7 @@ import { DBTestConfiguration } from "../../../tests/extra" import * as tenants from "../tenants" describe("tenants", () => { - const config = new DBTestConfiguration() + new DBTestConfiguration() describe("addTenant", () => { it("concurrently adds multiple tenants safely", async () => { diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts index 6f030afb7c..ccaad76b19 100644 --- a/packages/backend-core/src/platform/users.ts +++ b/packages/backend-core/src/platform/users.ts @@ -20,7 +20,7 @@ export async function lookupTenantId(userId: string) { return user.tenantId } -async function getUserDoc(emailOrId: string): Promise { +export async function getUserDoc(emailOrId: string): Promise { const db = getPlatformDB() return db.get(emailOrId) } @@ -79,6 +79,17 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) { } } +export async function addSsoUser( + ssoId: string, + email: string, + userId: string, + tenantId: string +) { + return addUserDoc(ssoId, () => + newUserSsoIdDoc(ssoId, email, userId, tenantId) + ) +} + export async function addUser( tenantId: string, userId: string, @@ -91,9 +102,7 @@ export async function addUser( ] if (ssoId) { - promises.push( - addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId)) - ) + promises.push(addSsoUser(ssoId, email, userId, tenantId)) } await Promise.all(promises) diff --git a/packages/backend-core/src/queue/inMemoryQueue.ts b/packages/backend-core/src/queue/inMemoryQueue.ts index afb5592562..87e43b324d 100644 --- a/packages/backend-core/src/queue/inMemoryQueue.ts +++ b/packages/backend-core/src/queue/inMemoryQueue.ts @@ -39,7 +39,7 @@ class InMemoryQueue implements Partial { _opts?: QueueOptions _messages: JobMessage[] _queuedJobIds: Set - _emitter: EventEmitter + _emitter: NodeJS.EventEmitter _runCount: number _addCount: number @@ -166,7 +166,7 @@ class InMemoryQueue implements Partial { return [] } - // eslint-disable-next-line no-unused-vars + // eslint-disable-next-line @typescript-eslint/no-unused-vars async removeJobs(pattern: string) { // no-op } diff --git a/packages/backend-core/src/queue/listeners.ts b/packages/backend-core/src/queue/listeners.ts index 14dce5fe8d..cd25ff2254 100644 --- a/packages/backend-core/src/queue/listeners.ts +++ b/packages/backend-core/src/queue/listeners.ts @@ -132,7 +132,7 @@ function logging(queue: Queue, jobQueue: JobQueue) { // A Job is waiting to be processed as soon as a worker is idling. console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId })) }) - .on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => { + .on(BullEvent.ACTIVE, async (job: Job) => { // A job has started. You can use `jobPromise.cancel()`` to abort it. await doInJobContext(job, () => { console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job })) diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index 7920dfed2d..44ba28a83c 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -40,6 +40,7 @@ export async function shutdown() { if (inviteClient) await inviteClient.finish() if (passwordResetClient) await passwordResetClient.finish() if (socketClient) await socketClient.finish() + if (docWritethroughClient) await docWritethroughClient.finish() } process.on("exit", async () => { diff --git a/packages/backend-core/src/redis/tests/redis.spec.ts b/packages/backend-core/src/redis/tests/redis.spec.ts index c2c9e4a14e..4d11caf220 100644 --- a/packages/backend-core/src/redis/tests/redis.spec.ts +++ b/packages/backend-core/src/redis/tests/redis.spec.ts @@ -120,7 +120,7 @@ describe("redis", () => { await redis.bulkStore(data, ttl) - for (const [key, value] of Object.entries(data)) { + for (const key of Object.keys(data)) { expect(await redis.get(key)).toBe(null) } @@ -147,17 +147,6 @@ describe("redis", () => { expect(results).toEqual([1, 2, 3, 4, 5]) }) - it("can increment on a new key", async () => { - const key1 = structures.uuid() - const key2 = structures.uuid() - - const result1 = await redis.increment(key1) - expect(result1).toBe(1) - - const result2 = await redis.increment(key2) - expect(result2).toBe(1) - }) - it("can increment multiple times in parallel", async () => { const key = structures.uuid() const results = await Promise.all( @@ -184,7 +173,7 @@ describe("redis", () => { const key = structures.uuid() await redis.store(key, value) - await expect(redis.increment(key)).rejects.toThrowError( + await expect(redis.increment(key)).rejects.toThrow( "ERR value is not an integer or out of range" ) }) diff --git a/packages/backend-core/src/redis/tests/redlockImpl.spec.ts b/packages/backend-core/src/redis/tests/redlockImpl.spec.ts index a1e83d8e6c..e647b63bf5 100644 --- a/packages/backend-core/src/redis/tests/redlockImpl.spec.ts +++ b/packages/backend-core/src/redis/tests/redlockImpl.spec.ts @@ -96,8 +96,8 @@ describe("redlockImpl", () => { task: mockTask, executionTimeMs: lockTtl * 2, }) - ).rejects.toThrowError( - `Unable to fully release the lock on resource \"lock:${config.tenantId}_persist_writethrough\".` + ).rejects.toThrow( + `Unable to fully release the lock on resource "lock:${config.tenantId}_persist_writethrough".` ) } ) diff --git a/packages/backend-core/src/tenancy/tests/tenancy.spec.ts b/packages/backend-core/src/tenancy/tests/tenancy.spec.ts index 95dd76a6dd..34e9f87064 100644 --- a/packages/backend-core/src/tenancy/tests/tenancy.spec.ts +++ b/packages/backend-core/src/tenancy/tests/tenancy.spec.ts @@ -158,8 +158,8 @@ describe("getTenantIDFromCtx", () => { ], } expect(getTenantIDFromCtx(ctx, mockOpts)).toBeUndefined() - expect(ctx.throw).toBeCalledTimes(1) - expect(ctx.throw).toBeCalledWith(403, "Tenant id not set") + expect(ctx.throw).toHaveBeenCalledTimes(1) + expect(ctx.throw).toHaveBeenCalledWith(403, "Tenant id not set") }) it("returns undefined if allowNoTenant is true", () => { diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 136cb4b8ad..04d3264e6f 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -500,13 +500,13 @@ export class UserDB { static async createAdminUser( email: string, - password: string, tenantId: string, + password?: string, opts?: CreateAdminUserOpts ) { const user: User = { email: email, - password: password, + password, createdAt: Date.now(), roles: {}, builder: { diff --git a/packages/backend-core/src/users/test/utils.spec.ts b/packages/backend-core/src/users/test/utils.spec.ts index 0fe27f57a6..cb98b8972b 100644 --- a/packages/backend-core/src/users/test/utils.spec.ts +++ b/packages/backend-core/src/users/test/utils.spec.ts @@ -45,7 +45,7 @@ describe("Users", () => { ...{ _id: groupId, roles: { app1: "ADMIN" } }, } const users: User[] = [] - for (const _ of Array.from({ length: usersInGroup })) { + for (let i = 0; i < usersInGroup; i++) { const userId = `us_${generator.guid()}` const user: User = structures.users.user({ _id: userId, diff --git a/packages/backend-core/tests/core/utilities/structures/userGroups.ts b/packages/backend-core/tests/core/utilities/structures/userGroups.ts index 4dc870a00a..4af3f72e51 100644 --- a/packages/backend-core/tests/core/utilities/structures/userGroups.ts +++ b/packages/backend-core/tests/core/utilities/structures/userGroups.ts @@ -3,7 +3,7 @@ import { generator } from "./generator" export function userGroup(): UserGroup { return { - name: generator.word(), + name: generator.guid(), icon: generator.word(), color: generator.word(), } diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index 7da6cbc777..5d4f5a3c11 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -1,80 +1,58 @@ +import { DatabaseImpl } from "../../../src/db" import { execSync } from "child_process" -let dockerPsResult: string | undefined - -function formatDockerPsResult(serverName: string, port: number) { - const lines = dockerPsResult?.split("\n") - let first = true - if (!lines) { - return null - } - for (let line of lines) { - if (first) { - first = false - continue - } - let toLookFor = serverName.split("-service")[0] - if (!line.includes(toLookFor)) { - continue - } - const regex = new RegExp(`0.0.0.0:([0-9]*)->${port}`, "g") - const found = line.match(regex) - if (found) { - return found[0].split(":")[1].split("->")[0] - } - } - return null +interface ContainerInfo { + Command: string + CreatedAt: string + ID: string + Image: string + Labels: string + LocalVolumes: string + Mounts: string + Names: string + Networks: string + Ports: string + RunningFor: string + Size: string + State: string + Status: string } -function getTestContainerSettings( - serverName: string, - key: string -): string | null { - const entry = Object.entries(global).find( - ([k]) => - k.includes(`${serverName.toUpperCase()}`) && - k.includes(`${key.toUpperCase()}`) - ) - if (!entry) { - return null - } - return entry[1] +function getTestcontainers(): ContainerInfo[] { + return execSync("docker ps --format json") + .toString() + .split("\n") + .filter(x => x.length > 0) + .map(x => JSON.parse(x) as ContainerInfo) + .filter(x => x.Labels.includes("org.testcontainers=true")) } -function getContainerInfo(containerName: string, port: number) { - let assignedPort = getTestContainerSettings( - containerName.toUpperCase(), - `PORT_${port}` - ) - if (!dockerPsResult) { - try { - const outputBuffer = execSync("docker ps") - dockerPsResult = outputBuffer.toString("utf8") - } catch (err) { - //no-op - } - } - const possiblePort = formatDockerPsResult(containerName, port) - if (possiblePort) { - assignedPort = possiblePort - } - const host = getTestContainerSettings(containerName.toUpperCase(), "IP") - return { - port: assignedPort, - host, - url: host && assignedPort && `http://${host}:${assignedPort}`, - } +function getContainerByImage(image: string) { + return getTestcontainers().find(x => x.Image.startsWith(image)) } -function getCouchConfig() { - return getContainerInfo("couchdb", 5984) +function getExposedPort(container: ContainerInfo, port: number) { + const match = container.Ports.match(new RegExp(`0.0.0.0:(\\d+)->${port}/tcp`)) + if (!match) { + return undefined + } + return parseInt(match[1]) } export function setupEnv(...envs: any[]) { - const couch = getCouchConfig() + const couch = getContainerByImage("budibase/couchdb") + if (!couch) { + throw new Error("CouchDB container not found") + } + + const couchPort = getExposedPort(couch, 5984) + if (!couchPort) { + throw new Error("CouchDB port not found") + } + const configs = [ - { key: "COUCH_DB_PORT", value: couch.port }, - { key: "COUCH_DB_URL", value: couch.url }, + { key: "COUCH_DB_PORT", value: `${couchPort}` }, + { key: "COUCH_DB_URL", value: `http://localhost:${couchPort}` }, ] for (const config of configs.filter(x => !!x.value)) { @@ -82,4 +60,7 @@ export function setupEnv(...envs: any[]) { env._set(config.key, config.value) } } + + // @ts-expect-error + DatabaseImpl.nano = undefined } diff --git a/packages/backend-core/tests/jestEnv.ts b/packages/backend-core/tests/jestEnv.ts index c2047118ec..2c797c9fff 100644 --- a/packages/backend-core/tests/jestEnv.ts +++ b/packages/backend-core/tests/jestEnv.ts @@ -4,3 +4,7 @@ process.env.NODE_ENV = "jest" process.env.MOCK_REDIS = "1" process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" process.env.REDIS_PASSWORD = "budibase" +process.env.COUCH_DB_PASSWORD = "budibase" +process.env.COUCH_DB_USER = "budibase" +process.env.API_ENCRYPTION_KEY = "testsecret" +process.env.JWT_SECRET = "testsecret" diff --git a/packages/bbui/rollup.config.js b/packages/bbui/rollup.config.js index e285d548d6..da274e0ba5 100644 --- a/packages/bbui/rollup.config.js +++ b/packages/bbui/rollup.config.js @@ -12,6 +12,13 @@ export default { format: "esm", file: "dist/bbui.es.js", }, + onwarn(warning, warn) { + // suppress eval warnings + if (warning.code === "EVAL") { + return + } + warn(warning) + }, plugins: [ resolve(), commonjs(), diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.js index 12c4c4d002..eafca657f3 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.js @@ -39,19 +39,23 @@ const handleClick = event => { return } + if (handler.allowedType && event.type !== handler.allowedType) { + return + } + handler.callback?.(event) }) } document.documentElement.addEventListener("click", handleClick, true) -document.documentElement.addEventListener("contextmenu", handleClick, true) +document.documentElement.addEventListener("mousedown", handleClick, true) /** * Adds or updates a click handler */ -const updateHandler = (id, element, anchor, callback) => { +const updateHandler = (id, element, anchor, callback, allowedType) => { let existingHandler = clickHandlers.find(x => x.id === id) if (!existingHandler) { - clickHandlers.push({ id, element, anchor, callback }) + clickHandlers.push({ id, element, anchor, callback, allowedType }) } else { existingHandler.callback = callback } @@ -75,9 +79,11 @@ const removeHandler = id => { export default (element, opts) => { const id = Math.random() const update = newOpts => { - const callback = newOpts?.callback || newOpts + const callback = + newOpts?.callback || (typeof newOpts === "function" ? newOpts : null) const anchor = newOpts?.anchor || element - updateHandler(id, element, anchor, callback) + const allowedType = newOpts?.allowedType || "click" + updateHandler(id, element, anchor, callback, allowedType) } update(opts) return { diff --git a/packages/bbui/src/Drawer/DrawerContent.svelte b/packages/bbui/src/Drawer/DrawerContent.svelte index 490dfecc31..f7345afb11 100644 --- a/packages/bbui/src/Drawer/DrawerContent.svelte +++ b/packages/bbui/src/Drawer/DrawerContent.svelte @@ -42,7 +42,6 @@ .main { height: 100%; overflow: auto; - overflow-x: hidden; } .padding .main { padding: var(--spacing-xl); diff --git a/packages/bbui/src/Table/CellRenderer.svelte b/packages/bbui/src/Table/CellRenderer.svelte index 4ad6e22d7e..eff1178f6d 100644 --- a/packages/bbui/src/Table/CellRenderer.svelte +++ b/packages/bbui/src/Table/CellRenderer.svelte @@ -12,6 +12,7 @@ export let schema export let value export let customRenderers = [] + export let snippets let renderer const typeMap = { @@ -44,7 +45,7 @@ if (!template) { return value } - return processStringSync(template, { value }) + return processStringSync(template, { value, snippets }) } diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index 33b9bd9a7e..868f7b3a0b 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -42,6 +42,7 @@ export let customPlaceholder = false export let showHeaderBorder = true export let placeholderText = "No rows found" + export let snippets = [] const dispatch = createEventDispatcher() @@ -425,6 +426,7 @@ - {:else if editableColumn.type === FieldType.ATTACHMENT} - { - if (!e.detail) { - editableColumn.constraints ??= { length: {} } - editableColumn.constraints.length ??= {} - editableColumn.constraints.length.maximum = 1 - editableColumn.constraints.length.message = - "cannot contain multiple files" - } else { - delete editableColumn.constraints?.length?.maximum - delete editableColumn.constraints?.length?.message - } - }} - thin - text="Allow multiple" - /> {/if} {#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn} + + + diff --git a/packages/builder/src/components/deploy/VersionModal.svelte b/packages/builder/src/components/deploy/VersionModal.svelte index 0944512fe8..316a981325 100644 --- a/packages/builder/src/components/deploy/VersionModal.svelte +++ b/packages/builder/src/components/deploy/VersionModal.svelte @@ -1,4 +1,5 @@ @@ -41,6 +44,9 @@ icon: "TableColumnMerge", }, ]} + context={{ + value: columnValue, + }} /> diff --git a/packages/builder/src/helpers/components.js b/packages/builder/src/helpers/components.js index 4f4f3ed380..a03ebfdfa7 100644 --- a/packages/builder/src/helpers/components.js +++ b/packages/builder/src/helpers/components.js @@ -279,3 +279,11 @@ export const buildContextTreeLookupMap = rootComponent => { }) return map } + +// Get a flat list of ids for all descendants of a component +export const getChildIdsForComponent = component => { + return [ + component._id, + ...(component?._children ?? []).map(getChildIdsForComponent).flat(1), + ] +} diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte index d714bafc70..035dc5a2ef 100644 --- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte @@ -129,10 +129,7 @@ filteredUsers = $usersFetch.rows .filter(user => user.email !== $auth.user.email) .map(user => { - const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder( - user, - prodAppId - ) + const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(user) const isAppBuilder = user.builder?.apps?.includes(prodAppId) let role if (isAdminOrGlobalBuilder) { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte index 50e1ad0cf8..4db218f60b 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Navigation/index.svelte @@ -24,6 +24,13 @@ navigationStore, } from "stores/builder" import { DefaultAppTheme } from "constants" + import BarButtonList from "/src/components/design/settings/controls/BarButtonList.svelte" + + $: alignmentOptions = [ + { value: "Left", barIcon: "TextAlignLeft" }, + { value: "Center", barIcon: "TextAlignCenter" }, + { value: "Right", barIcon: "TextAlignRight" }, + ] $: screenRouteOptions = $screenStore.screens .map(screen => screen.routing?.route) @@ -46,6 +53,10 @@ notifications.error("Error updating navigation settings") } } + + const updateTextAlign = textAlignValue => { + navigationStore.syncAppNavigation({ textAlign: textAlignValue }) + } update("title", e.detail)} updateOnChange={false} /> + +
+ +
+ {/if}
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index 96e8faf93c..b66c9cca3a 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -3,8 +3,6 @@ "name": "Blocks", "icon": "Article", "children": [ - "gridblock", - "tableblock", "cardsblock", "repeaterblock", "formblock", @@ -16,7 +14,7 @@ { "name": "Layout", "icon": "ClassicGridView", - "children": ["container", "section", "grid", "sidepanel"] + "children": ["container", "section", "sidepanel"] }, { "name": "Data", @@ -24,7 +22,7 @@ "children": [ "dataprovider", "repeater", - "table", + "gridblock", "spreadsheet", "dynamicfilter", "daterangepicker" diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte index ca1d18d688..dc22840261 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPreview.svelte @@ -10,20 +10,15 @@ navigationStore, selectedScreen, hoverStore, + componentTreeNodesStore, snippets, } from "stores/builder" import ConfirmDialog from "components/common/ConfirmDialog.svelte" - import { - ProgressCircle, - Layout, - Heading, - Body, - Icon, - notifications, - } from "@budibase/bbui" + import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui" import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw" import { findComponent, findComponentPath } from "helpers/components" import { isActive, goto } from "@roxi/routify" + import { ClientAppSkeleton } from "@budibase/frontend-core" let iframe let layout @@ -132,6 +127,7 @@ error = event.error || "An unknown error occurred" } else if (type === "select-component" && "id" in data) { componentStore.select(data.id) + componentTreeNodesStore.makeNodeVisible(data.id) } else if (type === "hover-component") { hoverStore.hover(data.id, false) } else if (type === "update-prop") { @@ -252,8 +248,16 @@
{#if loading} -
- +
+
{:else if error}
@@ -270,8 +274,6 @@ bind:this={iframe} src="/app/preview" class:hidden={loading || error} - class:tablet={$previewStore.previewDevice === "tablet"} - class:mobile={$previewStore.previewDevice === "mobile"} />
diff --git a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte index 5e3a035d89..ed09301bb9 100644 --- a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte +++ b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte @@ -40,16 +40,18 @@ } } + // Handle certain key presses regardless of selection state + if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && $config.canAddRows) { + e.preventDefault() + dispatch("add-row-inline") + return + } + // If nothing selected avoid processing further key presses if (!$focusedCellId) { if (e.key === "Tab" || e.key?.startsWith("Arrow")) { e.preventDefault() focusFirstCell() - } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { - if ($config.canAddRows) { - e.preventDefault() - dispatch("add-row-inline") - } } else if (e.key === "Delete" || e.key === "Backspace") { if (Object.keys($selectedRows).length && $config.canDeleteRows) { dispatch("request-bulk-delete") diff --git a/packages/frontend-core/src/components/index.js b/packages/frontend-core/src/components/index.js index f724e1e4d9..f71420b12b 100644 --- a/packages/frontend-core/src/components/index.js +++ b/packages/frontend-core/src/components/index.js @@ -5,3 +5,4 @@ export { default as UserAvatar } from "./UserAvatar.svelte" export { default as UserAvatars } from "./UserAvatars.svelte" export { default as Updating } from "./Updating.svelte" export { Grid } from "./grid" +export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte" diff --git a/packages/frontend-core/src/constants.js b/packages/frontend-core/src/constants.js index edfdb1c5ef..68da439195 100644 --- a/packages/frontend-core/src/constants.js +++ b/packages/frontend-core/src/constants.js @@ -3,6 +3,7 @@ */ export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core" export { Feature as Features } from "@budibase/types" +import { BpmCorrelationKey } from "@budibase/shared-core" // Cookie names export const Cookies = { @@ -10,6 +11,7 @@ export const Cookies = { CurrentApp: "budibase:currentapp", ReturnUrl: "budibase:returnurl", AccountReturnUrl: "budibase:account:returnurl", + OnboardingProcessCorrelationKey: BpmCorrelationKey.ONBOARDING, } // Table names diff --git a/packages/frontend-core/src/themes/midnight.css b/packages/frontend-core/src/themes/midnight.css index e311452262..501bc164bc 100644 --- a/packages/frontend-core/src/themes/midnight.css +++ b/packages/frontend-core/src/themes/midnight.css @@ -18,4 +18,3 @@ --drop-shadow: rgba(0, 0, 0, 0.25) !important; --spectrum-global-color-blue-100: rgba(35, 40, 50) !important; } - diff --git a/packages/frontend-core/src/utils/index.js b/packages/frontend-core/src/utils/index.js index 98998b7f0e..6b79e1d040 100644 --- a/packages/frontend-core/src/utils/index.js +++ b/packages/frontend-core/src/utils/index.js @@ -7,3 +7,4 @@ export * as RowUtils from "./rows" export { memo, derivedMemo } from "./memo" export { createWebsocket } from "./websocket" export * from "./download" +export * from "./theme" diff --git a/packages/frontend-core/src/utils/theme.js b/packages/frontend-core/src/utils/theme.js new file mode 100644 index 0000000000..165f7c9782 --- /dev/null +++ b/packages/frontend-core/src/utils/theme.js @@ -0,0 +1,12 @@ +import { Themes } from "../constants.js" + +export const getBaseTheme = theme => { + if (!theme) { + return "" + } + let base = Themes.find(x => `spectrum--${x.class}` === theme)?.base || "" + if (base) { + base = `spectrum--${base}` + } + return base +} diff --git a/packages/pro b/packages/pro index 65ac3fc8a2..6b62505be0 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 65ac3fc8a20a5244fbe47629cf79678db2d9ae8a +Subproject commit 6b62505be0c0b50a57b4f4980d86541ebdc86428 diff --git a/packages/server/.vscode/launch.json b/packages/server/.vscode/launch.json deleted file mode 100644 index a07cf96b80..0000000000 --- a/packages/server/.vscode/launch.json +++ /dev/null @@ -1,142 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Start Server", - "type": "node", - "request": "launch", - "runtimeExecutable": "node", - "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], - "args": ["src/index.ts"], - "cwd": "${workspaceRoot}", - }, - { - "type": "node", - "request": "launch", - "name": "Jest - All", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": [], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest - Users", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["user.spec", "--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest - Instances", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["instance.spec", "--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest - Roles", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["role.spec", "--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest - Records", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["record.spec", "--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest - Models", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["table.spec", "--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest - Views", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["view.spec", "--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest - Applications", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["application.spec", "--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest Builder", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["builder", "--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Initialise Budibase", - "program": "yarn", - "args": ["run", "initialise"], - "console": "externalTerminal" - } - ] -} diff --git a/packages/server/__mocks__/@google-cloud/firestore.ts b/packages/server/__mocks__/@google-cloud/firestore.ts index a438d6a7c5..a5bccb1fa8 100644 --- a/packages/server/__mocks__/@google-cloud/firestore.ts +++ b/packages/server/__mocks__/@google-cloud/firestore.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars module FirebaseMock { const firebase: any = {} diff --git a/packages/server/__mocks__/@sendgrid/mail.ts b/packages/server/__mocks__/@sendgrid/mail.ts index 030a2dbd4c..8613ae4b16 100644 --- a/packages/server/__mocks__/@sendgrid/mail.ts +++ b/packages/server/__mocks__/@sendgrid/mail.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars module SendgridMock { class Email { constructor() { diff --git a/packages/server/__mocks__/airtable.ts b/packages/server/__mocks__/airtable.ts index ee4b38ffcd..fed30ca069 100644 --- a/packages/server/__mocks__/airtable.ts +++ b/packages/server/__mocks__/airtable.ts @@ -1,8 +1,5 @@ -module AirtableMock { - function Airtable() { - // @ts-ignore - this.base = jest.fn() - } - - module.exports = Airtable +class Airtable { + base = jest.fn() } + +module.exports = Airtable diff --git a/packages/server/__mocks__/arangojs.ts b/packages/server/__mocks__/arangojs.ts index 5f980a7f97..77ec7a9d42 100644 --- a/packages/server/__mocks__/arangojs.ts +++ b/packages/server/__mocks__/arangojs.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars module ArangoMock { const arangodb: any = {} diff --git a/packages/server/__mocks__/aws-sdk.ts b/packages/server/__mocks__/aws-sdk.ts index 3cf4bba007..d6d33f6c46 100644 --- a/packages/server/__mocks__/aws-sdk.ts +++ b/packages/server/__mocks__/aws-sdk.ts @@ -1,102 +1,81 @@ import fs from "fs" import { join } from "path" -module AwsMock { - const aws: any = {} +const response = (body: any, extra?: any) => () => ({ + promise: () => body, + ...extra, +}) - const response = (body: any, extra?: any) => () => ({ - promise: () => body, - ...extra, - }) - - function DocumentClient() { - // @ts-ignore - this.put = jest.fn(response({})) - // @ts-ignore - this.query = jest.fn( - response({ - Items: [], - }) - ) - // @ts-ignore - this.scan = jest.fn( - response({ - Items: [ - { - Name: "test", - }, - ], - }) - ) - // @ts-ignore - this.get = jest.fn(response({})) - // @ts-ignore - this.update = jest.fn(response({})) - // @ts-ignore - this.delete = jest.fn(response({})) - } - - function S3() { - // @ts-ignore - this.listObjects = jest.fn( - response({ - Contents: [], - }) - ) - - // @ts-ignore - this.createBucket = jest.fn( - response({ - Contents: {}, - }) - ) - - // @ts-ignore - this.deleteObjects = jest.fn( - response({ - Contents: {}, - }) - ) - - // @ts-ignore - this.getSignedUrl = (operation, params) => { - return `http://example.com/${params.Bucket}/${params.Key}` - } - - // @ts-ignore - this.headBucket = jest.fn( - response({ - Contents: {}, - }) - ) - - // @ts-ignore - this.upload = jest.fn( - response({ - Contents: {}, - }) - ) - - // @ts-ignore - this.getObject = jest.fn( - response( +class DocumentClient { + put = jest.fn(response({})) + query = jest.fn( + response({ + Items: [], + }) + ) + scan = jest.fn( + response({ + Items: [ { - Body: "", + Name: "test", }, - { - createReadStream: jest - .fn() - .mockReturnValue( - fs.createReadStream(join(__dirname, "aws-sdk.ts")) - ), - } - ) - ) - } - - aws.DynamoDB = { DocumentClient } - aws.S3 = S3 - aws.config = { update: jest.fn() } - - module.exports = aws + ], + }) + ) + get = jest.fn(response({})) + update = jest.fn(response({})) + delete = jest.fn(response({})) +} + +class S3 { + listObjects = jest.fn( + response({ + Contents: [], + }) + ) + createBucket = jest.fn( + response({ + Contents: {}, + }) + ) + deleteObjects = jest.fn( + response({ + Contents: {}, + }) + ) + getSignedUrl = jest.fn((operation, params) => { + return `http://example.com/${params.Bucket}/${params.Key}` + }) + headBucket = jest.fn( + response({ + Contents: {}, + }) + ) + upload = jest.fn( + response({ + Contents: {}, + }) + ) + getObject = jest.fn( + response( + { + Body: "", + }, + { + createReadStream: jest + .fn() + .mockReturnValue(fs.createReadStream(join(__dirname, "aws-sdk.ts"))), + } + ) + ) +} + +module.exports = { + DynamoDB: { + DocumentClient, + }, + S3, + config: { + update: jest.fn(), + }, } diff --git a/packages/server/__mocks__/mongodb.ts b/packages/server/__mocks__/mongodb.ts deleted file mode 100644 index 01b6e76fc4..0000000000 --- a/packages/server/__mocks__/mongodb.ts +++ /dev/null @@ -1,39 +0,0 @@ -module MongoMock { - const mongodb: any = {} - - mongodb.MongoClient = function () { - this.connect = jest.fn() - this.close = jest.fn() - this.insertOne = jest.fn() - this.insertMany = jest.fn(() => ({ toArray: () => [] })) - this.find = jest.fn(() => ({ toArray: () => [] })) - this.findOne = jest.fn() - this.findOneAndUpdate = jest.fn() - this.count = jest.fn() - this.deleteOne = jest.fn() - this.deleteMany = jest.fn(() => ({ toArray: () => [] })) - this.updateOne = jest.fn() - this.updateMany = jest.fn(() => ({ toArray: () => [] })) - - this.collection = jest.fn(() => ({ - insertOne: this.insertOne, - find: this.find, - insertMany: this.insertMany, - findOne: this.findOne, - findOneAndUpdate: this.findOneAndUpdate, - count: this.count, - deleteOne: this.deleteOne, - deleteMany: this.deleteMany, - updateOne: this.updateOne, - updateMany: this.updateMany, - })) - - this.db = () => ({ - collection: this.collection, - }) - } - - mongodb.ObjectId = jest.requireActual("mongodb").ObjectId - - module.exports = mongodb -} diff --git a/packages/server/__mocks__/mssql.ts b/packages/server/__mocks__/mssql.ts deleted file mode 100644 index cdfb7d520e..0000000000 --- a/packages/server/__mocks__/mssql.ts +++ /dev/null @@ -1,24 +0,0 @@ -module MsSqlMock { - const mssql: any = {} - - mssql.query = jest.fn(() => ({ - recordset: [ - { - a: "string", - b: 1, - }, - ], - })) - - // mssql.connect = jest.fn(() => ({ recordset: [] })) - - mssql.ConnectionPool = jest.fn(() => ({ - connect: jest.fn(() => ({ - request: jest.fn(() => ({ - query: jest.fn(sql => ({ recordset: [sql] })), - })), - })), - })) - - module.exports = mssql -} diff --git a/packages/server/__mocks__/mysql2.ts b/packages/server/__mocks__/mysql2.ts deleted file mode 100644 index 9d42a35dd8..0000000000 --- a/packages/server/__mocks__/mysql2.ts +++ /dev/null @@ -1,14 +0,0 @@ -module MySQLMock { - const mysql: any = {} - - const client = { - connect: jest.fn(), - query: jest.fn((query, bindings, fn) => { - fn(null, []) - }), - } - - mysql.createConnection = jest.fn(() => client) - - module.exports = mysql -} diff --git a/packages/server/__mocks__/mysql2/promise.ts b/packages/server/__mocks__/mysql2/promise.ts deleted file mode 100644 index 8a8fb7fcf0..0000000000 --- a/packages/server/__mocks__/mysql2/promise.ts +++ /dev/null @@ -1,17 +0,0 @@ -module MySQLMock { - const mysql: any = {} - - const client = { - connect: jest.fn(), - end: jest.fn(), - query: jest.fn(async () => { - return [[]] - }), - } - - mysql.createConnection = jest.fn(async () => { - return client - }) - - module.exports = mysql -} diff --git a/packages/server/__mocks__/node-fetch.ts b/packages/server/__mocks__/node-fetch.ts index 98c75bb84f..c5073499a7 100644 --- a/packages/server/__mocks__/node-fetch.ts +++ b/packages/server/__mocks__/node-fetch.ts @@ -1,6 +1,7 @@ // @ts-ignore import fs from "fs" +// eslint-disable-next-line @typescript-eslint/no-unused-vars module FetchMock { // @ts-ignore const fetch = jest.requireActual("node-fetch") diff --git a/packages/server/__mocks__/oracledb.ts b/packages/server/__mocks__/oracledb.ts index fd19845eee..0172ace0e6 100644 --- a/packages/server/__mocks__/oracledb.ts +++ b/packages/server/__mocks__/oracledb.ts @@ -1,31 +1,21 @@ -module OracleDbMock { - // mock execute - const execute = jest.fn(() => ({ - rows: [ - { - a: "string", - b: 1, - }, - ], - })) +const executeMock = jest.fn(() => ({ + rows: [ + { + a: "string", + b: 1, + }, + ], +})) - const close = jest.fn() +const closeMock = jest.fn() - // mock connection - function Connection() {} - Connection.prototype.execute = execute - Connection.prototype.close = close - - // mock oracledb - const oracleDb: any = {} - oracleDb.getConnection = jest.fn(() => { - // @ts-ignore - return new Connection() - }) - - // expose mocks - oracleDb.executeMock = execute - oracleDb.closeMock = close - - module.exports = oracleDb +class Connection { + execute = executeMock + close = closeMock +} + +module.exports = { + getConnection: jest.fn(() => new Connection()), + executeMock, + closeMock, } diff --git a/packages/server/__mocks__/pg.ts b/packages/server/__mocks__/pg.ts index 110933ad52..50a7c7349e 100644 --- a/packages/server/__mocks__/pg.ts +++ b/packages/server/__mocks__/pg.ts @@ -1,30 +1,25 @@ -module PgMock { - const pg: any = {} +const query = jest.fn(() => ({ + rows: [ + { + a: "string", + b: 1, + }, + ], +})) - const query = jest.fn(() => ({ - rows: [ - { - a: "string", - b: 1, - }, - ], - })) - - // constructor - function Client() {} - - Client.prototype.query = query - Client.prototype.end = jest.fn(cb => { +class Client { + query = query + end = jest.fn(cb => { if (cb) cb() }) - Client.prototype.connect = jest.fn() - Client.prototype.release = jest.fn() - - const on = jest.fn() - - pg.Client = Client - pg.queryMock = query - pg.on = on - - module.exports = pg + connect = jest.fn() + release = jest.fn() +} + +const on = jest.fn() + +module.exports = { + Client, + queryMock: query, + on, } diff --git a/packages/server/jest-testcontainers-config.js b/packages/server/jest-testcontainers-config.js deleted file mode 100644 index 8ac0f0cd9d..0000000000 --- a/packages/server/jest-testcontainers-config.js +++ /dev/null @@ -1,8 +0,0 @@ -const { join } = require("path") -require("dotenv").config({ - path: join(__dirname, "..", "..", "hosting", ".env"), -}) - -const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator") - -module.exports = jestTestcontainersConfigGenerator() diff --git a/packages/server/jest.config.ts b/packages/server/jest.config.ts index 6c6d6a20d3..85c75f9039 100644 --- a/packages/server/jest.config.ts +++ b/packages/server/jest.config.ts @@ -4,7 +4,6 @@ import * as fs from "fs" import { join } from "path" const baseConfig: Config.InitialProjectOptions = { - preset: "@trendyol/jest-testcontainers", setupFiles: ["./src/tests/jestEnv.ts"], moduleFileExtensions: [ "js", @@ -18,6 +17,7 @@ const baseConfig: Config.InitialProjectOptions = { "svelte", ], setupFilesAfterEnv: ["./src/tests/jestSetup.ts"], + globalSetup: "./../../globalSetup.ts", transform: { "^.+\\.ts?$": "@swc/jest", "^.+\\.js?$": "@swc/jest", diff --git a/packages/server/package.json b/packages/server/package.json index 8e66f3d7e0..da99ff6dea 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -53,6 +53,7 @@ "@budibase/pro": "0.0.0", "@budibase/shared-core": "0.0.0", "@budibase/string-templates": "0.0.0", + "@budibase/frontend-core": "0.0.0", "@budibase/types": "0.0.0", "@bull-board/api": "5.10.2", "@bull-board/koa": "5.10.2", @@ -121,7 +122,6 @@ "@babel/preset-env": "7.16.11", "@swc/core": "1.3.71", "@swc/jest": "0.2.27", - "@trendyol/jest-testcontainers": "2.1.1", "@types/global-agent": "2.1.1", "@types/google-spreadsheet": "3.1.5", "@types/jest": "29.5.5", diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index 8e62954d88..ceef421fab 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -26,7 +26,6 @@ import { env as envCore, ErrorCode, events, - HTTPError, migrations, objectStore, roles, @@ -307,6 +306,7 @@ async function performAppCreate(ctx: UserCtx) { features: { componentValidation: true, disableUserMetadata: true, + skeletonLoader: true, }, } @@ -487,10 +487,11 @@ export async function updateClient(ctx: UserCtx) { const application = await db.get(DocumentType.APP_METADATA) const currentVersion = application.version + let manifest // Update client library and manifest if (!env.isTest()) { await backupClientLibrary(ctx.params.appId) - await updateClientLibrary(ctx.params.appId) + manifest = await updateClientLibrary(ctx.params.appId) } // Update versions in app package @@ -498,6 +499,10 @@ export async function updateClient(ctx: UserCtx) { const appPackageUpdates = { version: updatedToVersion, revertableVersion: currentVersion, + features: { + ...(application.features ?? {}), + skeletonLoader: manifest?.features?.skeletonLoader ?? false, + }, } const app = await updateAppPackage(appPackageUpdates, ctx.params.appId) await events.app.versionUpdated(app, currentVersion, updatedToVersion) @@ -513,9 +518,10 @@ export async function revertClient(ctx: UserCtx) { ctx.throw(400, "There is no version to revert to") } + let manifest // Update client library and manifest if (!env.isTest()) { - await revertClientLibrary(ctx.params.appId) + manifest = await revertClientLibrary(ctx.params.appId) } // Update versions in app package @@ -524,6 +530,10 @@ export async function revertClient(ctx: UserCtx) { const appPackageUpdates = { version: revertedToVersion, revertableVersion: undefined, + features: { + ...(application.features ?? {}), + skeletonLoader: manifest?.features?.skeletonLoader ?? false, + }, } const app = await updateAppPackage(appPackageUpdates, ctx.params.appId) await events.app.versionReverted(app, currentVersion, revertedToVersion) @@ -730,6 +740,21 @@ export async function updateAppPackage( }) } +export async function setRevertableVersion( + ctx: UserCtx<{ revertableVersion: string }, App> +) { + if (!env.isDev()) { + ctx.status = 403 + return + } + const db = context.getAppDB() + const app = await db.get(DocumentType.APP_METADATA) + app.revertableVersion = ctx.request.body.revertableVersion + await db.put(app) + + ctx.status = 200 +} + async function migrateAppNavigation() { const db = context.getAppDB() const existing: App = await db.get(DocumentType.APP_METADATA) diff --git a/packages/server/src/api/controllers/plugin/index.ts b/packages/server/src/api/controllers/plugin/index.ts index a8a451c98c..c7d4912db3 100644 --- a/packages/server/src/api/controllers/plugin/index.ts +++ b/packages/server/src/api/controllers/plugin/index.ts @@ -39,25 +39,28 @@ export async function create(ctx: any) { let name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000) switch (source) { - case PluginSource.NPM: + case PluginSource.NPM: { const { metadata: metadataNpm, directory: directoryNpm } = await npmUpload(url, name) metadata = metadataNpm directory = directoryNpm break - case PluginSource.GITHUB: + } + case PluginSource.GITHUB: { const { metadata: metadataGithub, directory: directoryGithub } = await githubUpload(url, name, githubToken) metadata = metadataGithub directory = directoryGithub break - case PluginSource.URL: + } + case PluginSource.URL: { const headersObj = headers || {} const { metadata: metadataUrl, directory: directoryUrl } = await urlUpload(url, name, headersObj) metadata = metadataUrl directory = directoryUrl break + } } pluginCore.validate(metadata?.schema) diff --git a/packages/server/src/api/controllers/query/import/sources/openapi2.ts b/packages/server/src/api/controllers/query/import/sources/openapi2.ts index 230647475e..6eb4766c70 100644 --- a/packages/server/src/api/controllers/query/import/sources/openapi2.ts +++ b/packages/server/src/api/controllers/query/import/sources/openapi2.ts @@ -109,13 +109,14 @@ export class OpenAPI2 extends OpenAPISource { for (let param of allParams) { if (parameterNotRef(param)) { switch (param.in) { - case "query": + case "query": { let prefix = "" if (queryString) { prefix = "&" } queryString = `${queryString}${prefix}${param.name}={{${param.name}}}` break + } case "header": headers[param.name] = `{{${param.name}}}` break @@ -125,7 +126,7 @@ export class OpenAPI2 extends OpenAPISource { case "formData": // future enhancement break - case "body": + case "body": { // set the request body to the example provided // future enhancement: generate an example from the schema let bodyParam: OpenAPIV2.InBodyParameterObject = @@ -135,6 +136,7 @@ export class OpenAPI2 extends OpenAPISource { requestBody = schema.example } break + } } // add the parameter if it can be bound in our config diff --git a/packages/server/src/api/controllers/query/import/sources/openapi3.ts b/packages/server/src/api/controllers/query/import/sources/openapi3.ts index f86f684c32..f6755c69ad 100644 --- a/packages/server/src/api/controllers/query/import/sources/openapi3.ts +++ b/packages/server/src/api/controllers/query/import/sources/openapi3.ts @@ -161,13 +161,14 @@ export class OpenAPI3 extends OpenAPISource { for (let param of allParams) { if (parameterNotRef(param)) { switch (param.in) { - case "query": + case "query": { let prefix = "" if (queryString) { prefix = "&" } queryString = `${queryString}${prefix}${param.name}={{${param.name}}}` break + } case "header": headers[param.name] = `{{${param.name}}}` break diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts index 3c21537484..055f3bd888 100644 --- a/packages/server/src/api/controllers/query/index.ts +++ b/packages/server/src/api/controllers/query/index.ts @@ -6,7 +6,7 @@ import { invalidateDynamicVariables } from "../../../threads/utils" import env from "../../../environment" import { events, context, utils, constants } from "@budibase/backend-core" import sdk from "../../../sdk" -import { QueryEvent } from "../../../threads/definitions" +import { QueryEvent, QueryEventParameters } from "../../../threads/definitions" import { ConfigType, Query, @@ -14,22 +14,34 @@ import { SessionCookie, JsonFieldSubType, QueryResponse, - QueryPreview, QuerySchema, FieldType, ExecuteQueryRequest, ExecuteQueryResponse, - Row, - QueryParameter, PreviewQueryRequest, PreviewQueryResponse, } from "@budibase/types" import { ValidQueryNameRegex, utils as JsonUtils } from "@budibase/shared-core" +import { findHBSBlocks } from "@budibase/string-templates" const Runner = new Thread(ThreadType.QUERY, { timeoutMs: env.QUERY_THREAD_TIMEOUT, }) +function validateQueryInputs(parameters: QueryEventParameters) { + for (let entry of Object.entries(parameters)) { + const [key, value] = entry + if (typeof value !== "string") { + continue + } + if (findHBSBlocks(value).length !== 0) { + throw new Error( + `Parameter '${key}' input contains a handlebars binding - this is not allowed.` + ) + } + } +} + export async function fetch(ctx: UserCtx) { ctx.body = await sdk.queries.fetch() } @@ -87,10 +99,18 @@ export async function save(ctx: UserCtx) { const datasource = await sdk.datasources.get(query.datasourceId) let eventFn - if (!query._id) { + if (!query._id && !query._rev) { query._id = generateQueryID(query.datasourceId) + // flag to state whether the default bindings are empty strings (old behaviour) or null + query.nullDefaultSupport = true eventFn = () => events.query.created(datasource, query) } else { + // check if flag has previously been set, don't let it change + // allow it to be explicitly set to false via API incase this is ever needed + const existingQuery = await db.get(query._id) + if (existingQuery.nullDefaultSupport && query.nullDefaultSupport == null) { + query.nullDefaultSupport = true + } eventFn = () => events.query.updated(datasource, query) } const response = await db.put(query) @@ -122,16 +142,20 @@ function getAuthConfig(ctx: UserCtx) { } function enrichParameters( - queryParameters: QueryParameter[], - requestParameters: { [key: string]: string } = {} -): { - [key: string]: string -} { + query: Query, + requestParameters: QueryEventParameters = {} +): QueryEventParameters { + const paramNotSet = (val: unknown) => val === "" || val == undefined + // first check parameters are all valid + validateQueryInputs(requestParameters) // make sure parameters are fully enriched with defaults - for (let parameter of queryParameters) { - if (!requestParameters[parameter.name]) { - requestParameters[parameter.name] = parameter.default + for (const parameter of query.parameters) { + let value: string | null = + requestParameters[parameter.name] || parameter.default + if (query.nullDefaultSupport && paramNotSet(value)) { + value = null } + requestParameters[parameter.name] = value } return requestParameters } @@ -144,10 +168,15 @@ export async function preview( ) // preview may not have a queryId as it hasn't been saved, but if it does // this stops dynamic variables from calling the same query - const { fields, parameters, queryVerb, transformer, queryId, schema } = - ctx.request.body + const queryId = ctx.request.body.queryId + // the body contains the makings of a query, which has not been saved yet + const query: Query = ctx.request.body + // hasn't been saved, new query + if (!queryId && !query._id) { + query.nullDefaultSupport = true + } - let existingSchema = schema + let existingSchema = query.schema if (queryId && !existingSchema) { try { const db = context.getAppDB() @@ -255,13 +284,14 @@ export async function preview( try { const inputs: QueryEvent = { appId: ctx.appId, - datasource, - queryVerb, - fields, - parameters: enrichParameters(parameters), - transformer, + queryVerb: query.queryVerb, + fields: query.fields, + parameters: enrichParameters(query), + transformer: query.transformer, + schema: query.schema, + nullDefaultSupport: query.nullDefaultSupport, queryId, - schema, + datasource, // have to pass down to the thread runner - can't put into context now environmentVariables: envVars, ctx: { @@ -323,14 +353,12 @@ async function execute( queryVerb: query.queryVerb, fields: query.fields, pagination: ctx.request.body.pagination, - parameters: enrichParameters( - query.parameters, - ctx.request.body.parameters - ), + parameters: enrichParameters(query, ctx.request.body.parameters), transformer: query.transformer, queryId: ctx.params.queryId, // have to pass down to the thread runner - can't put into context now environmentVariables: envVars, + nullDefaultSupport: query.nullDefaultSupport, ctx: { user: ctx.user, auth: { ...authConfigCtx }, diff --git a/packages/server/src/api/controllers/role.ts b/packages/server/src/api/controllers/role.ts index 2f5340d2e6..3398c8102c 100644 --- a/packages/server/src/api/controllers/role.ts +++ b/packages/server/src/api/controllers/role.ts @@ -116,7 +116,7 @@ export async function save(ctx: UserCtx) { target: prodDb.name, }) await replication.replicate({ - filter: (doc: any, params: any) => { + filter: (doc: any) => { return doc._id && doc._id.startsWith("role_") }, }) diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index 814b57567f..f89c9dc51a 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -7,13 +7,11 @@ import { FilterType, IncludeRelationship, ManyToManyRelationshipFieldMetadata, - ManyToOneRelationshipFieldMetadata, OneToManyRelationshipFieldMetadata, Operation, PaginationJson, RelationshipFieldMetadata, RelationshipsJson, - RelationshipType, Row, SearchFilters, SortJson, @@ -717,7 +715,7 @@ export class ExternalRequest { const rows = related[key]?.rows || [] - function relationshipMatchPredicate({ + const relationshipMatchPredicate = ({ row, linkPrimary, linkSecondary, @@ -725,7 +723,7 @@ export class ExternalRequest { row: Row linkPrimary: string linkSecondary?: string - }) { + }) => { const matchesPrimaryLink = row[linkPrimary] === relationship.id || row[linkPrimary] === body?.[linkPrimary] diff --git a/packages/server/src/api/controllers/row/alias.ts b/packages/server/src/api/controllers/row/alias.ts index 1d586c54fd..60c207c8ce 100644 --- a/packages/server/src/api/controllers/row/alias.ts +++ b/packages/server/src/api/controllers/row/alias.ts @@ -23,6 +23,12 @@ const DISABLED_WRITE_CLIENTS: SqlClient[] = [ SqlClient.ORACLE, ] +const DISABLED_OPERATIONS: Operation[] = [ + Operation.CREATE_TABLE, + Operation.UPDATE_TABLE, + Operation.DELETE_TABLE, +] + class CharSequence { static alphabet = "abcdefghijklmnopqrstuvwxyz" counters: number[] @@ -59,13 +65,18 @@ export default class AliasTables { } isAliasingEnabled(json: QueryJson, datasource: Datasource) { + const operation = json.endpoint.operation const fieldLength = json.resource?.fields?.length - if (!fieldLength || fieldLength <= 0) { + if ( + !fieldLength || + fieldLength <= 0 || + DISABLED_OPERATIONS.includes(operation) + ) { return false } try { const sqlClient = getSQLClient(datasource) - const isWrite = WRITE_OPERATIONS.includes(json.endpoint.operation) + const isWrite = WRITE_OPERATIONS.includes(operation) const isDisabledClient = DISABLED_WRITE_CLIENTS.includes(sqlClient) if (isWrite && isDisabledClient) { return false diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index 188fe86f17..2c6cb4b17a 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -1,4 +1,3 @@ -import { quotas } from "@budibase/pro" import { UserCtx, ViewV2, diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts index c718d5f704..f004921d08 100644 --- a/packages/server/src/api/controllers/static/index.ts +++ b/packages/server/src/api/controllers/static/index.ts @@ -1,10 +1,8 @@ import { InvalidFileExtensions } from "@budibase/shared-core" - import AppComponent from "./templates/BudibaseApp.svelte" - import { join } from "../../../utilities/centralPath" import * as uuid from "uuid" -import { ObjectStoreBuckets } from "../../../constants" +import { ObjectStoreBuckets, devClientVersion } from "../../../constants" import { processString } from "@budibase/string-templates" import { loadHandlebarsFile, @@ -24,13 +22,20 @@ import AWS from "aws-sdk" import fs from "fs" import sdk from "../../../sdk" import * as pro from "@budibase/pro" -import { App, Ctx, ProcessAttachmentResponse } from "@budibase/types" +import { + UserCtx, + App, + Ctx, + ProcessAttachmentResponse, + Feature, +} from "@budibase/types" import { getAppMigrationVersion, getLatestMigrationId, } from "../../../appMigrations" import send from "koa-send" +import { getThemeVariables } from "../../../constants/themes" export const toggleBetaUiFeature = async function (ctx: Ctx) { const cookieName = `beta:${ctx.params.feature}` @@ -146,7 +151,7 @@ const requiresMigration = async (ctx: Ctx) => { return requiresMigrations } -export const serveApp = async function (ctx: Ctx) { +export const serveApp = async function (ctx: UserCtx) { const needMigrations = await requiresMigration(ctx) const bbHeaderEmbed = @@ -165,12 +170,23 @@ export const serveApp = async function (ctx: Ctx) { try { db = context.getAppDB({ skip_setup: true }) const appInfo = await db.get(DocumentType.APP_METADATA) + let appId = context.getAppId() + const hideDevTools = !!ctx.params.appUrl + const sideNav = appInfo.navigation.navigation === "Left" + const hideFooter = + ctx?.user?.license?.features?.includes(Feature.BRANDING) || false + const themeVariables = getThemeVariables(appInfo?.theme) if (!env.isJest()) { const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins) + const { head, html, css } = AppComponent.render({ title: branding?.platformTitle || `${appInfo.name}`, + showSkeletonLoader: appInfo.features?.skeletonLoader ?? false, + hideDevTools, + sideNav, + hideFooter, metaImage: branding?.metaImageUrl || "https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png", @@ -195,7 +211,7 @@ export const serveApp = async function (ctx: Ctx) { ctx.body = await processString(appHbs, { head, body: html, - style: css.code, + css: `:root{${themeVariables}} ${css.code}`, appId, embedded: bbHeaderEmbed, }) @@ -247,18 +263,20 @@ export const serveBuilderPreview = async function (ctx: Ctx) { } export const serveClientLibrary = async function (ctx: Ctx) { + const version = ctx.request.query.version + const appId = context.getAppId() || (ctx.request.query.appId as string) let rootPath = join(NODE_MODULES_PATH, "@budibase", "client", "dist") if (!appId) { ctx.throw(400, "No app ID provided - cannot fetch client library.") } - if (env.isProd()) { + if (env.isProd() || (env.isDev() && version !== devClientVersion)) { ctx.body = await objectStore.getReadStream( ObjectStoreBuckets.APPS, objectStore.clientLibraryPath(appId!) ) ctx.set("Content-Type", "application/javascript") - } else if (env.isDev()) { + } else if (env.isDev() && version === devClientVersion) { // incase running from TS directly const tsPath = join(require.resolve("@budibase/client"), "..") return send(ctx, "budibase-client.js", { diff --git a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte index 7819368fc0..b4bfbe6660 100644 --- a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte +++ b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte @@ -1,4 +1,6 @@ @@ -96,6 +103,9 @@ + {#if showSkeletonLoader} + + {/if}
{#if clientLibPath}

There was an error loading your app

diff --git a/packages/server/src/api/controllers/static/templates/app.hbs b/packages/server/src/api/controllers/static/templates/app.hbs index 8c445158a0..b01b723c3e 100644 --- a/packages/server/src/api/controllers/static/templates/app.hbs +++ b/packages/server/src/api/controllers/static/templates/app.hbs @@ -1,8 +1,12 @@ - + {{{head}}} - +