diff --git a/.eslintignore b/.eslintignore index 94984a446f..2bc00912d2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,8 +9,5 @@ packages/server/client packages/server/coverage packages/builder/.routify packages/sdk/sdk -packages/account-portal/packages/server/build -packages/account-portal/packages/ui/.routify -packages/account-portal/packages/ui/build **/*.ivm.bundle.js packages/server/build/oldClientVersions/**/** diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 2d725bf28a..1258bddcca 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -64,18 +64,15 @@ jobs: - run: yarn --frozen-lockfile # Run build all the projects - - name: Build OSS - run: yarn build:oss - - name: Build account portal - run: yarn build:account-portal - if: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }} + - name: Build + run: yarn build # Check the types of the projects built via esbuild - name: Check types run: | if ${{ env.ONLY_AFFECTED_TASKS }}; then - yarn check:types --since=${{ env.NX_BASE_BRANCH }} --ignore @budibase/account-portal-server + yarn check:types --since=${{ env.NX_BASE_BRANCH }} else - yarn check:types --ignore @budibase/account-portal-server + yarn check:types fi helm-lint: @@ -117,9 +114,11 @@ jobs: - name: Test run: | if ${{ env.ONLY_AFFECTED_TASKS }}; then - yarn test --ignore=@budibase/worker --ignore=@budibase/server --since=${{ env.NX_BASE_BRANCH }} + yarn test -- --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/builder --no-prefix --since=${{ env.NX_BASE_BRANCH }} -- --verbose --reporters=default --reporters=github-actions + yarn test -- --scope=@budibase/builder --since=${{ env.NX_BASE_BRANCH }} else - yarn test --ignore=@budibase/worker --ignore=@budibase/server + yarn test -- --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/builder --no-prefix -- --verbose --reporters=default --reporters=github-actions + yarn test -- --scope=@budibase/builder --no-prefix fi test-worker: @@ -141,13 +140,22 @@ jobs: - name: Test worker run: | if ${{ env.ONLY_AFFECTED_TASKS }}; then - node scripts/run-affected.js --task=test --scope=@budibase/worker --since=${{ env.NX_BASE_BRANCH }} - else - yarn test --scope=@budibase/worker + AFFECTED=$(yarn --silent nx show projects --affected -t test --base=${{ env.NX_BASE_BRANCH }} -p @budibase/worker) + if [ -z "$AFFECTED" ]; then + echo "No affected tests to run" + exit 0 + fi fi + cd packages/worker + yarn test --verbose --reporters=default --reporters=github-actions + test-server: - runs-on: budi-tubby-tornado-quad-core-300gb + runs-on: ubuntu-latest + strategy: + matrix: + datasource: + [mssql, mysql, postgres, mongodb, mariadb, oracle, sqs, none] steps: - name: Checkout repo uses: actions/checkout@v4 @@ -170,12 +178,19 @@ jobs: - name: Pull testcontainers images run: | - docker pull mcr.microsoft.com/mssql/server@${{ steps.dotenv.outputs.MSSQL_SHA }} & - docker pull mysql@${{ steps.dotenv.outputs.MYSQL_SHA }} & - docker pull postgres@${{ steps.dotenv.outputs.POSTGRES_SHA }} & - docker pull mongo@${{ steps.dotenv.outputs.MONGODB_SHA }} & - docker pull mariadb@${{ steps.dotenv.outputs.MARIADB_SHA }} & - docker pull budibase/oracle-database:23.2-slim-faststart & + if [ "${{ matrix.datasource }}" == "mssql" ]; then + docker pull mcr.microsoft.com/mssql/server@${{ steps.dotenv.outputs.MSSQL_SHA }} + elif [ "${{ matrix.datasource }}" == "mysql" ]; then + docker pull mysql@${{ steps.dotenv.outputs.MYSQL_SHA }} + elif [ "${{ matrix.datasource }}" == "postgres" ]; then + docker pull postgres@${{ steps.dotenv.outputs.POSTGRES_SHA }} + elif [ "${{ matrix.datasource }}" == "mongodb" ]; then + docker pull mongo@${{ steps.dotenv.outputs.MONGODB_SHA }} + elif [ "${{ matrix.datasource }}" == "mariadb" ]; then + docker pull mariadb@${{ steps.dotenv.outputs.MARIADB_SHA }} + elif [ "${{ matrix.datasource }}" == "oracle" ]; then + docker pull budibase/oracle-database:23.2-slim-faststart + fi docker pull minio/minio & docker pull redis & docker pull testcontainers/ryuk:0.5.1 & @@ -186,13 +201,25 @@ jobs: - run: yarn --frozen-lockfile - name: Test server + env: + DATASOURCE: ${{ matrix.datasource }} run: | if ${{ env.ONLY_AFFECTED_TASKS }}; then - node scripts/run-affected.js --task=test --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }} - else - yarn test --scope=@budibase/server + AFFECTED=$(yarn --silent nx show projects --affected -t test --base=${{ env.NX_BASE_BRANCH }} -p @budibase/server) + if [ -z "$AFFECTED" ]; then + echo "No affected tests to run" + exit 0 + fi fi + FILTER="./src/tests/filters/datasource-tests.js" + if [ "${{ matrix.datasource }}" == "none" ]; then + FILTER="./src/tests/filters/non-datasource-tests.js" + fi + + cd packages/server + yarn test --filter $FILTER --verbose --reporters=default --reporters=github-actions + check-pro-submodule: runs-on: ubuntu-latest if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') @@ -252,64 +279,6 @@ jobs: echo 'All good, the submodule had been merged and setup correctly!' fi - check-accountportal-submodule: - runs-on: ubuntu-latest - if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') - steps: - - name: Checkout repo and submodules - uses: actions/checkout@v4 - with: - submodules: true - token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} - fetch-depth: 0 - - - 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 - accountportal_commit=$(git rev-parse HEAD) - - branch="${{ github.base_ref || github.ref_name }}" - echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})" - - base_commit=$(git rev-parse origin/master) - - if [[ ! -z $base_commit ]]; then - echo "target_branch=$branch" - echo "target_branch=$branch" >> "$GITHUB_OUTPUT" - echo "accountportal_commit=$accountportal_commit" - echo "accountportal_commit=$accountportal_commit" >> "$GITHUB_OUTPUT" - echo "base_commit=$base_commit" - echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT" - else - echo "Nothing to do - branch to branch merge." - fi - - - name: Check submodule merged to base branch - if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }} - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const submoduleCommit = '${{ steps.get_accountportal_commits.outputs.accountportal_commit }}'; - const baseCommit = '${{ steps.get_accountportal_commits.outputs.base_commit }}'; - - if (submoduleCommit !== baseCommit) { - console.error('Submodule commit does not match the latest commit on the "${{ steps.get_accountportal_commits.outputs.target_branch }}" branch.'); - console.error('Refer to the account portal repo to merge your changes: https://github.com/Budibase/account-portal/blob/master/docs/index.md') - process.exit(1); - } else { - console.log('All good, the submodule had been merged and setup correctly!') - } - check-lockfile: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index bac643e5df..21637edfbe 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ packages/server/build/oldClientVersions/**/* packages/builder/src/components/deploy/clientVersions.json packages/server/src/integrations/tests/utils/*.lock packages/builder/vite.config.mjs.timestamp* +packages/account-portal # Logs logs @@ -110,4 +111,4 @@ budibase-component budibase-datasource *.iml -.nx \ No newline at end of file +.nx diff --git a/.gitmodules b/.gitmodules index cb6d1c5dc8..2dd6ea53f2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "packages/pro"] path = packages/pro url = git@github.com:Budibase/budibase-pro.git -[submodule "packages/account-portal"] - path = packages/account-portal - url = git@github.com:Budibase/account-portal.git diff --git a/.prettierignore b/.prettierignore index 72cdc75a23..b1ee287391 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,8 +9,4 @@ packages/backend-core/coverage packages/builder/.routify packages/sdk/sdk packages/pro/coverage -packages/account-portal/packages/ui/build -packages/account-portal/packages/ui/.routify -packages/account-portal/packages/server/build -packages/account-portal/packages/server/coverage **/*.ivm.bundle.js \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 543d1e6179..2fda61345b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,16 +20,6 @@ "args": ["${workspaceFolder}/packages/worker/src/index.ts"], "cwd": "${workspaceFolder}/packages/worker" }, - { - "name": "Camunda Worker", - "type": "node", - "request": "launch", - "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], - "args": [ - "${workspaceFolder}/packages/account-portal/packages/server/src/v2/run.ts" - ], - "cwd": "${workspaceFolder}/packages/account-portal/packages/server" - }, { "type": "chrome", "request": "launch", diff --git a/examples/nextjs-api-sales/yarn.lock b/examples/nextjs-api-sales/yarn.lock index 9acbdfdeb6..867835a6b7 100644 --- a/examples/nextjs-api-sales/yarn.lock +++ b/examples/nextjs-api-sales/yarn.lock @@ -423,9 +423,9 @@ core-js-pure@^3.20.2: integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" diff --git a/globalSetup.ts b/globalSetup.ts index 5d8b0381c0..07a0cec5e2 100644 --- a/globalSetup.ts +++ b/globalSetup.ts @@ -62,6 +62,7 @@ export default async function setup() { }, ]) .withLabels({ "com.budibase": "true" }) + .withTmpFs({ "/data": "rw" }) .withReuse() .withWaitStrategy( Wait.forSuccessfulCommand( @@ -72,6 +73,7 @@ export default async function setup() { const minio = new GenericContainer("minio/minio") .withExposedPorts(9000) .withCommand(["server", "/data"]) + .withTmpFs({ "/data": "rw" }) .withEnvironment({ MINIO_ACCESS_KEY: "budibase", MINIO_SECRET_KEY: "budibase", diff --git a/hosting/nginx.dev.conf b/hosting/nginx.dev.conf index e3d6d47287..f0a58a9a98 100644 --- a/hosting/nginx.dev.conf +++ b/hosting/nginx.dev.conf @@ -45,20 +45,6 @@ http { client_max_body_size 50000m; ignore_invalid_headers off; proxy_buffering off; - set $csp_default "default-src 'self'"; - set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io https://d2l5prqdbvm3op.cloudfront.net https://us-assets.i.posthog.com"; - set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; - set $csp_object "object-src 'none'"; - set $csp_base_uri "base-uri 'self'"; - set $csp_connect "connect-src 'self' https://*.budibase.app https://*.budibaseqa.app https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com https://us.i.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; - set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; - set $csp_frame "frame-src 'self' https:"; - set $csp_img "img-src http: https: data: blob:"; - set $csp_manifest "manifest-src 'self'"; - set $csp_media "media-src 'self' https://js.intercomcdn.com https://cdn.budi.live"; - set $csp_worker "worker-src blob:"; - - add_header Content-Security-Policy "${csp_default}; ${csp_style}; ${csp_object}; ${csp_base_uri}; ${csp_connect}; ${csp_font}; ${csp_frame}; ${csp_img}; ${csp_manifest}; ${csp_media}; ${csp_worker};" always; error_page 502 503 504 /error.html; location = /error.html { diff --git a/lerna.json b/lerna.json index 582f95b303..dc238bb392 100644 --- a/lerna.json +++ b/lerna.json @@ -1,12 +1,7 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.3", + "version": "3.2.12", "npmClient": "yarn", - "packages": [ - "packages/*", - "!packages/account-portal", - "packages/account-portal/packages/*" - ], "concurrency": 20, "command": { "publish": { diff --git a/nx.json b/nx.json index fb05ea94d0..22b23a7874 100644 --- a/nx.json +++ b/nx.json @@ -2,7 +2,6 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "tasksRunnerOptions": { "default": { - "runner": "nx-cloud", "options": { "cacheableOperations": ["build", "test", "check:types"] } diff --git a/package.json b/package.json index fc7e202e3d..e354f36d2a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@types/node": "20.10.0", "@types/proper-lockfile": "^4.1.4", "@typescript-eslint/parser": "6.9.0", + "depcheck": "^1.4.7", "esbuild": "^0.18.17", "esbuild-node-externals": "^1.14.0", "eslint": "^8.52.0", @@ -24,22 +25,22 @@ "prettier": "2.8.8", "prettier-plugin-svelte": "^2.3.0", "proper-lockfile": "^4.1.2", - "svelte": "^4.2.10", + "svelte": "4.2.19", "svelte-eslint-parser": "^0.33.1", "typescript": "5.5.2", "typescript-eslint": "^7.3.1", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "cross-spawn": "7.0.6" }, "scripts": { "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": "DISABLE_V8_COMPILE_CACHE=1 NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream", "build:apps": "DISABLE_V8_COMPILE_CACHE=1 yarn build --scope @budibase/server --scope @budibase/worker", - "build:oss": "DISABLE_V8_COMPILE_CACHE=1 NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --ignore @budibase/account-portal-server --ignore @budibase/account-portal-ui", "build:cli": "yarn build --scope @budibase/cli", - "build:account-portal": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream --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 --concurrency 2 check:types --ignore @budibase/account-portal-server", + "check:types": "yarn check:dependencies && lerna run --concurrency 2 check:types", + "check:dependencies": "lerna run --concurrency 2 check:dependencies", "build:sdk": "lerna run --stream build:sdk", "deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular", "release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset", @@ -52,15 +53,12 @@ "kill-server": "kill-port 4001 4002", "kill-accountportal": "kill-port 3001 4003", "kill-all": "yarn run kill-builder && yarn run kill-server && yarn kill-accountportal", - "dev": "yarn run kill-all && lerna run --parallel prebuild && lerna run --stream dev --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server", - "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up --ignore @budibase/account-portal-server && lerna run --stream dev --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server", + "dev": "yarn run kill-all && lerna run --parallel prebuild && lerna run --stream dev", + "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "dev:server": "yarn run kill-server && lerna run --stream dev --scope @budibase/worker --scope @budibase/server", - "dev:accountportal": "yarn kill-accountportal && lerna run dev --stream --scope @budibase/account-portal-ui --scope @budibase/account-portal-server", - "dev:camunda": "./scripts/deploy-camunda.sh", - "dev:all": "yarn run kill-all && lerna run --stream dev", "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", "dev:docker": "./scripts/devDocker.sh", - "test": "lerna run --concurrency 1 --stream test --stream", + "test": "lerna run --concurrency 1 --stream test", "test:containers:kill": "./scripts/killTestcontainers.sh", "lint:eslint": "eslint packages --max-warnings=0", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"", @@ -98,9 +96,7 @@ }, "workspaces": { "packages": [ - "packages/*", - "!packages/account-portal", - "packages/account-portal/packages/*" + "packages/*" ] }, "resolutions": { @@ -114,7 +110,7 @@ "semver": "7.5.3", "http-cache-semantics": "4.1.1", "msgpackr": "1.10.1", - "axios": "1.6.3", + "axios": "1.7.7", "xml2js": "0.6.2", "unset-value": "2.0.1", "passport": "0.6.0", @@ -124,6 +120,5 @@ }, "engines": { "node": ">=20.0.0 <21.0.0" - }, - "dependencies": {} + } } diff --git a/packages/account-portal b/packages/account-portal deleted file mode 160000 index 9bef5d1656..0000000000 --- a/packages/account-portal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9bef5d1656b4f3c991447ded6d65b0eba393a140 diff --git a/packages/backend-core/.npmignore b/packages/backend-core/.npmignore index 30bba85ce8..fb547825eb 100644 --- a/packages/backend-core/.npmignore +++ b/packages/backend-core/.npmignore @@ -1,6 +1,4 @@ * !dist/**/* dist/tsconfig.build.tsbuildinfo -!package.json -!src/** -!tests/** \ No newline at end of file +!package.json \ No newline at end of file diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index b68cba5fd9..a4381b4200 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -9,6 +9,13 @@ "./tests": "./dist/tests/index.js", "./*": "./dist/*.js" }, + "typesVersions": { + "*": { + "tests": [ + "dist/tests/index.d.ts" + ] + } + }, "author": "Budibase", "license": "GPL-3.0", "scripts": { @@ -17,6 +24,7 @@ "build": "tsc -p tsconfig.build.json --paths null && node ./scripts/build.js", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "check:types": "tsc -p tsconfig.json --noEmit --paths null --target es2020", + "check:dependencies": "node ../../scripts/depcheck.js", "test": "bash scripts/test.sh", "test:watch": "jest --watchAll" }, @@ -25,17 +33,21 @@ "@budibase/pouchdb-replication-stream": "1.2.11", "@budibase/shared-core": "0.0.0", "@budibase/types": "0.0.0", + "@techpass/passport-openidconnect": "0.3.3", "aws-cloudfront-sign": "3.0.2", - "aws-sdk": "2.1030.0", + "aws-sdk": "2.1692.0", "bcrypt": "5.1.0", "bcryptjs": "2.4.3", "bull": "4.10.1", "correlation-id": "4.0.0", - "dd-trace": "5.2.0", + "dd-trace": "5.26.0", "dotenv": "16.0.1", + "google-auth-library": "^8.0.1", + "google-spreadsheet": "npm:@budibase/google-spreadsheet@4.1.5", "ioredis": "5.3.2", "joi": "17.6.0", "jsonwebtoken": "9.0.2", + "knex": "2.4.2", "koa-passport": "^6.0.0", "koa-pino-logger": "4.0.0", "lodash": "4.17.21", @@ -46,17 +58,17 @@ "pino": "8.11.0", "pino-http": "8.3.3", "posthog-node": "4.0.1", - "pouchdb": "7.3.0", - "pouchdb-find": "7.2.2", + "pouchdb": "9.0.0", + "pouchdb-find": "9.0.0", "redlock": "4.2.0", "rotating-file-stream": "3.1.0", "sanitize-s3-objectkey": "0.0.1", "semver": "^7.5.4", "tar-fs": "2.1.1", - "uuid": "^8.3.2", - "knex": "2.4.2" + "uuid": "^8.3.2" }, "devDependencies": { + "@jest/types": "^29.6.3", "@shopify/jest-koa-mocks": "5.1.1", "@swc/core": "1.3.71", "@swc/jest": "0.2.27", @@ -64,8 +76,9 @@ "@types/cookies": "0.7.8", "@types/jest": "29.5.5", "@types/lodash": "4.14.200", + "@types/node": "^22.9.0", "@types/node-fetch": "2.6.4", - "@types/pouchdb": "6.4.0", + "@types/pouchdb": "6.4.2", "@types/redlock": "4.0.7", "@types/semver": "7.3.7", "@types/tar-fs": "2.0.1", @@ -74,6 +87,7 @@ "ioredis-mock": "8.9.0", "jest": "29.7.0", "jest-serial-runner": "1.2.1", + "nock": "^13.5.6", "pino-pretty": "10.0.0", "pouchdb-adapter-memory": "7.2.2", "testcontainers": "^10.7.2", diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index b807db0ee3..371f3dc997 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -10,7 +10,6 @@ import { DatabaseQueryOpts, DBError, Document, - FeatureFlag, isDocument, RowResponse, RowValue, @@ -27,7 +26,6 @@ import { SQLITE_DESIGN_DOC_ID } from "../../constants" import { DDInstrumentedDatabase } from "../instrumentation" import { checkSlashesInUrl } from "../../helpers" import { sqlLog } from "../../sql/utils" -import { flags } from "../../features" const DATABASE_NOT_FOUND = "Database does not exist." @@ -192,7 +190,7 @@ export class DatabaseImpl implements Database { } } - private async performCall(call: DBCallback): Promise { + private async performCall(call: DBCallback): Promise { const db = this.getDb() const fnc = await call(db) try { @@ -456,10 +454,7 @@ export class DatabaseImpl implements Database { } async destroy() { - if ( - (await flags.isEnabled(FeatureFlag.SQS)) && - (await this.exists(SQLITE_DESIGN_DOC_ID)) - ) { + if (await this.exists(SQLITE_DESIGN_DOC_ID)) { // delete the design document, then run the cleanup operation const definition = await this.get(SQLITE_DESIGN_DOC_ID) // remove all tables - save the definition then trigger a cleanup @@ -472,7 +467,7 @@ export class DatabaseImpl implements Database { } catch (err: any) { // didn't exist, don't worry if (err.statusCode === 404) { - return + return { ok: true } } else { throw new CouchDBError(err.message, err) } diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index e08bfc0362..0c0056d6ed 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -27,7 +27,7 @@ export class DDInstrumentedDatabase implements Database { exists(docId?: string): Promise { return tracer.trace("db.exists", span => { - span?.addTags({ db_name: this.name, doc_id: docId }) + span.addTags({ db_name: this.name, doc_id: docId }) if (docId) { return this.db.exists(docId) } @@ -37,15 +37,17 @@ export class DDInstrumentedDatabase implements Database { get(id?: string | undefined): Promise { return tracer.trace("db.get", span => { - span?.addTags({ db_name: this.name, doc_id: id }) + span.addTags({ db_name: this.name, doc_id: id }) return this.db.get(id) }) } tryGet(id?: string | undefined): Promise { - return tracer.trace("db.tryGet", span => { - span?.addTags({ db_name: this.name, doc_id: id }) - return this.db.tryGet(id) + return tracer.trace("db.tryGet", async span => { + span.addTags({ db_name: this.name, doc_id: id }) + const doc = await this.db.tryGet(id) + span.addTags({ doc_found: doc !== undefined }) + return doc }) } @@ -53,13 +55,15 @@ export class DDInstrumentedDatabase implements Database { ids: string[], opts?: { allowMissing?: boolean | undefined } | undefined ): Promise { - return tracer.trace("db.getMultiple", span => { - span?.addTags({ + return tracer.trace("db.getMultiple", async span => { + span.addTags({ db_name: this.name, num_docs: ids.length, allow_missing: opts?.allowMissing, }) - return this.db.getMultiple(ids, opts) + const docs = await this.db.getMultiple(ids, opts) + span.addTags({ num_docs_found: docs.length }) + return docs }) } @@ -69,12 +73,14 @@ export class DDInstrumentedDatabase implements Database { idOrDoc: string | Document, rev?: string ): Promise { - return tracer.trace("db.remove", span => { - span?.addTags({ db_name: this.name, doc_id: idOrDoc }) + return tracer.trace("db.remove", async span => { + span.addTags({ db_name: this.name, doc_id: idOrDoc, rev }) const isDocument = typeof idOrDoc === "object" const id = isDocument ? idOrDoc._id! : idOrDoc rev = isDocument ? idOrDoc._rev : rev - return this.db.remove(id, rev) + const resp = await this.db.remove(id, rev) + span.addTags({ ok: resp.ok }) + return resp }) } @@ -83,7 +89,11 @@ export class DDInstrumentedDatabase implements Database { opts?: { silenceErrors?: boolean } ): Promise { return tracer.trace("db.bulkRemove", span => { - span?.addTags({ db_name: this.name, num_docs: documents.length }) + span.addTags({ + db_name: this.name, + num_docs: documents.length, + silence_errors: opts?.silenceErrors, + }) return this.db.bulkRemove(documents, opts) }) } @@ -92,15 +102,21 @@ export class DDInstrumentedDatabase implements Database { document: AnyDocument, opts?: DatabasePutOpts | undefined ): Promise { - return tracer.trace("db.put", span => { - span?.addTags({ db_name: this.name, doc_id: document._id }) - return this.db.put(document, opts) + return tracer.trace("db.put", async span => { + span.addTags({ + db_name: this.name, + doc_id: document._id, + force: opts?.force, + }) + const resp = await this.db.put(document, opts) + span.addTags({ ok: resp.ok }) + return resp }) } bulkDocs(documents: AnyDocument[]): Promise { return tracer.trace("db.bulkDocs", span => { - span?.addTags({ db_name: this.name, num_docs: documents.length }) + span.addTags({ db_name: this.name, num_docs: documents.length }) return this.db.bulkDocs(documents) }) } @@ -108,9 +124,15 @@ export class DDInstrumentedDatabase implements Database { allDocs( params: DatabaseQueryOpts ): Promise> { - return tracer.trace("db.allDocs", span => { - span?.addTags({ db_name: this.name }) - return this.db.allDocs(params) + return tracer.trace("db.allDocs", async span => { + span.addTags({ db_name: this.name, ...params }) + const resp = await this.db.allDocs(params) + span.addTags({ + total_rows: resp.total_rows, + rows_length: resp.rows.length, + offset: resp.offset, + }) + return resp }) } @@ -118,57 +140,75 @@ export class DDInstrumentedDatabase implements Database { viewName: string, params: DatabaseQueryOpts ): Promise> { - return tracer.trace("db.query", span => { - span?.addTags({ db_name: this.name, view_name: viewName }) - return this.db.query(viewName, params) + return tracer.trace("db.query", async span => { + span.addTags({ db_name: this.name, view_name: viewName, ...params }) + const resp = await this.db.query(viewName, params) + span.addTags({ + total_rows: resp.total_rows, + rows_length: resp.rows.length, + offset: resp.offset, + }) + return resp }) } - destroy(): Promise { - return tracer.trace("db.destroy", span => { - span?.addTags({ db_name: this.name }) - return this.db.destroy() + destroy(): Promise { + return tracer.trace("db.destroy", async span => { + span.addTags({ db_name: this.name }) + const resp = await this.db.destroy() + span.addTags({ ok: resp.ok }) + return resp }) } - compact(): Promise { - return tracer.trace("db.compact", span => { - span?.addTags({ db_name: this.name }) - return this.db.compact() + compact(): Promise { + return tracer.trace("db.compact", async span => { + span.addTags({ db_name: this.name }) + const resp = await this.db.compact() + span.addTags({ ok: resp.ok }) + return resp }) } dump(stream: Writable, opts?: DatabaseDumpOpts | undefined): Promise { return tracer.trace("db.dump", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ + db_name: this.name, + batch_limit: opts?.batch_limit, + batch_size: opts?.batch_size, + style: opts?.style, + timeout: opts?.timeout, + num_doc_ids: opts?.doc_ids?.length, + view: opts?.view, + }) return this.db.dump(stream, opts) }) } load(...args: any[]): Promise { return tracer.trace("db.load", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name, num_args: args.length }) return this.db.load(...args) }) } createIndex(...args: any[]): Promise { return tracer.trace("db.createIndex", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name, num_args: args.length }) return this.db.createIndex(...args) }) } deleteIndex(...args: any[]): Promise { return tracer.trace("db.deleteIndex", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name, num_args: args.length }) return this.db.deleteIndex(...args) }) } getIndexes(...args: any[]): Promise { return tracer.trace("db.getIndexes", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name, num_args: args.length }) return this.db.getIndexes(...args) }) } @@ -177,22 +217,27 @@ export class DDInstrumentedDatabase implements Database { sql: string, parameters?: SqlQueryBinding ): Promise { - return tracer.trace("db.sql", span => { - span?.addTags({ db_name: this.name }) - return this.db.sql(sql, parameters) + return tracer.trace("db.sql", async span => { + span.addTags({ db_name: this.name, num_bindings: parameters?.length }) + const resp = await this.db.sql(sql, parameters) + span.addTags({ num_rows: resp.length }) + return resp }) } sqlPurgeDocument(docIds: string[] | string): Promise { return tracer.trace("db.sqlPurgeDocument", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ + db_name: this.name, + num_docs: Array.isArray(docIds) ? docIds.length : 1, + }) return this.db.sqlPurgeDocument(docIds) }) } sqlDiskCleanup(): Promise { return tracer.trace("db.sqlDiskCleanup", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name }) return this.db.sqlDiskCleanup() }) } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index b2f95210d3..56d9cd6e10 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -1,6 +1,7 @@ import { existsSync, readFileSync } from "fs" import { ServiceType } from "@budibase/types" import { cloneDeep } from "lodash" +import { createSecretKey } from "crypto" function isTest() { return isJest() @@ -18,6 +19,12 @@ function isDev() { return process.env.NODE_ENV !== "production" } +function parseIntSafe(number?: string) { + if (number) { + return parseInt(number) + } +} + let LOADED = false if (!LOADED && isDev() && !isTest()) { require("dotenv").config() @@ -126,8 +133,12 @@ const environment = { }, BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT, JS_BCRYPT: process.env.JS_BCRYPT, - JWT_SECRET: process.env.JWT_SECRET, - JWT_SECRET_FALLBACK: process.env.JWT_SECRET_FALLBACK, + JWT_SECRET: process.env.JWT_SECRET + ? createSecretKey(Buffer.from(process.env.JWT_SECRET)) + : undefined, + JWT_SECRET_FALLBACK: process.env.JWT_SECRET_FALLBACK + ? createSecretKey(Buffer.from(process.env.JWT_SECRET_FALLBACK)) + : undefined, ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, API_ENCRYPTION_KEY: getAPIEncryptionKey(), COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", @@ -226,9 +237,7 @@ const environment = { MIN_VERSION_WITHOUT_POWER_ROLE: process.env.MIN_VERSION_WITHOUT_POWER_ROLE || "3.0.0", DISABLE_CONTENT_SECURITY_POLICY: process.env.DISABLE_CONTENT_SECURITY_POLICY, - // stopgap migration strategy until we can ensure backwards compat without unsafe-inline in CSP - DISABLE_CSP_UNSAFE_INLINE_SCRIPTS: - process.env.DISABLE_CSP_UNSAFE_INLINE_SCRIPTS, + BSON_BUFFER_SIZE: parseIntSafe(process.env.BSON_BUFFER_SIZE), } export function setEnv(newEnvVars: Partial): () => void { diff --git a/packages/backend-core/src/features/features.ts b/packages/backend-core/src/features/features.ts index de6b9cad8b..b3f016e88a 100644 --- a/packages/backend-core/src/features/features.ts +++ b/packages/backend-core/src/features/features.ts @@ -269,8 +269,6 @@ export class FlagSet, T extends { [key: string]: V }> { export const flags = new FlagSet({ [FeatureFlag.DEFAULT_VALUES]: Flag.boolean(true), [FeatureFlag.AUTOMATION_BRANCHING]: Flag.boolean(true), - [FeatureFlag.SQS]: Flag.boolean(true), - [FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(true), [FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(true), [FeatureFlag.BUDIBASE_AI]: Flag.boolean(true), }) diff --git a/packages/backend-core/src/middleware/contentSecurityPolicy.ts b/packages/backend-core/src/middleware/contentSecurityPolicy.ts index e0dfbe6f64..d1668d3dd5 100644 --- a/packages/backend-core/src/middleware/contentSecurityPolicy.ts +++ b/packages/backend-core/src/middleware/contentSecurityPolicy.ts @@ -1,5 +1,4 @@ import crypto from "crypto" -import env from "../environment" const CSP_DIRECTIVES = { "default-src": ["'self'"], @@ -97,10 +96,6 @@ export async function contentSecurityPolicy(ctx: any, next: any) { `'nonce-${nonce}'`, ] - if (!env.DISABLE_CSP_UNSAFE_INLINE_SCRIPTS) { - directives["script-src"].push("'unsafe-inline'") - } - ctx.state.nonce = nonce const cspHeader = Object.entries(directives) diff --git a/packages/backend-core/src/security/tests/encryption.spec.ts b/packages/backend-core/src/security/tests/encryption.spec.ts index 0b7eb96b68..8e0af846bd 100644 --- a/packages/backend-core/src/security/tests/encryption.spec.ts +++ b/packages/backend-core/src/security/tests/encryption.spec.ts @@ -4,7 +4,7 @@ import env from "../../environment" describe("encryption", () => { it("should throw an error if API encryption key is not set", () => { const jwt = getSecret(SecretOption.API) - expect(jwt).toBe(env.JWT_SECRET) + expect(jwt).toBe(env.JWT_SECRET?.export().toString()) }) it("should throw an error if encryption key is not set", () => { diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 0830f8ab6f..aeb7418526 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -81,6 +81,7 @@ "@spectrum-css/typography": "3.0.1", "@spectrum-css/underlay": "2.0.9", "@spectrum-css/vars": "3.0.1", + "atrament": "^4.3.0", "dayjs": "^1.10.8", "easymde": "^2.16.1", "svelte-dnd-action": "^0.9.8", diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte index 2922d88e7a..26f1dc86c6 100644 --- a/packages/bbui/src/Form/Core/Dropzone.svelte +++ b/packages/bbui/src/Form/Core/Dropzone.svelte @@ -8,6 +8,7 @@ import Link from "../../Link/Link.svelte" import Tag from "../../Tags/Tag.svelte" import Tags from "../../Tags/Tags.svelte" + import ProgressCircle from "../../ProgressCircle/ProgressCircle.svelte" const BYTES_IN_KB = 1000 const BYTES_IN_MB = 1000000 @@ -39,12 +40,14 @@ "jfif", "webp", ] - const fieldId = id || uuid() + let selectedImageIdx = 0 let fileDragged = false let selectedUrl let fileInput + let loading = false + $: selectedImage = value?.[selectedImageIdx] ?? null $: fileCount = value?.length ?? 0 $: isImage = @@ -86,10 +89,15 @@ } if (processFiles) { - const processedFiles = await processFiles(fileList) - const newValue = [...value, ...processedFiles] - dispatch("change", newValue) - selectedImageIdx = newValue.length - 1 + loading = true + try { + const processedFiles = await processFiles(fileList) + const newValue = [...value, ...processedFiles] + dispatch("change", newValue) + selectedImageIdx = newValue.length - 1 + } finally { + loading = false + } } else { dispatch("change", fileList) } @@ -227,7 +235,7 @@ {#if showDropzone}
+ + {#if loading} +
+ +
+ {/if}
{/if} @@ -464,6 +478,7 @@ .spectrum-Dropzone { height: 220px; + position: relative; } .compact .spectrum-Dropzone { height: 40px; @@ -488,4 +503,14 @@ .tag { margin-top: 8px; } + + .loading { + position: absolute; + display: grid; + place-items: center; + height: 100%; + width: 100%; + top: 0; + left: 0; + } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index f134c787ca..5ec66870a8 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -1,4 +1,5 @@ - +{#if responseType === FieldType.NUMBER} + +{:else if responseType === FieldType.BOOLEAN} + +{:else if responseType === FieldType.DATETIME} + +{:else} + +{/if} diff --git a/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte b/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte index 261954379b..215fdabd8d 100644 --- a/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte +++ b/packages/frontend-core/src/components/grid/layout/NewColumnButton.svelte @@ -53,6 +53,7 @@ on:close={close} maxHeight={null} resizable + minWidth={360} >
@@ -80,7 +81,6 @@ } .content { - width: 300px; padding: 20px; display: flex; flex-direction: column; diff --git a/packages/frontend-core/src/fetch/NestedProviderFetch.js b/packages/frontend-core/src/fetch/NestedProviderFetch.js index 01c22b6ba0..0a08b00cb4 100644 --- a/packages/frontend-core/src/fetch/NestedProviderFetch.js +++ b/packages/frontend-core/src/fetch/NestedProviderFetch.js @@ -5,6 +5,7 @@ export default class NestedProviderFetch extends DataFetch { // Nested providers should already have exposed their own schema return { schema: datasource?.value?.schema, + primaryDisplay: datasource?.value?.primaryDisplay, } } diff --git a/packages/pro b/packages/pro index 80770215c6..25dd40ee12 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 80770215c6159e4d47f3529fd02e74bc8ad07543 +Subproject commit 25dd40ee12b048307b558ebcedb36548d6e042cd diff --git a/packages/server/package.json b/packages/server/package.json index 76dd03b5a8..d2a51b4453 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -13,6 +13,7 @@ "build": "node ./scripts/build.js", "postbuild": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && copyfiles -f ../../yarn.lock ./dist/", "check:types": "tsc -p tsconfig.json --noEmit --paths null --target es2020", + "check:dependencies": "node ../../scripts/depcheck.js", "build:isolated-vm-lib:snippets": "esbuild --minify --bundle src/jsRunner/bundles/snippets.ts --outfile=src/jsRunner/bundles/snippets.ivm.bundle.js --platform=node --format=iife --global-name=snippets", "build:isolated-vm-lib:string-templates": "esbuild --minify --bundle src/jsRunner/bundles/index-helpers.ts --outfile=src/jsRunner/bundles/index-helpers.ivm.bundle.js --platform=node --format=iife --external:handlebars --global-name=helpers", "build:isolated-vm-lib:bson": "esbuild --minify --bundle src/jsRunner/bundles/bsonPackage.ts --outfile=src/jsRunner/bundles/bson.ivm.bundle.js --platform=node --format=iife --global-name=bson", @@ -49,9 +50,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", + "@azure/msal-node": "^2.5.1", "@budibase/backend-core": "0.0.0", "@budibase/client": "0.0.0", "@budibase/frontend-core": "0.0.0", + "@budibase/nano": "10.1.5", "@budibase/pro": "0.0.0", "@budibase/shared-core": "0.0.0", "@budibase/string-templates": "0.0.0", @@ -60,15 +63,17 @@ "@bull-board/koa": "5.10.2", "@elastic/elasticsearch": "7.10.0", "@google-cloud/firestore": "7.8.0", - "@koa/router": "8.0.8", + "@koa/cors": "5.0.0", + "@koa/router": "13.1.0", "@socket.io/redis-adapter": "^8.2.1", "@types/xml2js": "^0.4.14", "airtable": "0.12.2", "arangojs": "7.2.0", "archiver": "7.0.1", - "aws-sdk": "2.1030.0", + "aws-sdk": "2.1692.0", "bcrypt": "5.1.0", "bcryptjs": "2.4.3", + "bson": "^6.9.0", "buffer": "6.0.3", "bull": "4.10.1", "chokidar": "3.5.3", @@ -76,17 +81,20 @@ "cookies": "0.8.0", "csvtojson": "2.0.10", "curlconverter": "3.21.0", - "dd-trace": "5.2.0", + "dayjs": "^1.10.8", + "dd-trace": "5.26.0", "dotenv": "8.2.0", "form-data": "4.0.0", "global-agent": "3.0.0", + "google-auth-library": "^8.0.1", "google-spreadsheet": "npm:@budibase/google-spreadsheet@4.1.5", "ioredis": "5.3.2", "isolated-vm": "^4.7.2", - "jimp": "0.22.12", + "jimp": "1.1.4", "joi": "17.6.0", "js-yaml": "4.1.0", "jsonschema": "1.4.0", + "jsonwebtoken": "9.0.2", "knex": "2.4.2", "koa": "2.13.4", "koa-body": "4.2.0", @@ -97,7 +105,7 @@ "lodash": "4.17.21", "memorystream": "0.3.1", "mongodb": "6.7.0", - "mssql": "10.0.1", + "mssql": "11.0.1", "mysql2": "3.9.8", "node-fetch": "2.6.7", "object-sizeof": "2.6.1", @@ -105,24 +113,28 @@ "openapi-types": "9.3.1", "oracledb": "6.5.1", "pg": "8.10.0", - "pouchdb": "7.3.0", + "pouchdb": "9.0.0", "pouchdb-all-dbs": "1.1.1", - "pouchdb-find": "7.2.2", + "pouchdb-find": "9.0.0", "redis": "4", + "semver": "^7.5.4", "serialize-error": "^7.0.1", "server-destroy": "1.0.1", - "snowflake-promise": "^4.5.0", - "socket.io": "4.7.5", + "snowflake-sdk": "^1.15.0", + "socket.io": "4.8.1", + "svelte": "^4.2.10", "tar": "6.2.1", "tmp": "0.2.3", "to-json-schema": "0.2.5", "uuid": "^8.3.2", "validate.js": "0.13.1", "worker-farm": "1.7.0", - "xml2js": "0.5.0" + "xml2js": "0.6.2" }, "devDependencies": { + "@babel/core": "^7.22.5", "@babel/preset-env": "7.16.11", + "@jest/types": "^29.6.3", "@swc/core": "1.3.71", "@swc/jest": "0.2.27", "@types/archiver": "6.0.2", @@ -130,19 +142,24 @@ "@types/jest": "29.5.5", "@types/koa": "2.13.4", "@types/koa-send": "^4.1.6", - "@types/koa__router": "8.0.8", + "@types/koa__cors": "5.0.0", + "@types/koa__router": "12.0.4", "@types/lodash": "4.14.200", - "@types/mssql": "9.1.4", + "@types/mssql": "9.1.5", + "@types/node": "^22.9.0", "@types/node-fetch": "2.6.4", "@types/oracledb": "6.5.1", "@types/pg": "8.6.6", + "@types/pouchdb": "6.4.2", "@types/server-destroy": "1.0.1", "@types/supertest": "2.0.14", "@types/tar": "6.1.5", "@types/tmp": "0.2.6", "@types/uuid": "8.3.4", + "chance": "^1.1.12", "copyfiles": "2.4.1", "docker-compose": "0.23.17", + "ioredis-mock": "8.9.0", "jest": "29.7.0", "jest-extended": "^4.0.2", "jest-openapi": "0.14.2", diff --git a/packages/server/scripts/test.sh b/packages/server/scripts/test.sh index 4b456e4731..5a02628a26 100644 --- a/packages/server/scripts/test.sh +++ b/packages/server/scripts/test.sh @@ -1,12 +1,12 @@ #!/bin/bash -set -e +set -ex if [[ -n $CI ]] then export NODE_OPTIONS="--max-old-space-size=4096 --no-node-snapshot $NODE_OPTIONS" - jest --coverage --maxWorkers=4 --forceExit --workerIdleMemoryLimit=2000MB --bail $@ + jest --coverage --maxWorkers=4 --forceExit --workerIdleMemoryLimit=2000MB --bail "$@" else # --maxWorkers performs better in development export NODE_OPTIONS="--no-node-snapshot $NODE_OPTIONS" - jest --coverage --maxWorkers=2 --forceExit $@ + jest --coverage --maxWorkers=2 --forceExit "$@" fi \ No newline at end of file diff --git a/packages/server/specs/openapi.json b/packages/server/specs/openapi.json index f3091a1fc7..c47a14cf21 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -32,6 +32,15 @@ "type": "string" } }, + "viewId": { + "in": "path", + "name": "viewId", + "required": true, + "description": "The ID of the view which this request is targeting.", + "schema": { + "type": "string" + } + }, "rowId": { "in": "path", "name": "rowId", @@ -47,7 +56,7 @@ "required": true, "description": "The ID of the app which this request is targeting.", "schema": { - "default": "{{ appId }}", + "default": "{{appId}}", "type": "string" } }, @@ -57,7 +66,7 @@ "required": true, "description": "The ID of the app which this request is targeting.", "schema": { - "default": "{{ appId }}", + "default": "{{appId}}", "type": "string" } }, @@ -423,6 +432,110 @@ }, "metrics": { "value": "# HELP budibase_os_uptime Time in seconds that the host operating system has been up.\n# TYPE budibase_os_uptime counter\nbudibase_os_uptime 54958\n# HELP budibase_os_free_mem Bytes of memory free for usage on the host operating system.\n# TYPE budibase_os_free_mem gauge\nbudibase_os_free_mem 804507648\n# HELP budibase_os_total_mem Total bytes of memory on the host operating system.\n# TYPE budibase_os_total_mem gauge\nbudibase_os_total_mem 16742404096\n# HELP budibase_os_used_mem Total bytes of memory in use on the host operating system.\n# TYPE budibase_os_used_mem gauge\nbudibase_os_used_mem 15937896448\n# HELP budibase_os_load1 Host operating system load average.\n# TYPE budibase_os_load1 gauge\nbudibase_os_load1 1.91\n# HELP budibase_os_load5 Host operating system load average.\n# TYPE budibase_os_load5 gauge\nbudibase_os_load5 1.75\n# HELP budibase_os_load15 Host operating system load average.\n# TYPE budibase_os_load15 gauge\nbudibase_os_load15 1.56\n# HELP budibase_tenant_user_count The number of users created.\n# TYPE budibase_tenant_user_count gauge\nbudibase_tenant_user_count 1\n# HELP budibase_tenant_app_count The number of apps created by a user.\n# TYPE budibase_tenant_app_count gauge\nbudibase_tenant_app_count 2\n# HELP budibase_tenant_production_app_count The number of apps a user has published.\n# TYPE budibase_tenant_production_app_count gauge\nbudibase_tenant_production_app_count 1\n# HELP budibase_tenant_dev_app_count The number of apps a user has unpublished in development.\n# TYPE budibase_tenant_dev_app_count gauge\nbudibase_tenant_dev_app_count 1\n# HELP budibase_tenant_db_count The number of couchdb databases including global tables such as _users.\n# TYPE budibase_tenant_db_count gauge\nbudibase_tenant_db_count 3\n# HELP budibase_quota_usage_apps The number of apps created.\n# TYPE budibase_quota_usage_apps gauge\nbudibase_quota_usage_apps 1\n# HELP budibase_quota_limit_apps The limit on the number of apps that can be created.\n# TYPE budibase_quota_limit_apps gauge\nbudibase_quota_limit_apps 9007199254740991\n# HELP budibase_quota_usage_rows The number of database rows used from the quota.\n# TYPE budibase_quota_usage_rows gauge\nbudibase_quota_usage_rows 0\n# HELP budibase_quota_limit_rows The limit on the number of rows that can be created.\n# TYPE budibase_quota_limit_rows gauge\nbudibase_quota_limit_rows 9007199254740991\n# HELP budibase_quota_usage_plugins The number of plugins in use.\n# TYPE budibase_quota_usage_plugins gauge\nbudibase_quota_usage_plugins 0\n# HELP budibase_quota_limit_plugins The limit on the number of plugins that can be created.\n# TYPE budibase_quota_limit_plugins gauge\nbudibase_quota_limit_plugins 9007199254740991\n# HELP budibase_quota_usage_user_groups The number of user groups created.\n# TYPE budibase_quota_usage_user_groups gauge\nbudibase_quota_usage_user_groups 0\n# HELP budibase_quota_limit_user_groups The limit on the number of user groups that can be created.\n# TYPE budibase_quota_limit_user_groups gauge\nbudibase_quota_limit_user_groups 9007199254740991\n# HELP budibase_quota_usage_queries The number of queries used in the current month.\n# TYPE budibase_quota_usage_queries gauge\nbudibase_quota_usage_queries 0\n# HELP budibase_quota_limit_queries The limit on the number of queries for the current month.\n# TYPE budibase_quota_limit_queries gauge\nbudibase_quota_limit_queries 9007199254740991\n# HELP budibase_quota_usage_automations The number of automations used in the current month.\n# TYPE budibase_quota_usage_automations gauge\nbudibase_quota_usage_automations 0\n# HELP budibase_quota_limit_automations The limit on the number of automations that can be created.\n# TYPE budibase_quota_limit_automations gauge\nbudibase_quota_limit_automations 9007199254740991\n" + }, + "view": { + "value": { + "data": { + "name": "peopleView", + "tableId": "ta_896a325f7e8147d2a2cda93c5d236511", + "schema": { + "name": { + "visible": true, + "readonly": false, + "order": 1, + "width": 300 + }, + "age": { + "visible": true, + "readonly": true, + "order": 2, + "width": 200 + }, + "salary": { + "visible": false, + "readonly": false + } + }, + "query": { + "logicalOperator": "all", + "onEmptyFilter": "none", + "groups": [ + { + "logicalOperator": "any", + "filters": [ + { + "operator": "string", + "field": "name", + "value": "John" + }, + { + "operator": "range", + "field": "age", + "value": { + "low": 18, + "high": 100 + } + } + ] + } + ] + }, + "primaryDisplay": "name" + } + } + }, + "views": { + "value": { + "data": [ + { + "name": "peopleView", + "tableId": "ta_896a325f7e8147d2a2cda93c5d236511", + "schema": { + "name": { + "visible": true, + "readonly": false, + "order": 1, + "width": 300 + }, + "age": { + "visible": true, + "readonly": true, + "order": 2, + "width": 200 + }, + "salary": { + "visible": false, + "readonly": false + } + }, + "query": { + "logicalOperator": "all", + "onEmptyFilter": "none", + "groups": [ + { + "logicalOperator": "any", + "filters": [ + { + "operator": "string", + "field": "name", + "value": "John" + }, + { + "operator": "range", + "field": "age", + "value": { + "low": 18, + "high": 100 + } + } + ] + } + ] + }, + "primaryDisplay": "name" + } + ] + } } }, "securitySchemes": { @@ -831,8 +944,7 @@ "type": "string", "enum": [ "static", - "dynamic", - "ai" + "dynamic" ], "description": "Defines whether this is a static or dynamic formula." } @@ -1042,8 +1154,7 @@ "type": "string", "enum": [ "static", - "dynamic", - "ai" + "dynamic" ], "description": "Defines whether this is a static or dynamic formula." } @@ -1264,8 +1375,7 @@ "type": "string", "enum": [ "static", - "dynamic", - "ai" + "dynamic" ], "description": "Defines whether this is a static or dynamic formula." } @@ -2024,6 +2134,872 @@ "required": [ "data" ] + }, + "view": { + "description": "The view to be created/updated.", + "type": "object", + "required": [ + "name", + "schema", + "tableId" + ], + "properties": { + "name": { + "description": "The name of the view.", + "type": "string" + }, + "tableId": { + "description": "The ID of the table this view is based on.", + "type": "string" + }, + "type": { + "description": "The type of view - standard (empty value) or calculation.", + "type": "string", + "enum": [ + "calculation" + ] + }, + "primaryDisplay": { + "type": "string", + "description": "A column used to display rows from this view - usually used when rendered in tables." + }, + "query": { + "description": "Search parameters for view", + "type": "object", + "required": [], + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "onEmptyFilter": { + "description": "If no filters match, should the view return all rows, or no rows.", + "type": "string", + "enum": [ + "all", + "none" + ] + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "sort": { + "type": "object", + "required": [ + "field" + ], + "properties": { + "field": { + "type": "string", + "description": "The field from the table/view schema to sort on." + }, + "order": { + "type": "string", + "description": "The order in which to sort.", + "enum": [ + "ascending", + "descending" + ] + }, + "type": { + "type": "string", + "description": "The type of sort to perform (by number, or by alphabetically).", + "enum": [ + "string", + "number" + ] + } + } + }, + "schema": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "properties": { + "visible": { + "type": "boolean", + "description": "Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it." + }, + "readonly": { + "type": "boolean", + "description": "When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated." + }, + "order": { + "type": "integer", + "description": "A number defining where the column shows up in tables, lowest being first." + }, + "width": { + "type": "integer", + "description": "A width for the column, defined in pixels - this affects rendering in tables." + }, + "column": { + "type": "array", + "description": "If this is a relationship column, we can set the columns we wish to include", + "items": { + "type": "object", + "properties": { + "readonly": { + "type": "boolean" + } + } + } + } + } + }, + { + "type": "object", + "properties": { + "calculationType": { + "type": "string", + "description": "This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.", + "enum": [ + "sum", + "avg", + "count", + "min", + "max" + ] + }, + "field": { + "type": "string", + "description": "The field from the table to perform the calculation on." + }, + "distinct": { + "type": "boolean", + "description": "Can be used in tandem with the count calculation type, to count unique entries." + } + } + } + ] + } + } + } + }, + "viewOutput": { + "type": "object", + "properties": { + "data": { + "description": "The view to be created/updated.", + "type": "object", + "required": [ + "name", + "schema", + "tableId", + "id" + ], + "properties": { + "name": { + "description": "The name of the view.", + "type": "string" + }, + "tableId": { + "description": "The ID of the table this view is based on.", + "type": "string" + }, + "type": { + "description": "The type of view - standard (empty value) or calculation.", + "type": "string", + "enum": [ + "calculation" + ] + }, + "primaryDisplay": { + "type": "string", + "description": "A column used to display rows from this view - usually used when rendered in tables." + }, + "query": { + "description": "Search parameters for view", + "type": "object", + "required": [], + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "onEmptyFilter": { + "description": "If no filters match, should the view return all rows, or no rows.", + "type": "string", + "enum": [ + "all", + "none" + ] + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "sort": { + "type": "object", + "required": [ + "field" + ], + "properties": { + "field": { + "type": "string", + "description": "The field from the table/view schema to sort on." + }, + "order": { + "type": "string", + "description": "The order in which to sort.", + "enum": [ + "ascending", + "descending" + ] + }, + "type": { + "type": "string", + "description": "The type of sort to perform (by number, or by alphabetically).", + "enum": [ + "string", + "number" + ] + } + } + }, + "schema": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "properties": { + "visible": { + "type": "boolean", + "description": "Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it." + }, + "readonly": { + "type": "boolean", + "description": "When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated." + }, + "order": { + "type": "integer", + "description": "A number defining where the column shows up in tables, lowest being first." + }, + "width": { + "type": "integer", + "description": "A width for the column, defined in pixels - this affects rendering in tables." + }, + "column": { + "type": "array", + "description": "If this is a relationship column, we can set the columns we wish to include", + "items": { + "type": "object", + "properties": { + "readonly": { + "type": "boolean" + } + } + } + } + } + }, + { + "type": "object", + "properties": { + "calculationType": { + "type": "string", + "description": "This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.", + "enum": [ + "sum", + "avg", + "count", + "min", + "max" + ] + }, + "field": { + "type": "string", + "description": "The field from the table to perform the calculation on." + }, + "distinct": { + "type": "boolean", + "description": "Can be used in tandem with the count calculation type, to count unique entries." + } + } + } + ] + } + }, + "id": { + "description": "The ID of the view.", + "type": "string" + } + } + } + }, + "required": [ + "data" + ] + }, + "viewSearch": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "description": "The view to be created/updated.", + "type": "object", + "required": [ + "name", + "schema", + "tableId", + "id" + ], + "properties": { + "name": { + "description": "The name of the view.", + "type": "string" + }, + "tableId": { + "description": "The ID of the table this view is based on.", + "type": "string" + }, + "type": { + "description": "The type of view - standard (empty value) or calculation.", + "type": "string", + "enum": [ + "calculation" + ] + }, + "primaryDisplay": { + "type": "string", + "description": "A column used to display rows from this view - usually used when rendered in tables." + }, + "query": { + "description": "Search parameters for view", + "type": "object", + "required": [], + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "onEmptyFilter": { + "description": "If no filters match, should the view return all rows, or no rows.", + "type": "string", + "enum": [ + "all", + "none" + ] + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "sort": { + "type": "object", + "required": [ + "field" + ], + "properties": { + "field": { + "type": "string", + "description": "The field from the table/view schema to sort on." + }, + "order": { + "type": "string", + "description": "The order in which to sort.", + "enum": [ + "ascending", + "descending" + ] + }, + "type": { + "type": "string", + "description": "The type of sort to perform (by number, or by alphabetically).", + "enum": [ + "string", + "number" + ] + } + } + }, + "schema": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "properties": { + "visible": { + "type": "boolean", + "description": "Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it." + }, + "readonly": { + "type": "boolean", + "description": "When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated." + }, + "order": { + "type": "integer", + "description": "A number defining where the column shows up in tables, lowest being first." + }, + "width": { + "type": "integer", + "description": "A width for the column, defined in pixels - this affects rendering in tables." + }, + "column": { + "type": "array", + "description": "If this is a relationship column, we can set the columns we wish to include", + "items": { + "type": "object", + "properties": { + "readonly": { + "type": "boolean" + } + } + } + } + } + }, + { + "type": "object", + "properties": { + "calculationType": { + "type": "string", + "description": "This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.", + "enum": [ + "sum", + "avg", + "count", + "min", + "max" + ] + }, + "field": { + "type": "string", + "description": "The field from the table to perform the calculation on." + }, + "distinct": { + "type": "boolean", + "description": "Can be used in tandem with the count calculation type, to count unique entries." + } + } + } + ] + } + }, + "id": { + "description": "The ID of the view.", + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] } } }, @@ -2741,6 +3717,50 @@ } } }, + "/views/{viewId}/rows/search": { + "post": { + "operationId": "rowViewSearch", + "summary": "Search for rows in a view", + "tags": [ + "rows" + ], + "parameters": [ + { + "$ref": "#/components/parameters/viewId" + }, + { + "$ref": "#/components/parameters/appId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/rowSearch" + } + } + } + }, + "responses": { + "200": { + "description": "The response will contain an array of rows that match the search parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/searchOutput" + }, + "examples": { + "search": { + "$ref": "#/components/examples/rows" + } + } + } + } + } + } + } + }, "/tables": { "post": { "operationId": "tableCreate", @@ -3115,6 +4135,209 @@ } } } + }, + "/views": { + "post": { + "operationId": "viewCreate", + "summary": "Create a view", + "description": "Create a view, this can be against an internal or external table.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/appId" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/view" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + }, + "responses": { + "200": { + "description": "Returns the created view, including the ID which has been generated for it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewOutput" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + } + } + } + }, + "/views/{viewId}": { + "put": { + "operationId": "viewUpdate", + "summary": "Update a view", + "description": "Update a view, this can be against an internal or external table.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/viewId" + }, + { + "$ref": "#/components/parameters/appId" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/view" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + }, + "responses": { + "200": { + "description": "Returns the updated view.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewOutput" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + } + } + }, + "delete": { + "operationId": "viewDestroy", + "summary": "Delete a view", + "description": "Delete a view, this can be against an internal or external table.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/viewId" + }, + { + "$ref": "#/components/parameters/appId" + } + ], + "responses": { + "200": { + "description": "Returns the deleted view.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewOutput" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + } + } + }, + "get": { + "operationId": "viewGetById", + "summary": "Retrieve a view", + "description": "Lookup a view, this could be internal or external.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/viewId" + }, + { + "$ref": "#/components/parameters/appId" + } + ], + "responses": { + "200": { + "description": "Returns the retrieved view.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewOutput" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + } + } + } + }, + "/views/search": { + "post": { + "operationId": "viewSearch", + "summary": "Search for views", + "description": "Based on view properties (currently only name) search for views.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/appId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/nameSearch" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the found views, based on the search parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewSearch" + }, + "examples": { + "views": { + "$ref": "#/components/examples/views" + } + } + } + } + } + } + } } }, "tags": [] diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index 1e9b9921cf..edfb29f432 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -23,6 +23,13 @@ components: description: The ID of the table which this request is targeting. schema: type: string + viewId: + in: path + name: viewId + required: true + description: The ID of the view which this request is targeting. + schema: + type: string rowId: in: path name: rowId @@ -36,7 +43,7 @@ components: required: true description: The ID of the app which this request is targeting. schema: - default: "{{ appId }}" + default: "{{appId}}" type: string appIdUrl: in: path @@ -44,7 +51,7 @@ components: required: true description: The ID of the app which this request is targeting. schema: - default: "{{ appId }}" + default: "{{appId}}" type: string queryId: in: path @@ -442,6 +449,74 @@ components: # TYPE budibase_quota_limit_automations gauge budibase_quota_limit_automations 9007199254740991 + view: + value: + data: + name: peopleView + tableId: ta_896a325f7e8147d2a2cda93c5d236511 + schema: + name: + visible: true + readonly: false + order: 1 + width: 300 + age: + visible: true + readonly: true + order: 2 + width: 200 + salary: + visible: false + readonly: false + query: + logicalOperator: all + onEmptyFilter: none + groups: + - logicalOperator: any + filters: + - operator: string + field: name + value: John + - operator: range + field: age + value: + low: 18 + high: 100 + primaryDisplay: name + views: + value: + data: + - name: peopleView + tableId: ta_896a325f7e8147d2a2cda93c5d236511 + schema: + name: + visible: true + readonly: false + order: 1 + width: 300 + age: + visible: true + readonly: true + order: 2 + width: 200 + salary: + visible: false + readonly: false + query: + logicalOperator: all + onEmptyFilter: none + groups: + - logicalOperator: any + filters: + - operator: string + field: name + value: John + - operator: range + field: age + value: + low: 18 + high: 100 + primaryDisplay: name securitySchemes: ApiKeyAuth: type: apiKey @@ -761,7 +836,6 @@ components: enum: - static - dynamic - - ai description: Defines whether this is a static or dynamic formula. - type: object properties: @@ -931,7 +1005,6 @@ components: enum: - static - dynamic - - ai description: Defines whether this is a static or dynamic formula. - type: object properties: @@ -1108,7 +1181,6 @@ components: enum: - static - dynamic - - ai description: Defines whether this is a static or dynamic formula. - type: object properties: @@ -1704,6 +1776,644 @@ components: - userIds required: - data + view: + description: The view to be created/updated. + type: object + required: + - name + - schema + - tableId + properties: + name: + description: The name of the view. + type: string + tableId: + description: The ID of the table this view is based on. + type: string + type: + description: The type of view - standard (empty value) or calculation. + type: string + enum: + - calculation + primaryDisplay: + type: string + description: A column used to display rows from this view - usually used when + rendered in tables. + query: + description: Search parameters for view + type: object + required: [] + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + onEmptyFilter: + description: If no filters match, should the view return all rows, or no rows. + type: string + enum: + - all + - none + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + sort: + type: object + required: + - field + properties: + field: + type: string + description: The field from the table/view schema to sort on. + order: + type: string + description: The order in which to sort. + enum: + - ascending + - descending + type: + type: string + description: The type of sort to perform (by number, or by alphabetically). + enum: + - string + - number + schema: + type: object + additionalProperties: + oneOf: + - type: object + properties: + visible: + type: boolean + description: Defines whether the column is visible or not - rows + retrieved/updated through this view will not be able to + access it. + readonly: + type: boolean + description: "When used in combination with 'visible: true' the column will be + visible in row responses but cannot be updated." + order: + type: integer + description: A number defining where the column shows up in tables, lowest being + first. + width: + type: integer + description: A width for the column, defined in pixels - this affects rendering + in tables. + column: + type: array + description: If this is a relationship column, we can set the columns we wish to + include + items: + type: object + properties: + readonly: + type: boolean + - type: object + properties: + calculationType: + type: string + description: This column should be built from a calculation, specifying a type + and field. It is important to note when a calculation is + configured all non-calculation columns will be used for + grouping. + enum: + - sum + - avg + - count + - min + - max + field: + type: string + description: The field from the table to perform the calculation on. + distinct: + type: boolean + description: Can be used in tandem with the count calculation type, to count + unique entries. + viewOutput: + type: object + properties: + data: + description: The view to be created/updated. + type: object + required: + - name + - schema + - tableId + - id + properties: + name: + description: The name of the view. + type: string + tableId: + description: The ID of the table this view is based on. + type: string + type: + description: The type of view - standard (empty value) or calculation. + type: string + enum: + - calculation + primaryDisplay: + type: string + description: A column used to display rows from this view - usually used when + rendered in tables. + query: + description: Search parameters for view + type: object + required: [] + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + onEmptyFilter: + description: If no filters match, should the view return all rows, or no rows. + type: string + enum: + - all + - none + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + sort: + type: object + required: + - field + properties: + field: + type: string + description: The field from the table/view schema to sort on. + order: + type: string + description: The order in which to sort. + enum: + - ascending + - descending + type: + type: string + description: The type of sort to perform (by number, or by alphabetically). + enum: + - string + - number + schema: + type: object + additionalProperties: + oneOf: + - type: object + properties: + visible: + type: boolean + description: Defines whether the column is visible or not - rows + retrieved/updated through this view will not be able + to access it. + readonly: + type: boolean + description: "When used in combination with 'visible: true' the column will be + visible in row responses but cannot be updated." + order: + type: integer + description: A number defining where the column shows up in tables, lowest being + first. + width: + type: integer + description: A width for the column, defined in pixels - this affects rendering + in tables. + column: + type: array + description: If this is a relationship column, we can set the columns we wish to + include + items: + type: object + properties: + readonly: + type: boolean + - type: object + properties: + calculationType: + type: string + description: This column should be built from a calculation, specifying a type + and field. It is important to note when a calculation + is configured all non-calculation columns will be used + for grouping. + enum: + - sum + - avg + - count + - min + - max + field: + type: string + description: The field from the table to perform the calculation on. + distinct: + type: boolean + description: Can be used in tandem with the count calculation type, to count + unique entries. + id: + description: The ID of the view. + type: string + required: + - data + viewSearch: + type: object + properties: + data: + type: array + items: + description: The view to be created/updated. + type: object + required: + - name + - schema + - tableId + - id + properties: + name: + description: The name of the view. + type: string + tableId: + description: The ID of the table this view is based on. + type: string + type: + description: The type of view - standard (empty value) or calculation. + type: string + enum: + - calculation + primaryDisplay: + type: string + description: A column used to display rows from this view - usually used when + rendered in tables. + query: + description: Search parameters for view + type: object + required: [] + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + onEmptyFilter: + description: If no filters match, should the view return all rows, or no rows. + type: string + enum: + - all + - none + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + sort: + type: object + required: + - field + properties: + field: + type: string + description: The field from the table/view schema to sort on. + order: + type: string + description: The order in which to sort. + enum: + - ascending + - descending + type: + type: string + description: The type of sort to perform (by number, or by alphabetically). + enum: + - string + - number + schema: + type: object + additionalProperties: + oneOf: + - type: object + properties: + visible: + type: boolean + description: Defines whether the column is visible or not - rows + retrieved/updated through this view will not be able + to access it. + readonly: + type: boolean + description: "When used in combination with 'visible: true' the column will be + visible in row responses but cannot be updated." + order: + type: integer + description: A number defining where the column shows up in tables, lowest being + first. + width: + type: integer + description: A width for the column, defined in pixels - this affects rendering + in tables. + column: + type: array + description: If this is a relationship column, we can set the columns we wish to + include + items: + type: object + properties: + readonly: + type: boolean + - type: object + properties: + calculationType: + type: string + description: This column should be built from a calculation, specifying a type + and field. It is important to note when a + calculation is configured all non-calculation + columns will be used for grouping. + enum: + - sum + - avg + - count + - min + - max + field: + type: string + description: The field from the table to perform the calculation on. + distinct: + type: boolean + description: Can be used in tandem with the count calculation type, to count + unique entries. + id: + description: The ID of the view. + type: string + required: + - data security: - ApiKeyAuth: [] paths: @@ -2136,6 +2846,32 @@ paths: examples: search: $ref: "#/components/examples/rows" + "/views/{viewId}/rows/search": + post: + operationId: rowViewSearch + summary: Search for rows in a view + tags: + - rows + parameters: + - $ref: "#/components/parameters/viewId" + - $ref: "#/components/parameters/appId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/rowSearch" + responses: + "200": + description: The response will contain an array of rows that match the search + parameters. + content: + application/json: + schema: + $ref: "#/components/schemas/searchOutput" + examples: + search: + $ref: "#/components/examples/rows" /tables: post: operationId: tableCreate @@ -2359,4 +3095,123 @@ paths: examples: users: $ref: "#/components/examples/users" + /views: + post: + operationId: viewCreate + summary: Create a view + description: Create a view, this can be against an internal or external table. + tags: + - views + parameters: + - $ref: "#/components/parameters/appId" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/view" + examples: + view: + $ref: "#/components/examples/view" + responses: + "200": + description: Returns the created view, including the ID which has been generated + for it. + content: + application/json: + schema: + $ref: "#/components/schemas/viewOutput" + examples: + view: + $ref: "#/components/examples/view" + "/views/{viewId}": + put: + operationId: viewUpdate + summary: Update a view + description: Update a view, this can be against an internal or external table. + tags: + - views + parameters: + - $ref: "#/components/parameters/viewId" + - $ref: "#/components/parameters/appId" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/view" + examples: + view: + $ref: "#/components/examples/view" + responses: + "200": + description: Returns the updated view. + content: + application/json: + schema: + $ref: "#/components/schemas/viewOutput" + examples: + view: + $ref: "#/components/examples/view" + delete: + operationId: viewDestroy + summary: Delete a view + description: Delete a view, this can be against an internal or external table. + tags: + - views + parameters: + - $ref: "#/components/parameters/viewId" + - $ref: "#/components/parameters/appId" + responses: + "200": + description: Returns the deleted view. + content: + application/json: + schema: + $ref: "#/components/schemas/viewOutput" + examples: + view: + $ref: "#/components/examples/view" + get: + operationId: viewGetById + summary: Retrieve a view + description: Lookup a view, this could be internal or external. + tags: + - views + parameters: + - $ref: "#/components/parameters/viewId" + - $ref: "#/components/parameters/appId" + responses: + "200": + description: Returns the retrieved view. + content: + application/json: + schema: + $ref: "#/components/schemas/viewOutput" + examples: + view: + $ref: "#/components/examples/view" + /views/search: + post: + operationId: viewSearch + summary: Search for views + description: Based on view properties (currently only name) search for views. + tags: + - views + parameters: + - $ref: "#/components/parameters/appId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/nameSearch" + responses: + "200": + description: Returns the found views, based on the search parameters. + content: + application/json: + schema: + $ref: "#/components/schemas/viewSearch" + examples: + views: + $ref: "#/components/examples/views" tags: [] diff --git a/packages/server/specs/parameters.ts b/packages/server/specs/parameters.ts index 2726ca5064..b3fb274567 100644 --- a/packages/server/specs/parameters.ts +++ b/packages/server/specs/parameters.ts @@ -8,6 +8,16 @@ export const tableId = { }, } +export const viewId = { + in: "path", + name: "viewId", + required: true, + description: "The ID of the view which this request is targeting.", + schema: { + type: "string", + }, +} + export const rowId = { in: "path", name: "rowId", diff --git a/packages/server/specs/resources/index.ts b/packages/server/specs/resources/index.ts index 49508e2e4f..0d32f2a007 100644 --- a/packages/server/specs/resources/index.ts +++ b/packages/server/specs/resources/index.ts @@ -6,6 +6,7 @@ import user from "./user" import metrics from "./metrics" import misc from "./misc" import roles from "./roles" +import view from "./view" export const examples = { ...application.getExamples(), @@ -16,6 +17,7 @@ export const examples = { ...misc.getExamples(), ...metrics.getExamples(), ...roles.getExamples(), + ...view.getExamples(), } export const schemas = { @@ -26,4 +28,5 @@ export const schemas = { ...user.getSchemas(), ...misc.getSchemas(), ...roles.getSchemas(), + ...view.getSchemas(), } diff --git a/packages/server/specs/resources/misc.ts b/packages/server/specs/resources/misc.ts index f56dff3301..8f77d2b22a 100644 --- a/packages/server/specs/resources/misc.ts +++ b/packages/server/specs/resources/misc.ts @@ -1,99 +1,101 @@ import { object } from "./utils" import Resource from "./utils/Resource" +export const searchSchema = { + type: "object", + properties: { + allOr: { + type: "boolean", + description: + "Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used.", + }, + string: { + type: "object", + example: { + columnName1: "value", + columnName2: "value", + }, + description: + "A map of field name to the string to search for, this will look for rows that have a value starting with the string value.", + additionalProperties: { + type: "string", + description: "The value to search for in the column.", + }, + }, + fuzzy: { + type: "object", + description: + "Searches for a sub-string within a string column, e.g. searching for 'dib' will match 'Budibase'.", + }, + range: { + type: "object", + description: + 'Searches within a range, the format of this must be in the format of an object with a "low" and "high" property.', + example: { + columnName1: { + low: 10, + high: 20, + }, + }, + }, + equal: { + type: "object", + description: + "Searches for rows that have a column value that is exactly the value set.", + }, + notEqual: { + type: "object", + description: + "Searches for any row which does not contain the specified column value.", + }, + empty: { + type: "object", + description: + "Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.", + example: { + columnName1: "", + }, + }, + notEmpty: { + type: "object", + description: "Searches for rows which have the specified column.", + }, + oneOf: { + type: "object", + description: + "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2].", + }, + contains: { + type: "object", + description: + "Searches for a value, or set of values in array column types (such as a multi-select). If an array of search options is provided then it must match all.", + example: { + arrayColumn: ["a", "b"], + }, + }, + notContains: { + type: "object", + description: + "The logical inverse of contains. Only works on array column types. If an array of values is passed, the row must not match any of them to be returned in the response.", + example: { + arrayColumn: ["a", "b"], + }, + }, + containsAny: { + type: "object", + description: + "As with the contains search, only works on array column types and searches for any of the provided values when given an array.", + example: { + arrayColumn: ["a", "b"], + }, + }, + }, +} + export default new Resource().setSchemas({ rowSearch: object( { - query: { - type: "object", - properties: { - allOr: { - type: "boolean", - description: - "Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used.", - }, - string: { - type: "object", - example: { - columnName1: "value", - columnName2: "value", - }, - description: - "A map of field name to the string to search for, this will look for rows that have a value starting with the string value.", - additionalProperties: { - type: "string", - description: "The value to search for in the column.", - }, - }, - fuzzy: { - type: "object", - description: - "Searches for a sub-string within a string column, e.g. searching for 'dib' will match 'Budibase'.", - }, - range: { - type: "object", - description: - 'Searches within a range, the format of this must be in the format of an object with a "low" and "high" property.', - example: { - columnName1: { - low: 10, - high: 20, - }, - }, - }, - equal: { - type: "object", - description: - "Searches for rows that have a column value that is exactly the value set.", - }, - notEqual: { - type: "object", - description: - "Searches for any row which does not contain the specified column value.", - }, - empty: { - type: "object", - description: - "Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.", - example: { - columnName1: "", - }, - }, - notEmpty: { - type: "object", - description: "Searches for rows which have the specified column.", - }, - oneOf: { - type: "object", - description: - "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2].", - }, - contains: { - type: "object", - description: - "Searches for a value, or set of values in array column types (such as a multi-select). If an array of search options is provided then it must match all.", - example: { - arrayColumn: ["a", "b"], - }, - }, - notContains: { - type: "object", - description: - "The logical inverse of contains. Only works on array column types. If an array of values is passed, the row must not match any of them to be returned in the response.", - example: { - arrayColumn: ["a", "b"], - }, - }, - containsAny: { - type: "object", - description: - "As with the contains search, only works on array column types and searches for any of the provided values when given an array.", - example: { - arrayColumn: ["a", "b"], - }, - }, - }, - }, + query: searchSchema, paginate: { type: "boolean", description: "Enables pagination, by default this is disabled.", diff --git a/packages/server/specs/resources/view.ts b/packages/server/specs/resources/view.ts new file mode 100644 index 0000000000..aeb2b97aa9 --- /dev/null +++ b/packages/server/specs/resources/view.ts @@ -0,0 +1,274 @@ +import { object } from "./utils" +import Resource from "./utils/Resource" +import { + ArrayOperator, + BasicOperator, + CalculationType, + RangeOperator, + SortOrder, + SortType, +} from "@budibase/types" +import { cloneDeep } from "lodash" + +const view = { + name: "peopleView", + tableId: "ta_896a325f7e8147d2a2cda93c5d236511", + schema: { + name: { + visible: true, + readonly: false, + order: 1, + width: 300, + }, + age: { + visible: true, + readonly: true, + order: 2, + width: 200, + }, + salary: { + visible: false, + readonly: false, + }, + }, + query: { + logicalOperator: "all", + onEmptyFilter: "none", + groups: [ + { + logicalOperator: "any", + filters: [ + { operator: "string", field: "name", value: "John" }, + { operator: "range", field: "age", value: { low: 18, high: 100 } }, + ], + }, + ], + }, + primaryDisplay: "name", +} + +const baseColumnDef = { + visible: { + type: "boolean", + description: + "Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it.", + }, + readonly: { + type: "boolean", + description: + "When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated.", + }, + order: { + type: "integer", + description: + "A number defining where the column shows up in tables, lowest being first.", + }, + width: { + type: "integer", + description: + "A width for the column, defined in pixels - this affects rendering in tables.", + }, + column: { + type: "array", + description: + "If this is a relationship column, we can set the columns we wish to include", + items: { + type: "object", + properties: { + readonly: { + type: "boolean", + }, + }, + }, + }, +} + +const logicalOperator = { + description: + "When using groups this defines whether all of the filters must match, or only one of them.", + type: "string", + enum: ["all", "any"], +} + +const filterGroup = { + description: "A grouping of filters to be applied.", + type: "array", + items: { + type: "object", + properties: { + logicalOperator, + filters: { + description: "A list of filters to apply", + type: "array", + items: { + type: "object", + properties: { + operator: { + type: "string", + description: + "The type of search operation which is being performed.", + enum: [ + ...Object.values(BasicOperator), + ...Object.values(ArrayOperator), + ...Object.values(RangeOperator), + ], + }, + field: { + type: "string", + description: "The field in the view to perform the search on.", + }, + value: { + description: + "The value to search for - the type will depend on the operator in use.", + oneOf: [ + { type: "string" }, + { type: "number" }, + { type: "boolean" }, + { type: "object" }, + { type: "array" }, + ], + }, + }, + }, + }, + }, + }, +} + +// have to clone to avoid constantly recursive structure - we can't represent this easily +const layeredFilterGroup: any = cloneDeep(filterGroup) +layeredFilterGroup.items.properties.groups = filterGroup + +const viewQuerySchema = { + description: "Search parameters for view", + type: "object", + required: [], + properties: { + logicalOperator, + onEmptyFilter: { + description: + "If no filters match, should the view return all rows, or no rows.", + type: "string", + enum: ["all", "none"], + }, + groups: layeredFilterGroup, + }, +} + +const viewSchema = { + description: "The view to be created/updated.", + type: "object", + required: ["name", "schema", "tableId"], + properties: { + name: { + description: "The name of the view.", + type: "string", + }, + tableId: { + description: "The ID of the table this view is based on.", + type: "string", + }, + type: { + description: "The type of view - standard (empty value) or calculation.", + type: "string", + enum: ["calculation"], + }, + primaryDisplay: { + type: "string", + description: + "A column used to display rows from this view - usually used when rendered in tables.", + }, + query: viewQuerySchema, + sort: { + type: "object", + required: ["field"], + properties: { + field: { + type: "string", + description: "The field from the table/view schema to sort on.", + }, + order: { + type: "string", + description: "The order in which to sort.", + enum: Object.values(SortOrder), + }, + type: { + type: "string", + description: + "The type of sort to perform (by number, or by alphabetically).", + enum: Object.values(SortType), + }, + }, + }, + schema: { + type: "object", + additionalProperties: { + oneOf: [ + { + type: "object", + properties: baseColumnDef, + }, + { + type: "object", + properties: { + calculationType: { + type: "string", + description: + "This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.", + enum: Object.values(CalculationType), + }, + field: { + type: "string", + description: + "The field from the table to perform the calculation on.", + }, + distinct: { + type: "boolean", + description: + "Can be used in tandem with the count calculation type, to count unique entries.", + }, + }, + }, + ], + }, + }, + }, +} + +const viewOutputSchema = { + ...viewSchema, + properties: { + ...viewSchema.properties, + id: { + description: "The ID of the view.", + type: "string", + }, + }, + required: [...viewSchema.required, "id"], +} + +export default new Resource() + .setExamples({ + view: { + value: { + data: view, + }, + }, + views: { + value: { + data: [view], + }, + }, + }) + .setSchemas({ + view: viewSchema, + viewOutput: object({ + data: viewOutputSchema, + }), + viewSearch: object({ + data: { + type: "array", + items: viewOutputSchema, + }, + }), + }) diff --git a/packages/server/src/api/controllers/public/mapping/applications.ts b/packages/server/src/api/controllers/public/mapping/applications.ts index 0b729fc610..74c55e1c7b 100644 --- a/packages/server/src/api/controllers/public/mapping/applications.ts +++ b/packages/server/src/api/controllers/public/mapping/applications.ts @@ -1,6 +1,7 @@ import { Application } from "./types" +import { RequiredKeys } from "@budibase/types" -function application(body: any): Application { +function application(body: any): RequiredKeys { let app = body?.application ? body.application : body return { _id: app.appId, diff --git a/packages/server/src/api/controllers/public/mapping/index.ts b/packages/server/src/api/controllers/public/mapping/index.ts index 0cdcfbbe4b..b765f4bd76 100644 --- a/packages/server/src/api/controllers/public/mapping/index.ts +++ b/packages/server/src/api/controllers/public/mapping/index.ts @@ -3,6 +3,7 @@ import applications from "./applications" import users from "./users" import rows from "./rows" import queries from "./queries" +import views from "./views" export default { ...tables, @@ -10,4 +11,5 @@ export default { ...users, ...rows, ...queries, + ...views, } diff --git a/packages/server/src/api/controllers/public/mapping/queries.ts b/packages/server/src/api/controllers/public/mapping/queries.ts index 481b5f18c4..d0857453f6 100644 --- a/packages/server/src/api/controllers/public/mapping/queries.ts +++ b/packages/server/src/api/controllers/public/mapping/queries.ts @@ -1,6 +1,7 @@ import { Query, ExecuteQuery } from "./types" +import { RequiredKeys } from "@budibase/types" -function query(body: any): Query { +function query(body: any): RequiredKeys { return { _id: body._id, datasourceId: body.datasourceId, diff --git a/packages/server/src/api/controllers/public/mapping/rows.ts b/packages/server/src/api/controllers/public/mapping/rows.ts index c1cba43718..69f376bebf 100644 --- a/packages/server/src/api/controllers/public/mapping/rows.ts +++ b/packages/server/src/api/controllers/public/mapping/rows.ts @@ -1,6 +1,7 @@ import { Row, RowSearch } from "./types" +import { RequiredKeys } from "@budibase/types" -function row(body: any): Row { +function row(body: any): RequiredKeys { delete body._rev // have to input everything, since structure unknown return { diff --git a/packages/server/src/api/controllers/public/mapping/tables.ts b/packages/server/src/api/controllers/public/mapping/tables.ts index 72ed9f1a9a..857feb82ca 100644 --- a/packages/server/src/api/controllers/public/mapping/tables.ts +++ b/packages/server/src/api/controllers/public/mapping/tables.ts @@ -1,6 +1,7 @@ import { Table } from "./types" +import { RequiredKeys } from "@budibase/types" -function table(body: any): Table { +function table(body: any): RequiredKeys { return { _id: body._id, name: body.name, diff --git a/packages/server/src/api/controllers/public/mapping/types.ts b/packages/server/src/api/controllers/public/mapping/types.ts index 9fea9b7213..6cbcfddb92 100644 --- a/packages/server/src/api/controllers/public/mapping/types.ts +++ b/packages/server/src/api/controllers/public/mapping/types.ts @@ -9,6 +9,9 @@ export type CreateApplicationParams = components["schemas"]["application"] export type Table = components["schemas"]["tableOutput"]["data"] export type CreateTableParams = components["schemas"]["table"] +export type View = components["schemas"]["viewOutput"]["data"] +export type CreateViewParams = components["schemas"]["view"] + export type Row = components["schemas"]["rowOutput"]["data"] export type RowSearch = components["schemas"]["searchOutput"] export type CreateRowParams = components["schemas"]["row"] diff --git a/packages/server/src/api/controllers/public/mapping/users.ts b/packages/server/src/api/controllers/public/mapping/users.ts index 2a158bede9..232c81cec0 100644 --- a/packages/server/src/api/controllers/public/mapping/users.ts +++ b/packages/server/src/api/controllers/public/mapping/users.ts @@ -1,6 +1,7 @@ import { User } from "./types" +import { RequiredKeys } from "@budibase/types" -function user(body: any): User { +function user(body: any): RequiredKeys { return { _id: body._id, email: body.email, diff --git a/packages/server/src/api/controllers/public/mapping/views.ts b/packages/server/src/api/controllers/public/mapping/views.ts new file mode 100644 index 0000000000..9ee1fe42d5 --- /dev/null +++ b/packages/server/src/api/controllers/public/mapping/views.ts @@ -0,0 +1,32 @@ +import { View } from "./types" +import { ViewV2, Ctx, RequiredKeys } from "@budibase/types" +import { dataFilters } from "@budibase/shared-core" + +function view(body: ViewV2): RequiredKeys { + return { + id: body.id, + tableId: body.tableId, + type: body.type, + name: body.name, + schema: body.schema!, + primaryDisplay: body.primaryDisplay, + query: dataFilters.buildQuery(body.query), + sort: body.sort, + } +} + +function mapView(ctx: Ctx<{ data: ViewV2 }>): { data: View } { + return { + data: view(ctx.body.data), + } +} + +function mapViews(ctx: Ctx<{ data: ViewV2[] }>): { data: View[] } { + const views = ctx.body.data.map((body: ViewV2) => view(body)) + return { data: views } +} + +export default { + mapView, + mapViews, +} diff --git a/packages/server/src/api/controllers/public/rows.ts b/packages/server/src/api/controllers/public/rows.ts index 16403b06c9..3c9cbf0ddd 100644 --- a/packages/server/src/api/controllers/public/rows.ts +++ b/packages/server/src/api/controllers/public/rows.ts @@ -22,13 +22,13 @@ export function fixRow(row: Row, params: any) { return row } -export async function search(ctx: UserCtx, next: Next) { +function buildSearchRequestBody(ctx: UserCtx) { let { sort, paginate, bookmark, limit, query } = ctx.request.body // update the body to the correct format of the internal search if (!sort) { sort = {} } - ctx.request.body = { + return { sort: sort.column, sortType: sort.type, sortOrder: sort.order, @@ -37,10 +37,23 @@ export async function search(ctx: UserCtx, next: Next) { limit, query, } +} + +export async function search(ctx: UserCtx, next: Next) { + ctx.request.body = buildSearchRequestBody(ctx) await rowController.search(ctx) await next() } +export async function viewSearch(ctx: UserCtx, next: Next) { + ctx.request.body = buildSearchRequestBody(ctx) + ctx.params = { + viewId: ctx.params.viewId, + } + await rowController.views.searchView(ctx) + await next() +} + export async function create(ctx: UserCtx, next: Next) { ctx.request.body = fixRow(ctx.request.body, ctx.params) await rowController.save(ctx) @@ -79,4 +92,5 @@ export default { update, destroy, search, + viewSearch, } diff --git a/packages/server/src/api/controllers/public/views.ts b/packages/server/src/api/controllers/public/views.ts new file mode 100644 index 0000000000..5b08f39e36 --- /dev/null +++ b/packages/server/src/api/controllers/public/views.ts @@ -0,0 +1,95 @@ +import { search as stringSearch } from "./utils" +import * as controller from "../view" +import { ViewV2, UserCtx, UISearchFilter, PublicAPIView } from "@budibase/types" +import { Next } from "koa" +import { merge } from "lodash" + +function viewRequest(view: PublicAPIView, params?: { viewId: string }) { + const viewV2: ViewV2 = view + if (!viewV2) { + return viewV2 + } + if (params?.viewId) { + viewV2.id = params.viewId + } + if (!view.query) { + viewV2.query = {} + } else { + // public API only has one form of query + viewV2.queryUI = viewV2.query as UISearchFilter + } + viewV2.version = 2 + return viewV2 +} + +function viewResponse(view: ViewV2): PublicAPIView { + // remove our internal structure - always un-necessary + delete view.query + return { + ...view, + query: view.queryUI, + } +} + +function viewsResponse(views: ViewV2[]): PublicAPIView[] { + return views.map(viewResponse) +} + +export async function search(ctx: UserCtx, next: Next) { + const { name } = ctx.request.body + await controller.v2.fetch(ctx) + ctx.body.data = viewsResponse(stringSearch(ctx.body.data, name)) + await next() +} + +export async function create(ctx: UserCtx, next: Next) { + ctx = merge(ctx, { + request: { + body: viewRequest(ctx.request.body), + }, + }) + await controller.v2.create(ctx) + ctx.body.data = viewResponse(ctx.body.data) + await next() +} + +export async function read(ctx: UserCtx, next: Next) { + ctx = merge(ctx, { + params: { + viewId: ctx.params.viewId, + }, + }) + await controller.v2.get(ctx) + ctx.body.data = viewResponse(ctx.body.data) + await next() +} + +export async function update(ctx: UserCtx, next: Next) { + const viewId = ctx.params.viewId + ctx = merge(ctx, { + request: { + body: { + data: viewRequest(ctx.request.body, { viewId }), + }, + }, + params: { + viewId, + }, + }) + await controller.v2.update(ctx) + ctx.body.data = viewResponse(ctx.body.data) + await next() +} + +export async function destroy(ctx: UserCtx, next: Next) { + await controller.v2.remove(ctx) + await next() +} + +export default { + create, + read, + update, + destroy, + search, +} diff --git a/packages/server/src/api/controllers/query/import/sources/curl.ts b/packages/server/src/api/controllers/query/import/sources/curl.ts index ba85d82be0..5742d254af 100644 --- a/packages/server/src/api/controllers/query/import/sources/curl.ts +++ b/packages/server/src/api/controllers/query/import/sources/curl.ts @@ -4,7 +4,7 @@ import { URL } from "url" const curlconverter = require("curlconverter") -const parseCurl = (data: string): any => { +const parseCurl = (data: string): Promise => { const curlJson = curlconverter.toJsonString(data) return JSON.parse(curlJson) } @@ -53,8 +53,7 @@ export class Curl extends ImportSource { isSupported = async (data: string): Promise => { try { - const curl = parseCurl(data) - this.curl = curl + this.curl = parseCurl(data) } catch (err) { return false } diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts index 54f672c3f3..15c60bcf47 100644 --- a/packages/server/src/api/controllers/query/index.ts +++ b/packages/server/src/api/controllers/query/index.ts @@ -23,6 +23,7 @@ import { } from "@budibase/types" import { ValidQueryNameRegex, utils as JsonUtils } from "@budibase/shared-core" import { findHBSBlocks } from "@budibase/string-templates" +import { ObjectId } from "mongodb" const Runner = new Thread(ThreadType.QUERY, { timeoutMs: env.QUERY_THREAD_TIMEOUT, @@ -223,6 +224,8 @@ export async function preview( } else { fieldMetadata = makeQuerySchema(FieldType.ARRAY, key) } + } else if (field instanceof ObjectId) { + fieldMetadata = makeQuerySchema(FieldType.STRING, key) } else { fieldMetadata = makeQuerySchema(FieldType.JSON, key) } diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index b8d01424f2..02ac871de0 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -50,6 +50,7 @@ export async function searchView( result.rows.forEach(r => (r._viewId = view.id)) ctx.body = result } + function getSortOptions(request: SearchViewRowRequest, view: ViewV2) { if (request.sort) { return { diff --git a/packages/server/src/api/controllers/table/utils.ts b/packages/server/src/api/controllers/table/utils.ts index 04e77fbe62..d6b29dc12c 100644 --- a/packages/server/src/api/controllers/table/utils.ts +++ b/packages/server/src/api/controllers/table/utils.ts @@ -15,12 +15,11 @@ import { getViews, saveView } from "../view/utils" import viewTemplate from "../view/viewBuilder" import { cloneDeep } from "lodash/fp" import { quotas } from "@budibase/pro" -import { context, events, features, HTTPError } from "@budibase/backend-core" +import { context, events, HTTPError } from "@budibase/backend-core" import { AutoFieldSubType, Database, Datasource, - FeatureFlag, FieldSchema, FieldType, NumberFieldMetadata, @@ -336,9 +335,8 @@ class TableSaveFunctions { importRows: this.importRows, userId: this.userId, }) - if (await features.flags.isEnabled(FeatureFlag.SQS)) { - await sdk.tables.sqs.addTable(table) - } + + await sdk.tables.sqs.addTable(table) return table } @@ -530,9 +528,8 @@ export async function internalTableCleanup(table: Table, rows?: Row[]) { if (rows) { await AttachmentCleanup.tableDelete(table, rows) } - if (await features.flags.isEnabled(FeatureFlag.SQS)) { - await sdk.tables.sqs.removeTable(table) - } + + await sdk.tables.sqs.removeTable(table) } const _TableSaveFunctions = TableSaveFunctions diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts index 753125e0f6..986764a697 100644 --- a/packages/server/src/api/controllers/view/viewsV2.ts +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -12,6 +12,7 @@ import { RelationSchemaField, ViewFieldMetadata, CalculationType, + ViewFetchResponseEnriched, CountDistinctCalculationFieldMetadata, CountCalculationFieldMetadata, } from "@budibase/types" @@ -125,6 +126,12 @@ export async function get(ctx: Ctx) { } } +export async function fetch(ctx: Ctx) { + ctx.body = { + data: await sdk.views.getAllEnriched(), + } +} + export async function create(ctx: Ctx) { const view = ctx.request.body const { tableId } = view diff --git a/packages/server/src/api/routes/public/index.ts b/packages/server/src/api/routes/public/index.ts index b665e83f6f..531192811c 100644 --- a/packages/server/src/api/routes/public/index.ts +++ b/packages/server/src/api/routes/public/index.ts @@ -4,19 +4,21 @@ import queryEndpoints from "./queries" import tableEndpoints from "./tables" import rowEndpoints from "./rows" import userEndpoints from "./users" +import viewEndpoints from "./views" import roleEndpoints from "./roles" import authorized from "../../../middleware/authorized" import publicApi from "../../../middleware/publicApi" import { paramResource, paramSubResource } from "../../../middleware/resourceId" -import { PermissionType, PermissionLevel } from "@budibase/types" +import { PermissionLevel, PermissionType } from "@budibase/types" import { CtxFn } from "./utils/Endpoint" import mapperMiddleware from "./middleware/mapper" import env from "../../../environment" +import { middleware, redis } from "@budibase/backend-core" +import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils" +import cors from "@koa/cors" // below imports don't have declaration files const Router = require("@koa/router") const { RateLimit, Stores } = require("koa2-ratelimit") -import { middleware, redis } from "@budibase/backend-core" -import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils" interface KoaRateLimitOptions { socket: { @@ -81,6 +83,7 @@ const publicRouter = new Router({ if (limiter && !env.isDev()) { publicRouter.use(limiter) } +publicRouter.use(cors()) function addMiddleware( endpoints: any, @@ -149,6 +152,7 @@ applyAdminRoutes(metricEndpoints) applyAdminRoutes(roleEndpoints) applyRoutes(appEndpoints, PermissionType.APP, "appId") applyRoutes(tableEndpoints, PermissionType.TABLE, "tableId") +applyRoutes(viewEndpoints, PermissionType.VIEW, "viewId") applyRoutes(userEndpoints, PermissionType.USER, "userId") applyRoutes(queryEndpoints, PermissionType.QUERY, "queryId") // needs to be applied last for routing purposes, don't override other endpoints diff --git a/packages/server/src/api/routes/public/middleware/mapper.ts b/packages/server/src/api/routes/public/middleware/mapper.ts index 03feb6cc5c..9d0cae5d61 100644 --- a/packages/server/src/api/routes/public/middleware/mapper.ts +++ b/packages/server/src/api/routes/public/middleware/mapper.ts @@ -1,9 +1,10 @@ import { Ctx } from "@budibase/types" import mapping from "../../../controllers/public/mapping" -enum Resources { +enum Resource { APPLICATION = "applications", TABLES = "tables", + VIEWS = "views", ROWS = "rows", USERS = "users", QUERIES = "queries", @@ -15,7 +16,7 @@ function isAttachment(ctx: Ctx) { } function isArrayResponse(ctx: Ctx) { - return ctx.url.endsWith(Resources.SEARCH) || Array.isArray(ctx.body) + return ctx.url.endsWith(Resource.SEARCH) || Array.isArray(ctx.body) } function noResponse(ctx: Ctx) { @@ -38,6 +39,14 @@ function processTables(ctx: Ctx) { } } +function processViews(ctx: Ctx) { + if (isArrayResponse(ctx)) { + return mapping.mapViews(ctx) + } else { + return mapping.mapView(ctx) + } +} + function processRows(ctx: Ctx) { if (isArrayResponse(ctx)) { return mapping.mapRowSearch(ctx) @@ -71,20 +80,27 @@ export default async (ctx: Ctx, next: any) => { let body = {} switch (urlParts[0]) { - case Resources.APPLICATION: + case Resource.APPLICATION: body = processApplications(ctx) break - case Resources.TABLES: - if (urlParts[2] === Resources.ROWS) { + case Resource.TABLES: + if (urlParts[2] === Resource.ROWS) { body = processRows(ctx) } else { body = processTables(ctx) } break - case Resources.USERS: + case Resource.VIEWS: + if (urlParts[2] === Resource.ROWS) { + body = processRows(ctx) + } else { + body = processViews(ctx) + } + break + case Resource.USERS: body = processUsers(ctx) break - case Resources.QUERIES: + case Resource.QUERIES: body = processQueries(ctx) break } diff --git a/packages/server/src/api/routes/public/rows.ts b/packages/server/src/api/routes/public/rows.ts index 80ad6d6434..2fb81d4601 100644 --- a/packages/server/src/api/routes/public/rows.ts +++ b/packages/server/src/api/routes/public/rows.ts @@ -1,4 +1,4 @@ -import controller from "../../controllers/public/rows" +import controller, { viewSearch } from "../../controllers/public/rows" import Endpoint from "./utils/Endpoint" import { externalSearchValidator } from "../utils/validators" @@ -168,4 +168,40 @@ read.push( ).addMiddleware(externalSearchValidator()) ) +/** + * @openapi + * /views/{viewId}/rows/search: + * post: + * operationId: rowViewSearch + * summary: Search for rows in a view + * tags: + * - rows + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/rowSearch' + * responses: + * 200: + * description: The response will contain an array of rows that match the search parameters. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/searchOutput' + * examples: + * search: + * $ref: '#/components/examples/rows' + */ +read.push( + new Endpoint( + "post", + "/views/:viewId/rows/search", + controller.viewSearch + ).addMiddleware(externalSearchValidator()) +) + export default { read, write } diff --git a/packages/server/src/api/routes/public/tests/Request.ts b/packages/server/src/api/routes/public/tests/Request.ts index 92a4996124..3dfa11c0b3 100644 --- a/packages/server/src/api/routes/public/tests/Request.ts +++ b/packages/server/src/api/routes/public/tests/Request.ts @@ -1,13 +1,24 @@ -import { User, Table, SearchFilters, Row } from "@budibase/types" +import { + User, + Table, + SearchFilters, + Row, + ViewV2Schema, + ViewV2, + ViewV2Type, + PublicAPIView, +} from "@budibase/types" import { HttpMethod, MakeRequestResponse, generateMakeRequest } from "./utils" import TestConfiguration from "../../../../tests/utilities/TestConfiguration" -import { Expectations } from "../../../../tests/utilities/api/base" type RequestOpts = { internal?: boolean; appId?: string } +type Response = { data: T } + export interface PublicAPIExpectations { status?: number body?: Record + headers?: Record } export class PublicAPIRequest { @@ -15,6 +26,7 @@ export class PublicAPIRequest { private appId: string | undefined tables: PublicTableAPI + views: PublicViewAPI rows: PublicRowAPI apiKey: string @@ -28,6 +40,7 @@ export class PublicAPIRequest { this.appId = appId this.tables = new PublicTableAPI(this) this.rows = new PublicRowAPI(this) + this.views = new PublicViewAPI(this) } static async init(config: TestConfiguration, user: User, opts?: RequestOpts) { @@ -59,6 +72,12 @@ export class PublicAPIRequest { if (expectations?.body) { expect(res.body).toEqual(expectations?.body) } + if (expectations?.headers) { + for (let [header, value] of Object.entries(expectations.headers)) { + const found = res.headers[header] + expect(found?.toLowerCase()).toEqual(value) + } + } return res.body } } @@ -73,9 +92,16 @@ export class PublicTableAPI { async create( table: Table, expectations?: PublicAPIExpectations - ): Promise<{ data: Table }> { + ): Promise> { return this.request.send("post", "/tables", table, expectations) } + + async search( + name: string, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send("post", "/tables/search", { name }, expectations) + } } export class PublicRowAPI { @@ -85,11 +111,24 @@ export class PublicRowAPI { this.request = request } + async create( + tableId: string, + row: Row, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send( + "post", + `/tables/${tableId}/rows`, + row, + expectations + ) + } + async search( tableId: string, query: SearchFilters, expectations?: PublicAPIExpectations - ): Promise<{ data: Row[] }> { + ): Promise> { return this.request.send( "post", `/tables/${tableId}/rows/search`, @@ -99,4 +138,75 @@ export class PublicRowAPI { expectations ) } + + async viewSearch( + viewId: string, + query: SearchFilters, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send( + "post", + `/views/${viewId}/rows/search`, + { + query, + }, + expectations + ) + } +} + +export class PublicViewAPI { + request: PublicAPIRequest + + constructor(request: PublicAPIRequest) { + this.request = request + } + + async create( + view: Omit, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send("post", "/views", view, expectations) + } + + async update( + viewId: string, + view: Omit, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send("put", `/views/${viewId}`, view, expectations) + } + + async destroy( + viewId: string, + expectations?: PublicAPIExpectations + ): Promise { + return this.request.send( + "delete", + `/views/${viewId}`, + undefined, + expectations + ) + } + + async find( + viewId: string, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send("get", `/views/${viewId}`, undefined, expectations) + } + + async search( + viewName: string, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send( + "post", + "/views/search", + { + name: viewName, + }, + expectations + ) + } } diff --git a/packages/server/src/api/routes/public/tests/cors.spec.ts b/packages/server/src/api/routes/public/tests/cors.spec.ts new file mode 100644 index 0000000000..1a5895575d --- /dev/null +++ b/packages/server/src/api/routes/public/tests/cors.spec.ts @@ -0,0 +1,21 @@ +import * as setup from "../../tests/utilities" +import { PublicAPIRequest } from "./Request" + +describe("check public API security", () => { + const config = setup.getConfig() + let request: PublicAPIRequest + + beforeAll(async () => { + await config.init() + request = await PublicAPIRequest.init(config, await config.globalUser()) + }) + + it("should have Access-Control-Allow-Origin set to *", async () => { + await request.tables.search("", { + status: 200, + headers: { + "access-control-allow-origin": "*", + }, + }) + }) +}) diff --git a/packages/server/src/api/routes/public/tests/views.spec.ts b/packages/server/src/api/routes/public/tests/views.spec.ts new file mode 100644 index 0000000000..1cbc383f7e --- /dev/null +++ b/packages/server/src/api/routes/public/tests/views.spec.ts @@ -0,0 +1,95 @@ +import * as setup from "../../tests/utilities" +import { basicTable } from "../../../../tests/utilities/structures" +import { BasicOperator, Table, UILogicalOperator } from "@budibase/types" +import { PublicAPIRequest } from "./Request" +import { generator } from "@budibase/backend-core/tests" + +describe("check public API security", () => { + const config = setup.getConfig() + let request: PublicAPIRequest, table: Table + + beforeAll(async () => { + await config.init() + request = await PublicAPIRequest.init(config, await config.globalUser()) + table = (await request.tables.create(basicTable())).data + }) + + function baseView() { + return { + name: generator.word(), + tableId: table._id!, + query: {}, + schema: { + name: { + readonly: true, + visible: true, + }, + }, + } + } + + it("should be able to create a view", async () => { + await request.views.create(baseView(), { status: 201 }) + }) + + it("should be able to update a view", async () => { + const view = await request.views.create(baseView(), { status: 201 }) + const response = await request.views.update(view.data.id, { + ...view.data, + name: "new name", + }) + }) + + it("should be able to search views", async () => { + const viewName = "view to search for" + const view = await request.views.create( + { + ...baseView(), + name: viewName, + }, + { status: 201 } + ) + const results = await request.views.search(viewName, { + status: 200, + }) + expect(results.data.length).toEqual(1) + expect(results.data[0].id).toEqual(view.data.id) + }) + + it("should be able to delete a view", async () => { + const view = await request.views.create(baseView(), { status: 201 }) + const result = await request.views.destroy(view.data.id, { status: 204 }) + expect(result).toBeDefined() + }) + + it("should be able to search rows through a view", async () => { + const row1 = await request.rows.create( + table._id!, + { name: "hello world" }, + { status: 200 } + ) + await request.rows.create(table._id!, { name: "foo bar" }, { status: 200 }) + const response = await request.views.create( + { + ...baseView(), + query: { + logicalOperator: UILogicalOperator.ANY, + groups: [ + { + filters: [ + { + operator: BasicOperator.STRING, + field: "name", + value: "hello", + }, + ], + }, + ], + }, + }, + { status: 201 } + ) + const results = await request.rows.viewSearch(response.data.id, {}) + expect(results.data.length).toEqual(1) + }) +}) diff --git a/packages/server/src/api/routes/public/views.ts b/packages/server/src/api/routes/public/views.ts new file mode 100644 index 0000000000..139d79be1f --- /dev/null +++ b/packages/server/src/api/routes/public/views.ts @@ -0,0 +1,165 @@ +import controller from "../../controllers/public/views" +import Endpoint from "./utils/Endpoint" +import { viewValidator, nameValidator } from "../utils/validators" + +const read = [], + write = [] + +/** + * @openapi + * /views: + * post: + * operationId: viewCreate + * summary: Create a view + * description: Create a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/appId' + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/view' + * examples: + * view: + * $ref: '#/components/examples/view' + * responses: + * 200: + * description: Returns the created view, including the ID which has been generated for it. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push( + new Endpoint("post", "/views", controller.create).addMiddleware( + viewValidator() + ) +) + +/** + * @openapi + * /views/{viewId}: + * put: + * operationId: viewUpdate + * summary: Update a view + * description: Update a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/view' + * examples: + * view: + * $ref: '#/components/examples/view' + * responses: + * 200: + * description: Returns the updated view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push( + new Endpoint("put", "/views/:viewId", controller.update).addMiddleware( + viewValidator() + ) +) + +/** + * @openapi + * /views/{viewId}: + * delete: + * operationId: viewDestroy + * summary: Delete a view + * description: Delete a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * responses: + * 200: + * description: Returns the deleted view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push(new Endpoint("delete", "/views/:viewId", controller.destroy)) + +/** + * @openapi + * /views/{viewId}: + * get: + * operationId: viewGetById + * summary: Retrieve a view + * description: Lookup a view, this could be internal or external. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * responses: + * 200: + * description: Returns the retrieved view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +read.push(new Endpoint("get", "/views/:viewId", controller.read)) + +/** + * @openapi + * /views/search: + * post: + * operationId: viewSearch + * summary: Search for views + * description: Based on view properties (currently only name) search for views. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/appId' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/nameSearch' + * responses: + * 200: + * description: Returns the found views, based on the search parameters. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewSearch' + * examples: + * views: + * $ref: '#/components/examples/views' + */ +read.push( + new Endpoint("post", "/views/search", controller.search).addMiddleware( + nameValidator() + ) +) + +export default { read, write } diff --git a/packages/server/src/api/routes/tests/application.spec.ts b/packages/server/src/api/routes/tests/application.spec.ts index 6d85cdbda9..1511c1aa61 100644 --- a/packages/server/src/api/routes/tests/application.spec.ts +++ b/packages/server/src/api/routes/tests/application.spec.ts @@ -16,7 +16,7 @@ jest.mock("../../../utilities/redis", () => ({ import { checkBuilderEndpoint } from "./utilities/TestFunctions" import * as setup from "./utilities" import { AppStatus } from "../../../db/utils" -import { events, utils, context, features } from "@budibase/backend-core" +import { events, utils, context } from "@budibase/backend-core" import env from "../../../environment" import { type App, BuiltinPermissionID } from "@budibase/types" import tk from "timekeeper" @@ -355,21 +355,6 @@ describe("/applications", () => { expect(events.app.deleted).toHaveBeenCalledTimes(1) expect(events.app.unpublished).toHaveBeenCalledTimes(1) }) - - it("should be able to delete an app after SQS has been set but app hasn't been migrated", async () => { - const prodAppId = app.appId.replace("_dev", "") - nock("http://localhost:10000") - .delete(`/api/global/roles/${prodAppId}`) - .reply(200, {}) - - await features.testutils.withFeatureFlags( - "*", - { SQS: true }, - async () => { - await config.api.application.delete(app.appId) - } - ) - }) }) describe("POST /api/applications/:appId/duplicate", () => { diff --git a/packages/server/src/api/routes/tests/datasource.spec.ts b/packages/server/src/api/routes/tests/datasource.spec.ts index 7545253181..f3fac5b99b 100644 --- a/packages/server/src/api/routes/tests/datasource.spec.ts +++ b/packages/server/src/api/routes/tests/datasource.spec.ts @@ -19,8 +19,7 @@ import { } from "@budibase/types" import { DatabaseName, - getDatasource, - knexClient, + datasourceDescribe, } from "../../../integrations/tests/utils" import { tableForDatasource } from "../../../tests/utilities/structures" import nock from "nock" @@ -69,7 +68,7 @@ describe("/datasources", () => { { status: 500, body: { - message: "No datasource implementation found.", + message: 'No datasource implementation found called: "invalid"', }, } ) @@ -163,21 +162,26 @@ describe("/datasources", () => { }) }) }) +}) - describe.each([ - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], - [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], - ])("%s", (_, dsProvider) => { +const descriptions = datasourceDescribe({ + exclude: [DatabaseName.MONGODB, DatabaseName.SQS], +}) + +if (descriptions.length) { + describe.each(descriptions)("$dbName", ({ config, dsProvider }) => { + let datasource: Datasource let rawDatasource: Datasource let client: Knex beforeEach(async () => { - rawDatasource = await dsProvider - datasource = await config.api.datasource.create(rawDatasource) - client = await knexClient(rawDatasource) + const ds = await dsProvider() + rawDatasource = ds.rawDatasource! + datasource = ds.datasource! + client = ds.client! + + jest.clearAllMocks() + nock.cleanAll() }) describe("get", () => { @@ -492,4 +496,4 @@ describe("/datasources", () => { }) }) }) -}) +} diff --git a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts index 4e9a1e5548..44b21e0350 100644 --- a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts +++ b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts @@ -3,961 +3,952 @@ import { Operation, Query, QueryPreview, - SourceName, TableSourceType, } from "@budibase/types" -import * as setup from "../utilities" import { DatabaseName, - getDatasource, - knexClient, + datasourceDescribe, } from "../../../../integrations/tests/utils" import { Expectations } from "src/tests/utilities/api/base" import { events } from "@budibase/backend-core" import { Knex } from "knex" +import { generator } from "@budibase/backend-core/tests" -describe.each( - [ - DatabaseName.POSTGRES, - DatabaseName.MYSQL, - DatabaseName.SQL_SERVER, - DatabaseName.MARIADB, - DatabaseName.ORACLE, - ].map(name => [name, getDatasource(name)]) -)("queries (%s)", (dbName, dsProvider) => { - const config = setup.getConfig() - const isOracle = dbName === DatabaseName.ORACLE - const isMsSQL = dbName === DatabaseName.SQL_SERVER - const isPostgres = dbName === DatabaseName.POSTGRES - const mainTableName = "test_table" +const descriptions = datasourceDescribe({ + exclude: [DatabaseName.MONGODB, DatabaseName.SQS], +}) - let rawDatasource: Datasource - let datasource: Datasource - let client: Knex +if (descriptions.length) { + describe.each(descriptions)( + "queries ($dbName)", + ({ config, dsProvider, isOracle, isMSSQL, isPostgres }) => { + let rawDatasource: Datasource + let datasource: Datasource + let client: Knex - async function createQuery( - query: Partial, - expectations?: Expectations - ): Promise { - const defaultQuery: Query = { - datasourceId: datasource._id!, - name: "New Query", - parameters: [], - fields: {}, - schema: {}, - queryVerb: "read", - transformer: "return data", - readable: true, - } - if (query.fields?.sql && typeof query.fields.sql !== "string") { - throw new Error("Unable to create with knex structure in 'sql' field") - } - return await config.api.query.save( - { ...defaultQuery, ...query }, - expectations - ) - } + let tableName: string - beforeAll(async () => { - await config.init() - }) - - beforeEach(async () => { - rawDatasource = await dsProvider - datasource = await config.api.datasource.create(rawDatasource) - - // The Datasource API doesn ot return the password, but we need it later to - // connect to the underlying database, so we fill it back in here. - datasource.config!.password = rawDatasource.config!.password - - client = await knexClient(rawDatasource) - - await client.schema.dropTableIfExists(mainTableName) - await client.schema.createTable(mainTableName, table => { - table.increments("id").primary() - table.string("name") - table.timestamp("birthday") - table.integer("number") - }) - - await client(mainTableName).insert([ - { name: "one" }, - { name: "two" }, - { name: "three" }, - { name: "four" }, - { name: "five" }, - ]) - - jest.clearAllMocks() - }) - - afterEach(async () => { - const ds = await config.api.datasource.get(datasource._id!) - await config.api.datasource.delete(ds) - }) - - afterAll(async () => { - setup.afterAll() - }) - - describe("query admin", () => { - describe("create", () => { - it("should be able to create a query", async () => { - const query = await createQuery({ - name: "New Query", - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - expect(query).toMatchObject({ + async function createQuery( + query: Partial, + expectations?: Expectations + ): Promise { + const defaultQuery: Query = { datasourceId: datasource._id!, name: "New Query", parameters: [], - fields: { - sql: client(mainTableName).select("*").toString(), - }, + fields: {}, schema: {}, queryVerb: "read", transformer: "return data", readable: true, - createdAt: expect.any(String), - updatedAt: expect.any(String), - }) - - expect(events.query.created).toHaveBeenCalledTimes(1) - expect(events.query.updated).not.toHaveBeenCalled() - }) - }) - - describe("update", () => { - it("should be able to update a query", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - jest.clearAllMocks() - - const updatedQuery = await config.api.query.save({ - ...query, - name: "Updated Query", - fields: { - sql: client(mainTableName).where({ id: 1 }).toString(), - }, - }) - - expect(updatedQuery).toMatchObject({ - datasourceId: datasource._id!, - name: "Updated Query", - parameters: [], - fields: { - sql: client(mainTableName).where({ id: 1 }).toString(), - }, - schema: {}, - queryVerb: "read", - transformer: "return data", - readable: true, - }) - - expect(events.query.created).not.toHaveBeenCalled() - expect(events.query.updated).toHaveBeenCalledTimes(1) - }) - }) - - describe("delete", () => { - it("should be able to delete a query", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - await config.api.query.delete(query) - await config.api.query.get(query._id!, { status: 404 }) - - const queries = await config.api.query.fetch() - expect(queries).not.toContainEqual(query) - - expect(events.query.deleted).toHaveBeenCalledTimes(1) - expect(events.query.deleted).toHaveBeenCalledWith(datasource, query) - }) - }) - - describe("read", () => { - it("should be able to list queries", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - const queries = await config.api.query.fetch() - expect(queries).toContainEqual(query) - }) - - it("should strip sensitive fields for prod apps", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").toString(), - }, - }) - - await config.api.application.publish(config.getAppId()) - const prodQuery = await config.api.query.getProd(query._id!) - - expect(prodQuery._id).toEqual(query._id) - expect(prodQuery.fields).toBeUndefined() - expect(prodQuery.parameters).toBeUndefined() - expect(prodQuery.schema).toBeDefined() - }) - - isPostgres && - it("should be able to handle a JSON aggregate with newlines", async () => { - const jsonStatement = `COALESCE(json_build_object('name', name),'{"name":{}}'::json)` - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .select([ - "*", - client.raw( - `${jsonStatement} as json,\n${jsonStatement} as json2` - ), - ]) - .toString(), - }, - }) - const res = await config.api.query.execute( - query._id!, - {}, - { - status: 200, - } - ) - expect(res).toBeDefined() - }) - }) - }) - - describe("preview", () => { - it("should be able to preview a query", async () => { - const request: QueryPreview = { - datasourceId: datasource._id!, - queryVerb: "read", - fields: { - sql: client(mainTableName).where({ id: 1 }).toString(), - }, - parameters: [], - transformer: "return data", - name: datasource.name!, - schema: {}, - readable: true, - } - const response = await config.api.query.preview(request) - expect(response.schema).toEqual({ - birthday: { - name: "birthday", - type: "string", - }, - id: { - name: "id", - type: "number", - }, - name: { - name: "name", - type: "string", - }, - number: { - name: "number", - type: "string", - }, - }) - expect(response.rows).toEqual([ - { - birthday: null, - id: 1, - name: "one", - number: null, - }, - ]) - expect(events.query.previewed).toHaveBeenCalledTimes(1) - }) - - it("should update schema when column type changes from number to string", async () => { - const tableName = "schema_change_test" - await client.schema.dropTableIfExists(tableName) - - await client.schema.createTable(tableName, table => { - table.increments("id").primary() - table.string("name") - table.integer("data") - }) - - await client(tableName).insert({ - name: "test", - data: 123, - }) - - const firstPreview = await config.api.query.preview({ - datasourceId: datasource._id!, - name: "Test Query", - queryVerb: "read", - fields: { - sql: client(tableName).select("*").toString(), - }, - parameters: [], - transformer: "return data", - schema: {}, - readable: true, - }) - - expect(firstPreview.schema).toEqual( - expect.objectContaining({ - data: { type: "number", name: "data" }, - }) - ) - - await client(tableName).delete() - await client.schema.alterTable(tableName, table => { - table.string("data").alter() - }) - - await client(tableName).insert({ - name: "test", - data: "string value", - }) - - const secondPreview = await config.api.query.preview({ - datasourceId: datasource._id!, - name: "Test Query", - queryVerb: "read", - fields: { - sql: client(tableName).select("*").toString(), - }, - parameters: [], - transformer: "return data", - schema: firstPreview.schema, - readable: true, - }) - - expect(secondPreview.schema).toEqual( - expect.objectContaining({ - data: { type: "string", name: "data" }, - }) - ) - }) - - it("should work with static variables", async () => { - await config.api.datasource.update({ - ...datasource, - config: { - ...datasource.config, - staticVariables: { - foo: "bar", - }, - }, - }) - - const request: QueryPreview = { - datasourceId: datasource._id!, - queryVerb: "read", - fields: { - sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, - }, - parameters: [], - transformer: "return data", - name: datasource.name!, - schema: {}, - readable: true, + } + if (query.fields?.sql && typeof query.fields.sql !== "string") { + throw new Error("Unable to create with knex structure in 'sql' field") + } + return await config.api.query.save( + { ...defaultQuery, ...query }, + expectations + ) } - const response = await config.api.query.preview(request) - - let key = isOracle ? "FOO" : "foo" - expect(response.schema).toEqual({ - [key]: { - name: key, - type: "string", - }, + beforeAll(async () => { + const ds = await dsProvider() + rawDatasource = ds.rawDatasource! + datasource = ds.datasource! + client = ds.client! }) - expect(response.rows).toEqual([ - { - [key]: "bar", - }, - ]) - }) + beforeEach(async () => { + // The Datasource API doesn ot return the password, but we need it later to + // connect to the underlying database, so we fill it back in here. + datasource.config!.password = rawDatasource.config!.password - it("should work with dynamic variables", async () => { - const basedOnQuery = await createQuery({ - fields: { - sql: client(mainTableName).select("name").where({ id: 1 }).toString(), - }, - }) + tableName = generator.guid() - await config.api.datasource.update({ - ...datasource, - config: { - ...datasource.config, - dynamicVariables: [ - { - queryId: basedOnQuery._id!, - name: "foo", - value: "{{ data[0].name }}", - }, - ], - }, - }) - - const preview = await config.api.query.preview({ - datasourceId: datasource._id!, - queryVerb: "read", - fields: { - sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, - }, - parameters: [], - transformer: "return data", - name: datasource.name!, - schema: {}, - readable: true, - }) - - let key = isOracle ? "FOO" : "foo" - expect(preview.schema).toEqual({ - [key]: { - name: key, - type: "string", - }, - }) - - expect(preview.rows).toEqual([ - { - [key]: "one", - }, - ]) - }) - - it("should handle the dynamic base query being deleted", async () => { - const basedOnQuery = await createQuery({ - fields: { - sql: client(mainTableName).select("name").where({ id: 1 }).toString(), - }, - }) - - await config.api.datasource.update({ - ...datasource, - config: { - ...datasource.config, - dynamicVariables: [ - { - queryId: basedOnQuery._id!, - name: "foo", - value: "{{ data[0].name }}", - }, - ], - }, - }) - - await config.api.query.delete(basedOnQuery) - - const preview = await config.api.query.preview({ - datasourceId: datasource._id!, - queryVerb: "read", - fields: { - sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, - }, - parameters: [], - transformer: "return data", - name: datasource.name!, - schema: {}, - readable: true, - }) - - let key = isOracle ? "FOO" : "foo" - expect(preview.schema).toEqual({ - [key]: { - name: key, - type: "string", - }, - }) - - expect(preview.rows).toEqual([ - { - [key]: datasource.source === SourceName.SQL_SERVER ? "" : null, - }, - ]) - }) - }) - - describe("query verbs", () => { - describe("create", () => { - it("should be able to insert with bindings", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).insert({ name: "{{ foo }}" }).toString(), - }, - parameters: [ - { - name: "foo", - default: "bar", - }, - ], - queryVerb: "create", + await client.schema.dropTableIfExists(tableName) + await client.schema.createTable(tableName, table => { + table.increments("id").primary() + table.string("name") + table.timestamp("birthday") + table.integer("number") }) - const result = await config.api.query.execute(query._id!, { - parameters: { - foo: "baz", - }, - }) - - expect(result.data).toEqual([ - { - created: true, - }, + await client(tableName).insert([ + { name: "one" }, + { name: "two" }, + { name: "three" }, + { name: "four" }, + { name: "five" }, ]) - const rows = await client(mainTableName).where({ name: "baz" }).select() - expect(rows).toHaveLength(1) - for (const row of rows) { - expect(row).toMatchObject({ name: "baz" }) - } + jest.clearAllMocks() }) - it("should not allow handlebars as parameters", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).insert({ name: "{{ foo }}" }).toString(), - }, - parameters: [ - { - name: "foo", - default: "bar", - }, - ], - queryVerb: "create", + describe("query admin", () => { + describe("create", () => { + it("should be able to create a query", async () => { + const query = await createQuery({ + name: "New Query", + fields: { + sql: client(tableName).select("*").toString(), + }, + }) + + expect(query).toMatchObject({ + datasourceId: datasource._id!, + name: "New Query", + parameters: [], + fields: { + sql: client(tableName).select("*").toString(), + }, + schema: {}, + queryVerb: "read", + transformer: "return data", + readable: true, + createdAt: expect.any(String), + updatedAt: expect.any(String), + }) + + expect(events.query.created).toHaveBeenCalledTimes(1) + expect(events.query.updated).not.toHaveBeenCalled() + }) }) - await config.api.query.execute( - query._id!, - { - parameters: { - foo: "{{ 'test' }}", - }, - }, - { - status: 400, - body: { - message: - "Parameter 'foo' input contains a handlebars binding - this is not allowed.", - }, - } - ) - }) - - // Oracle doesn't automatically coerce strings into dates. - !isOracle && - it.each(["2021-02-05T12:01:00.000Z", "2021-02-05"])( - "should coerce %s into a date", - async datetimeStr => { - const date = new Date(datetimeStr) + describe("update", () => { + it("should be able to update a query", async () => { const query = await createQuery({ fields: { - sql: client(mainTableName) - .insert({ - name: "foo", - birthday: client.raw("{{ birthday }}"), - }) - .toString(), + sql: client(tableName).select("*").toString(), + }, + }) + + jest.clearAllMocks() + + const updatedQuery = await config.api.query.save({ + ...query, + name: "Updated Query", + fields: { + sql: client(tableName).where({ id: 1 }).toString(), + }, + }) + + expect(updatedQuery).toMatchObject({ + datasourceId: datasource._id!, + name: "Updated Query", + parameters: [], + fields: { + sql: client(tableName).where({ id: 1 }).toString(), + }, + schema: {}, + queryVerb: "read", + transformer: "return data", + readable: true, + }) + + expect(events.query.created).not.toHaveBeenCalled() + expect(events.query.updated).toHaveBeenCalledTimes(1) + }) + }) + + describe("delete", () => { + it("should be able to delete a query", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName).select("*").toString(), + }, + }) + + await config.api.query.delete(query) + await config.api.query.get(query._id!, { status: 404 }) + + const queries = await config.api.query.fetch() + expect(queries).not.toContainEqual(query) + + expect(events.query.deleted).toHaveBeenCalledTimes(1) + expect(events.query.deleted).toHaveBeenCalledWith(datasource, query) + }) + }) + + describe("read", () => { + it("should be able to list queries", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName).select("*").toString(), + }, + }) + + const queries = await config.api.query.fetch() + expect(queries).toContainEqual(query) + }) + + it("should strip sensitive fields for prod apps", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName).select("*").toString(), + }, + }) + + await config.api.application.publish(config.getAppId()) + const prodQuery = await config.api.query.getProd(query._id!) + + expect(prodQuery._id).toEqual(query._id) + expect(prodQuery.fields).toBeUndefined() + expect(prodQuery.parameters).toBeUndefined() + expect(prodQuery.schema).toBeDefined() + }) + + isPostgres && + it("should be able to handle a JSON aggregate with newlines", async () => { + const jsonStatement = `COALESCE(json_build_object('name', name),'{"name":{}}'::json)` + const query = await createQuery({ + fields: { + sql: client(tableName) + .select([ + "*", + client.raw( + `${jsonStatement} as json,\n${jsonStatement} as json2` + ), + ]) + .toString(), + }, + }) + const res = await config.api.query.execute( + query._id!, + {}, + { + status: 200, + } + ) + expect(res).toBeDefined() + }) + }) + }) + + describe("preview", () => { + it("should be able to preview a query", async () => { + const request: QueryPreview = { + datasourceId: datasource._id!, + queryVerb: "read", + fields: { + sql: client(tableName).where({ id: 1 }).toString(), + }, + parameters: [], + transformer: "return data", + name: datasource.name!, + schema: {}, + readable: true, + } + const response = await config.api.query.preview(request) + expect(response.schema).toEqual({ + birthday: { + name: "birthday", + type: "string", + }, + id: { + name: "id", + type: "number", + }, + name: { + name: "name", + type: "string", + }, + number: { + name: "number", + type: "string", + }, + }) + expect(response.rows).toEqual([ + { + birthday: null, + id: 1, + name: "one", + number: null, + }, + ]) + expect(events.query.previewed).toHaveBeenCalledTimes(1) + }) + + it("should update schema when column type changes from number to string", async () => { + const tableName = "schema_change_test" + await client.schema.dropTableIfExists(tableName) + + await client.schema.createTable(tableName, table => { + table.increments("id").primary() + table.string("name") + table.integer("data") + }) + + await client(tableName).insert({ + name: "test", + data: 123, + }) + + const firstPreview = await config.api.query.preview({ + datasourceId: datasource._id!, + name: "Test Query", + queryVerb: "read", + fields: { + sql: client(tableName).select("*").toString(), + }, + parameters: [], + transformer: "return data", + schema: {}, + readable: true, + }) + + expect(firstPreview.schema).toEqual( + expect.objectContaining({ + data: { type: "number", name: "data" }, + }) + ) + + await client(tableName).delete() + await client.schema.alterTable(tableName, table => { + table.string("data").alter() + }) + + await client(tableName).insert({ + name: "test", + data: "string value", + }) + + const secondPreview = await config.api.query.preview({ + datasourceId: datasource._id!, + name: "Test Query", + queryVerb: "read", + fields: { + sql: client(tableName).select("*").toString(), + }, + parameters: [], + transformer: "return data", + schema: firstPreview.schema, + readable: true, + }) + + expect(secondPreview.schema).toEqual( + expect.objectContaining({ + data: { type: "string", name: "data" }, + }) + ) + }) + + it("should work with static variables", async () => { + const datasource = await config.api.datasource.create({ + ...rawDatasource, + config: { + ...rawDatasource.config, + staticVariables: { + foo: "bar", + }, + }, + }) + + const request: QueryPreview = { + datasourceId: datasource._id!, + queryVerb: "read", + fields: { + sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, + }, + parameters: [], + transformer: "return data", + name: datasource.name!, + schema: {}, + readable: true, + } + + const response = await config.api.query.preview(request) + + let key = isOracle ? "FOO" : "foo" + expect(response.schema).toEqual({ + [key]: { + name: key, + type: "string", + }, + }) + + expect(response.rows).toEqual([ + { + [key]: "bar", + }, + ]) + }) + + it("should work with dynamic variables", async () => { + const datasource = await config.api.datasource.create(rawDatasource) + + const basedOnQuery = await createQuery({ + datasourceId: datasource._id!, + fields: { + sql: client(tableName).select("name").where({ id: 1 }).toString(), + }, + }) + + await config.api.datasource.update({ + ...datasource, + config: { + ...datasource.config, + dynamicVariables: [ + { + queryId: basedOnQuery._id!, + name: "foo", + value: "{{ data[0].name }}", + }, + ], + }, + }) + + const preview = await config.api.query.preview({ + datasourceId: datasource._id!, + queryVerb: "read", + fields: { + sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, + }, + parameters: [], + transformer: "return data", + name: datasource.name!, + schema: {}, + readable: true, + }) + + let key = isOracle ? "FOO" : "foo" + expect(preview.schema).toEqual({ + [key]: { + name: key, + type: "string", + }, + }) + + expect(preview.rows).toEqual([ + { + [key]: "one", + }, + ]) + }) + + it("should handle the dynamic base query being deleted", async () => { + const datasource = await config.api.datasource.create(rawDatasource) + + const basedOnQuery = await createQuery({ + datasourceId: datasource._id!, + fields: { + sql: client(tableName).select("name").where({ id: 1 }).toString(), + }, + }) + + await config.api.datasource.update({ + ...datasource, + config: { + ...datasource.config, + dynamicVariables: [ + { + queryId: basedOnQuery._id!, + name: "foo", + value: "{{ data[0].name }}", + }, + ], + }, + }) + + await config.api.query.delete(basedOnQuery) + + const preview = await config.api.query.preview({ + datasourceId: datasource._id!, + queryVerb: "read", + fields: { + sql: `SELECT '{{ foo }}' AS foo ${isOracle ? "FROM dual" : ""}`, + }, + parameters: [], + transformer: "return data", + name: datasource.name!, + schema: {}, + readable: true, + }) + + let key = isOracle ? "FOO" : "foo" + expect(preview.schema).toEqual({ + [key]: { + name: key, + type: "string", + }, + }) + + expect(preview.rows).toEqual([{ [key]: isMSSQL ? "" : null }]) + }) + }) + + describe("query verbs", () => { + describe("create", () => { + it("should be able to insert with bindings", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName).insert({ name: "{{ foo }}" }).toString(), }, parameters: [ { - name: "birthday", - default: "", + name: "foo", + default: "bar", }, ], queryVerb: "create", }) const result = await config.api.query.execute(query._id!, { - parameters: { birthday: datetimeStr }, + parameters: { + foo: "baz", + }, }) - expect(result.data).toEqual([{ created: true }]) + expect(result.data).toEqual([ + { + created: true, + }, + ]) - const rows = await client(mainTableName) - .where({ birthday: datetimeStr }) - .select() + const rows = await client(tableName).where({ name: "baz" }).select() expect(rows).toHaveLength(1) - for (const row of rows) { - expect(new Date(row.birthday)).toEqual(date) + expect(row).toMatchObject({ name: "baz" }) } - } - ) + }) + + it("should not allow handlebars as parameters", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName).insert({ name: "{{ foo }}" }).toString(), + }, + parameters: [ + { + name: "foo", + default: "bar", + }, + ], + queryVerb: "create", + }) + + await config.api.query.execute( + query._id!, + { + parameters: { + foo: "{{ 'test' }}", + }, + }, + { + status: 400, + body: { + message: + "Parameter 'foo' input contains a handlebars binding - this is not allowed.", + }, + } + ) + }) + + // Oracle doesn't automatically coerce strings into dates. + !isOracle && + it.each(["2021-02-05T12:01:00.000Z", "2021-02-05"])( + "should coerce %s into a date", + async datetimeStr => { + const date = new Date(datetimeStr) + const query = await createQuery({ + fields: { + sql: client(tableName) + .insert({ + name: "foo", + birthday: client.raw("{{ birthday }}"), + }) + .toString(), + }, + parameters: [ + { + name: "birthday", + default: "", + }, + ], + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { birthday: datetimeStr }, + }) + + expect(result.data).toEqual([{ created: true }]) + + const rows = await client(tableName) + .where({ birthday: datetimeStr }) + .select() + expect(rows).toHaveLength(1) + + for (const row of rows) { + expect(new Date(row.birthday)).toEqual(date) + } + } + ) + + it.each(["2021,02,05", "202205-1500"])( + "should not coerce %s as a date", + async notDateStr => { + const query = await createQuery({ + fields: { + sql: client(tableName) + .insert({ name: client.raw("{{ name }}") }) + .toString(), + }, + parameters: [ + { + name: "name", + default: "", + }, + ], + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + name: notDateStr, + }, + }) + + expect(result.data).toEqual([{ created: true }]) + + const rows = await client(tableName) + .where({ name: notDateStr }) + .select() + expect(rows).toHaveLength(1) + } + ) + }) + + describe("read", () => { + it("should execute a query", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName).select("*").orderBy("id").toString(), + }, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + id: 1, + name: "one", + birthday: null, + number: null, + }, + { + id: 2, + name: "two", + birthday: null, + number: null, + }, + { + id: 3, + name: "three", + birthday: null, + number: null, + }, + { + id: 4, + name: "four", + birthday: null, + number: null, + }, + { + id: 5, + name: "five", + birthday: null, + number: null, + }, + ]) + }) + + it("should be able to transform a query", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName).where({ id: 1 }).select("*").toString(), + }, + transformer: ` + data[0].id = data[0].id + 1; + return data; + `, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + id: 2, + name: "one", + birthday: null, + number: null, + }, + ]) + }) + + it("should coerce numeric bindings", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName) + .where({ id: client.raw("{{ id }}") }) + .select("*") + .toString(), + }, + parameters: [ + { + name: "id", + default: "", + }, + ], + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { + id: "1", + }, + }) + + expect(result.data).toEqual([ + { + id: 1, + name: "one", + birthday: null, + number: null, + }, + ]) + }) + }) + + describe("update", () => { + it("should be able to update rows", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName) + .update({ name: client.raw("{{ name }}") }) + .where({ id: client.raw("{{ id }}") }) + .toString(), + }, + parameters: [ + { + name: "id", + default: "", + }, + { + name: "name", + default: "updated", + }, + ], + queryVerb: "update", + }) + + await config.api.query.execute(query._id!, { + parameters: { + id: "1", + name: "foo", + }, + }) + + const rows = await client(tableName).where({ id: 1 }).select() + expect(rows).toEqual([ + { id: 1, name: "foo", birthday: null, number: null }, + ]) + }) + + it("should be able to execute an update that updates no rows", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName) + .update({ name: "updated" }) + .where({ id: 100 }) + .toString(), + }, + queryVerb: "update", + }) + + await config.api.query.execute(query._id!) + + const rows = await client(tableName).select() + for (const row of rows) { + expect(row.name).not.toEqual("updated") + } + }) + + it("should be able to execute a delete that deletes no rows", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName).where({ id: 100 }).delete().toString(), + }, + queryVerb: "delete", + }) + + await config.api.query.execute(query._id!) + + const rows = await client(tableName).select() + expect(rows).toHaveLength(5) + }) + }) + + describe("delete", () => { + it("should be able to delete rows", async () => { + const query = await createQuery({ + fields: { + sql: client(tableName) + .where({ id: client.raw("{{ id }}") }) + .delete() + .toString(), + }, + parameters: [ + { + name: "id", + default: "", + }, + ], + queryVerb: "delete", + }) + + await config.api.query.execute(query._id!, { + parameters: { + id: "1", + }, + }) + + const rows = await client(tableName).where({ id: 1 }).select() + expect(rows).toHaveLength(0) + }) + }) + }) + + describe("query through datasource", () => { + it("should be able to query the datasource", async () => { + const datasource = await config.api.datasource.create(rawDatasource) + + const entityId = tableName + await config.api.datasource.update({ + ...datasource, + entities: { + [entityId]: { + name: entityId, + schema: {}, + type: "table", + primary: ["id"], + sourceId: datasource._id!, + sourceType: TableSourceType.EXTERNAL, + }, + }, + }) + + const res = await config.api.datasource.query({ + endpoint: { + datasourceId: datasource._id!, + operation: Operation.READ, + entityId, + }, + resource: { + fields: ["id", "name"], + }, + filters: { + string: { + name: "two", + }, + }, + }) + expect(res).toHaveLength(1) + expect(res[0]).toEqual({ + id: 2, + name: "two", + // the use of table.* introduces the possibility of nulls being returned + birthday: null, + number: null, + }) + }) + + // this parameter really only impacts SQL queries + describe("confirm nullDefaultSupport", () => { + let queryParams: Partial + beforeAll(async () => { + queryParams = { + fields: { + sql: client(tableName) + .insert({ + name: client.raw("{{ bindingName }}"), + number: client.raw("{{ bindingNumber }}"), + }) + .toString(), + }, + parameters: [ + { + name: "bindingName", + default: "", + }, + { + name: "bindingNumber", + default: "", + }, + ], + queryVerb: "create", + } + }) + + it("should error for old queries", async () => { + const query = await createQuery(queryParams) + await config.api.query.save({ ...query, nullDefaultSupport: false }) + let error: string | undefined + try { + await config.api.query.execute(query._id!, { + parameters: { + bindingName: "testing", + }, + }) + } catch (err: any) { + error = err.message + } + if (isMSSQL || isOracle) { + expect(error).toBeUndefined() + } else { + expect(error).toBeDefined() + expect(error).toContain("integer") + } + }) + + it("should not error for new queries", async () => { + const query = await createQuery(queryParams) + const results = await config.api.query.execute(query._id!, { + parameters: { + bindingName: "testing", + }, + }) + expect(results).toEqual({ data: [{ created: true }] }) + }) + }) + }) + + describe("edge cases", () => { + it("should find rows with a binding containing a slash", async () => { + const slashValue = "1/10" + await client(tableName).insert([{ name: slashValue }]) - it.each(["2021,02,05", "202205-1500"])( - "should not coerce %s as a date", - async notDateStr => { const query = await createQuery({ fields: { - sql: client(mainTableName) - .insert({ name: client.raw("{{ name }}") }) + sql: client(tableName) + .select("*") + .where("name", "=", client.raw("{{ bindingName }}")) .toString(), }, parameters: [ { - name: "name", + name: "bindingName", default: "", }, ], - queryVerb: "create", + queryVerb: "read", }) - - const result = await config.api.query.execute(query._id!, { + const results = await config.api.query.execute(query._id!, { parameters: { - name: notDateStr, + bindingName: slashValue, }, }) - - expect(result.data).toEqual([{ created: true }]) - - const rows = await client(mainTableName) - .where({ name: notDateStr }) - .select() - expect(rows).toHaveLength(1) - } - ) - }) - - describe("read", () => { - it("should execute a query", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).select("*").orderBy("id").toString(), - }, + expect(results).toBeDefined() + expect(results.data.length).toEqual(1) }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([ - { - id: 1, - name: "one", - birthday: null, - number: null, - }, - { - id: 2, - name: "two", - birthday: null, - number: null, - }, - { - id: 3, - name: "three", - birthday: null, - number: null, - }, - { - id: 4, - name: "four", - birthday: null, - number: null, - }, - { - id: 5, - name: "five", - birthday: null, - number: null, - }, - ]) }) - - it("should be able to transform a query", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).where({ id: 1 }).select("*").toString(), - }, - transformer: ` - data[0].id = data[0].id + 1; - return data; - `, - }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([ - { - id: 2, - name: "one", - birthday: null, - number: null, - }, - ]) - }) - - it("should coerce numeric bindings", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .where({ id: client.raw("{{ id }}") }) - .select("*") - .toString(), - }, - parameters: [ - { - name: "id", - default: "", - }, - ], - }) - - const result = await config.api.query.execute(query._id!, { - parameters: { - id: "1", - }, - }) - - expect(result.data).toEqual([ - { - id: 1, - name: "one", - birthday: null, - number: null, - }, - ]) - }) - }) - - describe("update", () => { - it("should be able to update rows", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .update({ name: client.raw("{{ name }}") }) - .where({ id: client.raw("{{ id }}") }) - .toString(), - }, - parameters: [ - { - name: "id", - default: "", - }, - { - name: "name", - default: "updated", - }, - ], - queryVerb: "update", - }) - - await config.api.query.execute(query._id!, { - parameters: { - id: "1", - name: "foo", - }, - }) - - const rows = await client(mainTableName).where({ id: 1 }).select() - expect(rows).toEqual([ - { id: 1, name: "foo", birthday: null, number: null }, - ]) - }) - - it("should be able to execute an update that updates no rows", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .update({ name: "updated" }) - .where({ id: 100 }) - .toString(), - }, - queryVerb: "update", - }) - - await config.api.query.execute(query._id!) - - const rows = await client(mainTableName).select() - for (const row of rows) { - expect(row.name).not.toEqual("updated") - } - }) - - it("should be able to execute a delete that deletes no rows", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName).where({ id: 100 }).delete().toString(), - }, - queryVerb: "delete", - }) - - await config.api.query.execute(query._id!) - - const rows = await client(mainTableName).select() - expect(rows).toHaveLength(5) - }) - }) - - describe("delete", () => { - it("should be able to delete rows", async () => { - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .where({ id: client.raw("{{ id }}") }) - .delete() - .toString(), - }, - parameters: [ - { - name: "id", - default: "", - }, - ], - queryVerb: "delete", - }) - - await config.api.query.execute(query._id!, { - parameters: { - id: "1", - }, - }) - - const rows = await client(mainTableName).where({ id: 1 }).select() - expect(rows).toHaveLength(0) - }) - }) - }) - - describe("query through datasource", () => { - it("should be able to query the datasource", async () => { - const entityId = mainTableName - await config.api.datasource.update({ - ...datasource, - entities: { - [entityId]: { - name: entityId, - schema: {}, - type: "table", - primary: ["id"], - sourceId: datasource._id!, - sourceType: TableSourceType.EXTERNAL, - }, - }, - }) - const res = await config.api.datasource.query({ - endpoint: { - datasourceId: datasource._id!, - operation: Operation.READ, - entityId, - }, - resource: { - fields: ["id", "name"], - }, - filters: { - string: { - name: "two", - }, - }, - }) - expect(res).toHaveLength(1) - expect(res[0]).toEqual({ - id: 2, - name: "two", - // the use of table.* introduces the possibility of nulls being returned - birthday: null, - number: null, - }) - }) - - // this parameter really only impacts SQL queries - describe("confirm nullDefaultSupport", () => { - let queryParams: Partial - beforeAll(async () => { - queryParams = { - fields: { - sql: client(mainTableName) - .insert({ - name: client.raw("{{ bindingName }}"), - number: client.raw("{{ bindingNumber }}"), - }) - .toString(), - }, - parameters: [ - { - name: "bindingName", - default: "", - }, - { - name: "bindingNumber", - default: "", - }, - ], - queryVerb: "create", - } - }) - - it("should error for old queries", async () => { - const query = await createQuery(queryParams) - await config.api.query.save({ ...query, nullDefaultSupport: false }) - let error: string | undefined - try { - await config.api.query.execute(query._id!, { - parameters: { - bindingName: "testing", - }, - }) - } catch (err: any) { - error = err.message - } - if (isMsSQL || isOracle) { - expect(error).toBeUndefined() - } else { - expect(error).toBeDefined() - expect(error).toContain("integer") - } - }) - - it("should not error for new queries", async () => { - const query = await createQuery(queryParams) - const results = await config.api.query.execute(query._id!, { - parameters: { - bindingName: "testing", - }, - }) - expect(results).toEqual({ data: [{ created: true }] }) - }) - }) - }) - - describe("edge cases", () => { - it("should find rows with a binding containing a slash", async () => { - const slashValue = "1/10" - await client(mainTableName).insert([{ name: slashValue }]) - - const query = await createQuery({ - fields: { - sql: client(mainTableName) - .select("*") - .where("name", "=", client.raw("{{ bindingName }}")) - .toString(), - }, - parameters: [ - { - name: "bindingName", - default: "", - }, - ], - queryVerb: "read", - }) - const results = await config.api.query.execute(query._id!, { - parameters: { - bindingName: slashValue, - }, - }) - expect(results).toBeDefined() - expect(results.data.length).toEqual(1) - }) - }) -}) + } + ) +} diff --git a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts index 4822729478..a37957fe7e 100644 --- a/packages/server/src/api/routes/tests/queries/mongodb.spec.ts +++ b/packages/server/src/api/routes/tests/queries/mongodb.spec.ts @@ -1,8 +1,7 @@ import { Datasource, Query } from "@budibase/types" -import * as setup from "../utilities" import { DatabaseName, - getDatasource, + datasourceDescribe, } from "../../../../integrations/tests/utils" import { MongoClient, type Collection, BSON, Db } from "mongodb" import { generator } from "@budibase/backend-core/tests" @@ -10,711 +9,713 @@ import { generator } from "@budibase/backend-core/tests" const expectValidId = expect.stringMatching(/^\w{24}$/) const expectValidBsonObjectId = expect.any(BSON.ObjectId) -describe("/queries", () => { - let collection: string - let config = setup.getConfig() - let datasource: Datasource +const descriptions = datasourceDescribe({ only: [DatabaseName.MONGODB] }) - async function createQuery(query: Partial): Promise { - const defaultQuery: Query = { - datasourceId: datasource._id!, - name: "New Query", - parameters: [], - fields: {}, - schema: {}, - queryVerb: "read", - transformer: "return data", - readable: true, - } - const combinedQuery = { ...defaultQuery, ...query } - if ( - combinedQuery.fields && - combinedQuery.fields.extra && - !combinedQuery.fields.extra.collection - ) { - combinedQuery.fields.extra.collection = collection - } - return await config.api.query.save(combinedQuery) - } +if (descriptions.length) { + describe.each(descriptions)( + "/queries ($dbName)", + ({ config, dsProvider }) => { + let collection: string + let datasource: Datasource - async function withClient( - callback: (client: MongoClient) => Promise - ): Promise { - const client = new MongoClient(datasource.config!.connectionString) - await client.connect() - try { - return await callback(client) - } finally { - await client.close() - } - } - - async function withDb(callback: (db: Db) => Promise): Promise { - return await withClient(async client => { - return await callback(client.db(datasource.config!.db)) - }) - } - - async function withCollection( - callback: (collection: Collection) => Promise - ): Promise { - return await withDb(async db => { - return await callback(db.collection(collection)) - }) - } - - afterAll(async () => { - setup.afterAll() - }) - - beforeAll(async () => { - await config.init() - datasource = await config.api.datasource.create( - await getDatasource(DatabaseName.MONGODB) - ) - }) - - beforeEach(async () => { - collection = generator.guid() - await withCollection(async collection => { - await collection.insertMany([ - { name: "one" }, - { name: "two" }, - { name: "three" }, - { name: "four" }, - { name: "five" }, - ]) - }) - }) - - afterEach(async () => { - await withCollection(collection => collection.drop()) - }) - - describe("preview", () => { - it("should generate a nested schema with an empty array", async () => { - const name = generator.guid() - await withCollection( - async collection => await collection.insertOne({ name, nested: [] }) - ) - - const preview = await config.api.query.preview({ - name: "New Query", - datasourceId: datasource._id!, - fields: { - json: { - name: { $eq: name }, - }, - extra: { - collection, - actionType: "findOne", - }, - }, - schema: {}, - queryVerb: "read", - parameters: [], - transformer: "return data", - readable: true, - }) - - expect(preview).toEqual({ - nestedSchemaFields: {}, - rows: [{ _id: expect.any(String), name, nested: [] }], - schema: { - _id: { - type: "string", - name: "_id", - }, - name: { - type: "string", - name: "name", - }, - nested: { - type: "array", - name: "nested", - }, - }, - }) - }) - - it("should update schema when structure changes from object to array", async () => { - const name = generator.guid() - - await withCollection(async collection => { - await collection.insertOne({ name, field: { subfield: "value" } }) - }) - - const firstPreview = await config.api.query.preview({ - name: "Test Query", - datasourceId: datasource._id!, - fields: { - json: { name: { $eq: name } }, - extra: { - collection, - actionType: "findOne", - }, - }, - schema: {}, - queryVerb: "read", - parameters: [], - transformer: "return data", - readable: true, - }) - - expect(firstPreview.schema).toEqual( - expect.objectContaining({ - field: { type: "json", name: "field" }, - }) - ) - - await withCollection(async collection => { - await collection.updateOne( - { name }, - { $set: { field: ["value1", "value2"] } } - ) - }) - - const secondPreview = await config.api.query.preview({ - name: "Test Query", - datasourceId: datasource._id!, - fields: { - json: { name: { $eq: name } }, - extra: { - collection, - actionType: "findOne", - }, - }, - schema: firstPreview.schema, - queryVerb: "read", - parameters: [], - transformer: "return data", - readable: true, - }) - - expect(secondPreview.schema).toEqual( - expect.objectContaining({ - field: { type: "array", name: "field" }, - }) - ) - }) - - it("should generate a nested schema based on all of the nested items", async () => { - const name = generator.guid() - const item = { - name, - contacts: [ - { - address: "123 Lane", - }, - { - address: "456 Drive", - }, - { - postcode: "BT1 12N", - lat: 54.59, - long: -5.92, - }, - { - city: "Belfast", - }, - { - address: "789 Avenue", - phoneNumber: "0800-999-5555", - }, - { - name: "Name", - isActive: false, - }, - ], + async function createQuery(query: Partial): Promise { + const defaultQuery: Query = { + datasourceId: datasource._id!, + name: "New Query", + parameters: [], + fields: {}, + schema: {}, + queryVerb: "read", + transformer: "return data", + readable: true, + } + const combinedQuery = { ...defaultQuery, ...query } + if ( + combinedQuery.fields && + combinedQuery.fields.extra && + !combinedQuery.fields.extra.collection + ) { + combinedQuery.fields.extra.collection = collection + } + return await config.api.query.save(combinedQuery) } - await withCollection(collection => collection.insertOne(item)) + async function withClient( + callback: (client: MongoClient) => Promise + ): Promise { + const client = new MongoClient(datasource.config!.connectionString) + await client.connect() + try { + return await callback(client) + } finally { + await client.close() + } + } - const preview = await config.api.query.preview({ - name: "New Query", - datasourceId: datasource._id!, - fields: { - json: { - name: { $eq: name }, - }, - extra: { - collection, - actionType: "findOne", - }, - }, - schema: {}, - queryVerb: "read", - parameters: [], - transformer: "return data", - readable: true, + async function withDb(callback: (db: Db) => Promise): Promise { + return await withClient(async client => { + return await callback(client.db(datasource.config!.db)) + }) + } + + async function withCollection( + callback: (collection: Collection) => Promise + ): Promise { + return await withDb(async db => { + return await callback(db.collection(collection)) + }) + } + + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource! }) - expect(preview).toEqual({ - nestedSchemaFields: { - contacts: { - address: { - type: "string", - name: "address", - }, - postcode: { - type: "string", - name: "postcode", - }, - lat: { - type: "number", - name: "lat", - }, - long: { - type: "number", - name: "long", - }, - city: { - type: "string", - name: "city", - }, - phoneNumber: { - type: "string", - name: "phoneNumber", - }, - name: { - type: "string", - name: "name", - }, - isActive: { - type: "boolean", - name: "isActive", - }, - }, - }, - rows: [{ ...item, _id: expect.any(String) }], - schema: { - _id: { type: "string", name: "_id" }, - name: { type: "string", name: "name" }, - contacts: { type: "json", name: "contacts", subtype: "array" }, - }, - }) - }) - }) - - describe("execute", () => { - it("a count query", async () => { - const query = await createQuery({ - fields: { - json: {}, - extra: { - actionType: "count", - }, - }, - }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([{ value: 5 }]) - }) - - it("should be able to updateOne by ObjectId", async () => { - const insertResult = await withCollection(c => - c.insertOne({ name: "one" }) - ) - const query = await createQuery({ - fields: { - json: { - filter: { _id: { $eq: `ObjectId("${insertResult.insertedId}")` } }, - update: { $set: { name: "newName" } }, - }, - extra: { - actionType: "updateOne", - }, - }, - queryVerb: "update", - }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([ - { - acknowledged: true, - matchedCount: 1, - modifiedCount: 1, - upsertedCount: 0, - upsertedId: null, - }, - ]) - - await withCollection(async collection => { - const doc = await collection.findOne({ name: { $eq: "newName" } }) - expect(doc).toEqual({ - _id: insertResult.insertedId, - name: "newName", + beforeEach(async () => { + collection = generator.guid() + await withCollection(async collection => { + await collection.insertMany([ + { name: "one" }, + { name: "two" }, + { name: "three" }, + { name: "four" }, + { name: "five" }, + ]) }) }) - }) - it("a count query with a transformer", async () => { - const query = await createQuery({ - fields: { - json: {}, - extra: { - actionType: "count", - }, - }, - transformer: "return data + 1", + afterEach(async () => { + await withCollection(collection => collection.drop()) }) - const result = await config.api.query.execute(query._id!) + describe("preview", () => { + it("should generate a nested schema with an empty array", async () => { + const name = generator.guid() + await withCollection( + async collection => await collection.insertOne({ name, nested: [] }) + ) - expect(result.data).toEqual([{ value: 6 }]) - }) - - it("a find query", async () => { - const query = await createQuery({ - fields: { - json: {}, - extra: { - actionType: "find", - }, - }, - }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([ - { _id: expectValidId, name: "one" }, - { _id: expectValidId, name: "two" }, - { _id: expectValidId, name: "three" }, - { _id: expectValidId, name: "four" }, - { _id: expectValidId, name: "five" }, - ]) - }) - - it("a findOne query", async () => { - const query = await createQuery({ - fields: { - json: {}, - extra: { - actionType: "findOne", - }, - }, - }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([{ _id: expectValidId, name: "one" }]) - }) - - it("a findOneAndUpdate query", async () => { - const query = await createQuery({ - fields: { - json: { - filter: { name: { $eq: "one" } }, - update: { $set: { name: "newName" } }, - }, - extra: { - actionType: "findOneAndUpdate", - }, - }, - }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([ - { - lastErrorObject: { n: 1, updatedExisting: true }, - ok: 1, - value: { _id: expectValidId, name: "one" }, - }, - ]) - - await withCollection(async collection => { - expect(await collection.countDocuments()).toBe(5) - - const doc = await collection.findOne({ name: { $eq: "newName" } }) - expect(doc).toEqual({ - _id: expectValidBsonObjectId, - name: "newName", - }) - }) - }) - - it("a distinct query", async () => { - const query = await createQuery({ - fields: { - json: "name", - extra: { - actionType: "distinct", - }, - }, - }) - - const result = await config.api.query.execute(query._id!) - const values = result.data.map(o => o.value).sort() - expect(values).toEqual(["five", "four", "one", "three", "two"]) - }) - - it("a create query with parameters", async () => { - const query = await createQuery({ - fields: { - json: { foo: "{{ foo }}" }, - extra: { - actionType: "insertOne", - }, - }, - queryVerb: "create", - parameters: [ - { - name: "foo", - default: "default", - }, - ], - }) - - const result = await config.api.query.execute(query._id!, { - parameters: { foo: "bar" }, - }) - - expect(result.data).toEqual([ - { - acknowledged: true, - insertedId: expectValidId, - }, - ]) - - await withCollection(async collection => { - const doc = await collection.findOne({ foo: { $eq: "bar" } }) - expect(doc).toEqual({ - _id: expectValidBsonObjectId, - foo: "bar", - }) - }) - }) - - it("a delete query with parameters", async () => { - const query = await createQuery({ - fields: { - json: { name: { $eq: "{{ name }}" } }, - extra: { - actionType: "deleteOne", - }, - }, - queryVerb: "delete", - parameters: [ - { - name: "name", - default: "", - }, - ], - }) - - const result = await config.api.query.execute(query._id!, { - parameters: { name: "one" }, - }) - - expect(result.data).toEqual([ - { - acknowledged: true, - deletedCount: 1, - }, - ]) - - await withCollection(async collection => { - const doc = await collection.findOne({ name: { $eq: "one" } }) - expect(doc).toBeNull() - }) - }) - - it("an update query with parameters", async () => { - const query = await createQuery({ - fields: { - json: { - filter: { name: { $eq: "{{ name }}" } }, - update: { $set: { name: "{{ newName }}" } }, - }, - extra: { - actionType: "updateOne", - }, - }, - queryVerb: "update", - parameters: [ - { - name: "name", - default: "", - }, - { - name: "newName", - default: "", - }, - ], - }) - - const result = await config.api.query.execute(query._id!, { - parameters: { name: "one", newName: "newOne" }, - }) - - expect(result.data).toEqual([ - { - acknowledged: true, - matchedCount: 1, - modifiedCount: 1, - upsertedCount: 0, - upsertedId: null, - }, - ]) - - await withCollection(async collection => { - const doc = await collection.findOne({ name: { $eq: "newOne" } }) - expect(doc).toEqual({ - _id: expectValidBsonObjectId, - name: "newOne", - }) - - const oldDoc = await collection.findOne({ name: { $eq: "one" } }) - expect(oldDoc).toBeNull() - }) - }) - - it("should be able to delete all records", async () => { - const query = await createQuery({ - fields: { - json: {}, - extra: { - actionType: "deleteMany", - }, - }, - queryVerb: "delete", - }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([ - { - acknowledged: true, - deletedCount: 5, - }, - ]) - - await withCollection(async collection => { - const docs = await collection.find().toArray() - expect(docs).toHaveLength(0) - }) - }) - - it("should be able to update all documents", async () => { - const query = await createQuery({ - fields: { - json: { - filter: {}, - update: { $set: { name: "newName" } }, - }, - extra: { - actionType: "updateMany", - }, - }, - queryVerb: "update", - }) - - const result = await config.api.query.execute(query._id!) - - expect(result.data).toEqual([ - { - acknowledged: true, - matchedCount: 5, - modifiedCount: 5, - upsertedCount: 0, - upsertedId: null, - }, - ]) - - await withCollection(async collection => { - const docs = await collection.find().toArray() - expect(docs).toHaveLength(5) - for (const doc of docs) { - expect(doc).toEqual({ - _id: expectValidBsonObjectId, - name: "newName", + const preview = await config.api.query.preview({ + name: "New Query", + datasourceId: datasource._id!, + fields: { + json: { + name: { $eq: name }, + }, + extra: { + collection, + actionType: "findOne", + }, + }, + schema: {}, + queryVerb: "read", + parameters: [], + transformer: "return data", + readable: true, }) + + expect(preview).toEqual({ + nestedSchemaFields: {}, + rows: [{ _id: expect.any(String), name, nested: [] }], + schema: { + _id: { + type: "string", + name: "_id", + }, + name: { + type: "string", + name: "name", + }, + nested: { + type: "array", + name: "nested", + }, + }, + }) + }) + + it("should update schema when structure changes from object to array", async () => { + const name = generator.guid() + + await withCollection(async collection => { + await collection.insertOne({ name, field: { subfield: "value" } }) + }) + + const firstPreview = await config.api.query.preview({ + name: "Test Query", + datasourceId: datasource._id!, + fields: { + json: { name: { $eq: name } }, + extra: { + collection, + actionType: "findOne", + }, + }, + schema: {}, + queryVerb: "read", + parameters: [], + transformer: "return data", + readable: true, + }) + + expect(firstPreview.schema).toEqual( + expect.objectContaining({ + field: { type: "json", name: "field" }, + }) + ) + + await withCollection(async collection => { + await collection.updateOne( + { name }, + { $set: { field: ["value1", "value2"] } } + ) + }) + + const secondPreview = await config.api.query.preview({ + name: "Test Query", + datasourceId: datasource._id!, + fields: { + json: { name: { $eq: name } }, + extra: { + collection, + actionType: "findOne", + }, + }, + schema: firstPreview.schema, + queryVerb: "read", + parameters: [], + transformer: "return data", + readable: true, + }) + + expect(secondPreview.schema).toEqual( + expect.objectContaining({ + field: { type: "array", name: "field" }, + }) + ) + }) + + it("should generate a nested schema based on all of the nested items", async () => { + const name = generator.guid() + const item = { + name, + contacts: [ + { + address: "123 Lane", + }, + { + address: "456 Drive", + }, + { + postcode: "BT1 12N", + lat: 54.59, + long: -5.92, + }, + { + city: "Belfast", + }, + { + address: "789 Avenue", + phoneNumber: "0800-999-5555", + }, + { + name: "Name", + isActive: false, + }, + ], + } + + await withCollection(collection => collection.insertOne(item)) + + const preview = await config.api.query.preview({ + name: "New Query", + datasourceId: datasource._id!, + fields: { + json: { + name: { $eq: name }, + }, + extra: { + collection, + actionType: "findOne", + }, + }, + schema: {}, + queryVerb: "read", + parameters: [], + transformer: "return data", + readable: true, + }) + + expect(preview).toEqual({ + nestedSchemaFields: { + contacts: { + address: { + type: "string", + name: "address", + }, + postcode: { + type: "string", + name: "postcode", + }, + lat: { + type: "number", + name: "lat", + }, + long: { + type: "number", + name: "long", + }, + city: { + type: "string", + name: "city", + }, + phoneNumber: { + type: "string", + name: "phoneNumber", + }, + name: { + type: "string", + name: "name", + }, + isActive: { + type: "boolean", + name: "isActive", + }, + }, + }, + rows: [{ ...item, _id: expect.any(String) }], + schema: { + _id: { type: "string", name: "_id" }, + name: { type: "string", name: "name" }, + contacts: { type: "json", name: "contacts", subtype: "array" }, + }, + }) + }) + }) + + describe("execute", () => { + it("a count query", async () => { + const query = await createQuery({ + fields: { + json: {}, + extra: { + actionType: "count", + }, + }, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([{ value: 5 }]) + }) + + it("should be able to updateOne by ObjectId", async () => { + const insertResult = await withCollection(c => + c.insertOne({ name: "one" }) + ) + const query = await createQuery({ + fields: { + json: { + filter: { + _id: { $eq: `ObjectId("${insertResult.insertedId}")` }, + }, + update: { $set: { name: "newName" } }, + }, + extra: { + actionType: "updateOne", + }, + }, + queryVerb: "update", + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + acknowledged: true, + matchedCount: 1, + modifiedCount: 1, + upsertedCount: 0, + upsertedId: null, + }, + ]) + + await withCollection(async collection => { + const doc = await collection.findOne({ name: { $eq: "newName" } }) + expect(doc).toEqual({ + _id: insertResult.insertedId, + name: "newName", + }) + }) + }) + + it("a count query with a transformer", async () => { + const query = await createQuery({ + fields: { + json: {}, + extra: { + actionType: "count", + }, + }, + transformer: "return data + 1", + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([{ value: 6 }]) + }) + + it("a find query", async () => { + const query = await createQuery({ + fields: { + json: {}, + extra: { + actionType: "find", + }, + }, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { _id: expectValidId, name: "one" }, + { _id: expectValidId, name: "two" }, + { _id: expectValidId, name: "three" }, + { _id: expectValidId, name: "four" }, + { _id: expectValidId, name: "five" }, + ]) + }) + + it("a findOne query", async () => { + const query = await createQuery({ + fields: { + json: {}, + extra: { + actionType: "findOne", + }, + }, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([{ _id: expectValidId, name: "one" }]) + }) + + it("a findOneAndUpdate query", async () => { + const query = await createQuery({ + fields: { + json: { + filter: { name: { $eq: "one" } }, + update: { $set: { name: "newName" } }, + }, + extra: { + actionType: "findOneAndUpdate", + }, + }, + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + lastErrorObject: { n: 1, updatedExisting: true }, + ok: 1, + value: { _id: expectValidId, name: "one" }, + }, + ]) + + await withCollection(async collection => { + expect(await collection.countDocuments()).toBe(5) + + const doc = await collection.findOne({ name: { $eq: "newName" } }) + expect(doc).toEqual({ + _id: expectValidBsonObjectId, + name: "newName", + }) + }) + }) + + it("a distinct query", async () => { + const query = await createQuery({ + fields: { + json: "name", + extra: { + actionType: "distinct", + }, + }, + }) + + const result = await config.api.query.execute(query._id!) + const values = result.data.map(o => o.value).sort() + expect(values).toEqual(["five", "four", "one", "three", "two"]) + }) + + it("a create query with parameters", async () => { + const query = await createQuery({ + fields: { + json: { foo: "{{ foo }}" }, + extra: { + actionType: "insertOne", + }, + }, + queryVerb: "create", + parameters: [ + { + name: "foo", + default: "default", + }, + ], + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { foo: "bar" }, + }) + + expect(result.data).toEqual([ + { + acknowledged: true, + insertedId: expectValidId, + }, + ]) + + await withCollection(async collection => { + const doc = await collection.findOne({ foo: { $eq: "bar" } }) + expect(doc).toEqual({ + _id: expectValidBsonObjectId, + foo: "bar", + }) + }) + }) + + it("a delete query with parameters", async () => { + const query = await createQuery({ + fields: { + json: { name: { $eq: "{{ name }}" } }, + extra: { + actionType: "deleteOne", + }, + }, + queryVerb: "delete", + parameters: [ + { + name: "name", + default: "", + }, + ], + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { name: "one" }, + }) + + expect(result.data).toEqual([ + { + acknowledged: true, + deletedCount: 1, + }, + ]) + + await withCollection(async collection => { + const doc = await collection.findOne({ name: { $eq: "one" } }) + expect(doc).toBeNull() + }) + }) + + it("an update query with parameters", async () => { + const query = await createQuery({ + fields: { + json: { + filter: { name: { $eq: "{{ name }}" } }, + update: { $set: { name: "{{ newName }}" } }, + }, + extra: { + actionType: "updateOne", + }, + }, + queryVerb: "update", + parameters: [ + { + name: "name", + default: "", + }, + { + name: "newName", + default: "", + }, + ], + }) + + const result = await config.api.query.execute(query._id!, { + parameters: { name: "one", newName: "newOne" }, + }) + + expect(result.data).toEqual([ + { + acknowledged: true, + matchedCount: 1, + modifiedCount: 1, + upsertedCount: 0, + upsertedId: null, + }, + ]) + + await withCollection(async collection => { + const doc = await collection.findOne({ name: { $eq: "newOne" } }) + expect(doc).toEqual({ + _id: expectValidBsonObjectId, + name: "newOne", + }) + + const oldDoc = await collection.findOne({ name: { $eq: "one" } }) + expect(oldDoc).toBeNull() + }) + }) + + it("should be able to delete all records", async () => { + const query = await createQuery({ + fields: { + json: {}, + extra: { + actionType: "deleteMany", + }, + }, + queryVerb: "delete", + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + acknowledged: true, + deletedCount: 5, + }, + ]) + + await withCollection(async collection => { + const docs = await collection.find().toArray() + expect(docs).toHaveLength(0) + }) + }) + + it("should be able to update all documents", async () => { + const query = await createQuery({ + fields: { + json: { + filter: {}, + update: { $set: { name: "newName" } }, + }, + extra: { + actionType: "updateMany", + }, + }, + queryVerb: "update", + }) + + const result = await config.api.query.execute(query._id!) + + expect(result.data).toEqual([ + { + acknowledged: true, + matchedCount: 5, + modifiedCount: 5, + upsertedCount: 0, + upsertedId: null, + }, + ]) + + await withCollection(async collection => { + const docs = await collection.find().toArray() + expect(docs).toHaveLength(5) + for (const doc of docs) { + expect(doc).toEqual({ + _id: expectValidBsonObjectId, + name: "newName", + }) + } + }) + }) + }) + + it("should throw an error if the incorrect actionType is specified", async () => { + const verbs = ["read", "create", "update", "delete"] + for (const verb of verbs) { + const query = await createQuery({ + fields: { json: {}, extra: { actionType: "invalid" } }, + queryVerb: verb, + }) + await config.api.query.execute(query._id!, undefined, { status: 400 }) } }) - }) - }) - it("should throw an error if the incorrect actionType is specified", async () => { - const verbs = ["read", "create", "update", "delete"] - for (const verb of verbs) { - const query = await createQuery({ - fields: { json: {}, extra: { actionType: "invalid" } }, - queryVerb: verb, - }) - await config.api.query.execute(query._id!, undefined, { status: 400 }) - } - }) - - it("should ignore extra brackets in query", async () => { - const query = await createQuery({ - fields: { - json: { foo: "te}st" }, - extra: { - actionType: "insertOne", - }, - }, - queryVerb: "create", - }) - - const result = await config.api.query.execute(query._id!) - expect(result.data).toEqual([ - { - acknowledged: true, - insertedId: expectValidId, - }, - ]) - - await withCollection(async collection => { - const doc = await collection.findOne({ foo: { $eq: "te}st" } }) - expect(doc).toEqual({ - _id: expectValidBsonObjectId, - foo: "te}st", - }) - }) - }) - - it("should be able to save deeply nested data", async () => { - const data = { - foo: "bar", - data: [ - { cid: 1 }, - { cid: 2 }, - { - nested: { - name: "test", - ary: [1, 2, 3], - aryOfObjects: [{ a: 1 }, { b: 2 }], + it("should ignore extra brackets in query", async () => { + const query = await createQuery({ + fields: { + json: { foo: "te}st" }, + extra: { + actionType: "insertOne", + }, }, - }, - ], - } - const query = await createQuery({ - fields: { - json: data, - extra: { - actionType: "insertOne", - }, - }, - queryVerb: "create", - }) + queryVerb: "create", + }) - const result = await config.api.query.execute(query._id!) - expect(result.data).toEqual([ - { - acknowledged: true, - insertedId: expectValidId, - }, - ]) + const result = await config.api.query.execute(query._id!) + expect(result.data).toEqual([ + { + acknowledged: true, + insertedId: expectValidId, + }, + ]) - await withCollection(async collection => { - const doc = await collection.findOne({ foo: { $eq: "bar" } }) - expect(doc).toEqual({ - _id: expectValidBsonObjectId, - ...data, + await withCollection(async collection => { + const doc = await collection.findOne({ foo: { $eq: "te}st" } }) + expect(doc).toEqual({ + _id: expectValidBsonObjectId, + foo: "te}st", + }) + }) }) - }) - }) -}) + + it("should be able to save deeply nested data", async () => { + const data = { + foo: "bar", + data: [ + { cid: 1 }, + { cid: 2 }, + { + nested: { + name: "test", + ary: [1, 2, 3], + aryOfObjects: [{ a: 1 }, { b: 2 }], + }, + }, + ], + } + const query = await createQuery({ + fields: { + json: data, + extra: { + actionType: "insertOne", + }, + }, + queryVerb: "create", + }) + + const result = await config.api.query.execute(query._id!) + expect(result.data).toEqual([ + { + acknowledged: true, + insertedId: expectValidId, + }, + ]) + + await withCollection(async collection => { + const doc = await collection.findOne({ foo: { $eq: "bar" } }) + expect(doc).toEqual({ + _id: expectValidBsonObjectId, + ...data, + }) + }) + }) + } + ) +} diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index bf8f5a2a1c..02995e6d0a 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -2,34 +2,24 @@ import * as setup from "./utilities" import { DatabaseName, - getDatasource, - knexClient, + datasourceDescribe, } from "../../../integrations/tests/utils" import tk from "timekeeper" import emitter from "../../../../src/events" import { outputProcessing } from "../../../utilities/rowProcessor" -import { - context, - InternalTable, - tenancy, - features, - utils, -} from "@budibase/backend-core" +import { context, InternalTable, tenancy, utils } from "@budibase/backend-core" import { quotas } from "@budibase/pro" import { AIOperationEnum, - AttachmentFieldMetadata, AutoFieldSubType, Datasource, - DateFieldMetadata, DeleteRow, FieldSchema, FieldType, BBReferenceFieldSubType, FormulaType, INTERNAL_TABLE_SOURCE_ID, - NumberFieldMetadata, QuotaUsageType, RelationshipType, Row, @@ -42,6 +32,7 @@ import { JsonFieldSubType, RowExportFormat, RelationSchemaField, + FormulaResponseType, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" import _, { merge } from "lodash" @@ -50,6 +41,7 @@ import { Knex } from "knex" import { InternalTables } from "../../../db/utils" import { withEnv } from "../../../environment" import { JsTimeoutError } from "@budibase/string-templates" +import { isDate } from "../../../utilities" jest.mock("@budibase/pro", () => ({ ...jest.requireActual("@budibase/pro"), @@ -89,609 +81,167 @@ async function waitForEvent( return await p } -describe.each([ - ["lucene", undefined], - ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], - [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], -])("/rows (%s)", (providerType, dsProvider) => { - const isInternal = dsProvider === undefined - const isLucene = providerType === "lucene" - const isSqs = providerType === "sqs" - const isMSSQL = providerType === DatabaseName.SQL_SERVER - const isOracle = providerType === DatabaseName.ORACLE - const config = setup.getConfig() +function encodeJS(binding: string) { + return `{{ js "${Buffer.from(binding).toString("base64")}"}}` +} - let table: Table - let datasource: Datasource | undefined - let client: Knex | undefined - let envCleanup: (() => void) | undefined +const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] }) - beforeAll(async () => { - await features.testutils.withFeatureFlags("*", { SQS: true }, () => - config.init() - ) - envCleanup = features.testutils.setFeatureFlags("*", { - SQS: isSqs, - }) - - if (dsProvider) { - const rawDatasource = await dsProvider - datasource = await config.createDatasource({ - datasource: rawDatasource, - }) - client = await knexClient(rawDatasource) - } - }) - - afterAll(async () => { - setup.afterAll() - if (envCleanup) { - envCleanup() - } - }) - - function saveTableRequest( - // We omit the name field here because it's generated in the function with a - // high likelihood to be unique. Tests should not have any reason to control - // the table name they're writing to. - ...overrides: Partial>[] - ): SaveTableRequest { - const defaultSchema: TableSchema = { - id: { - type: FieldType.NUMBER, - name: "id", - autocolumn: true, - constraints: { - presence: true, - }, - }, - } - - for (const override of overrides) { - if (override.primary) { - delete defaultSchema.id - } - } - - const req: SaveTableRequest = { - name: uuid.v4().substring(0, 10), - type: "table", - sourceType: datasource - ? TableSourceType.EXTERNAL - : TableSourceType.INTERNAL, - sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID, - primary: ["id"], - schema: defaultSchema, - } - const merged = merge(req, ...overrides) - return merged - } - - function defaultTable( - // We omit the name field here because it's generated in the function with a - // high likelihood to be unique. Tests should not have any reason to control - // the table name they're writing to. - ...overrides: Partial>[] - ): SaveTableRequest { - return saveTableRequest( - { - primaryDisplay: "name", - schema: { - name: { - type: FieldType.STRING, - name: "name", - constraints: { - type: "string", - }, - }, - description: { - type: FieldType.STRING, - name: "description", - constraints: { - type: "string", - }, - }, - }, - }, - ...overrides - ) - } - - beforeEach(async () => { - mocks.licenses.useCloudFree() - }) - - const getRowUsage = async () => { - const { total } = await config.doInContext(undefined, () => - quotas.getCurrentUsageValues(QuotaUsageType.STATIC, StaticQuotaName.ROWS) - ) - return total - } - - const assertRowUsage = async (expected: number) => { - const usage = await getRowUsage() - - // Because our quota tracking is not perfect, we allow a 10% margin of - // error. This is to account for the fact that parallel writes can result - // in some quota updates getting lost. We don't have any need to solve this - // right now, so we just allow for some error. - if (expected === 0) { - expect(usage).toEqual(0) - return - } - expect(usage).toBeGreaterThan(expected * 0.9) - expect(usage).toBeLessThan(expected * 1.1) - } - - const defaultRowFields = isInternal - ? { - type: "row", - createdAt: timestamp, - updatedAt: timestamp, - } - : undefined - - beforeAll(async () => { - table = await config.api.table.save(defaultTable()) - }) - - describe("create", () => { - it("creates a new row successfully", async () => { - const rowUsage = await getRowUsage() - const row = await config.api.row.save(table._id!, { - name: "Test Contact", - }) - expect(row.name).toEqual("Test Contact") - expect(row._rev).toBeDefined() - await assertRowUsage(isInternal ? rowUsage + 1 : rowUsage) - }) - - it("fails to create a row for a table that does not exist", async () => { - const rowUsage = await getRowUsage() - await config.api.row.save("1234567", {}, { status: 404 }) - await assertRowUsage(rowUsage) - }) - - it("fails to create a row if required fields are missing", async () => { - const rowUsage = await getRowUsage() - const table = await config.api.table.save( - saveTableRequest({ - schema: { - required: { - type: FieldType.STRING, - name: "required", - constraints: { - type: "string", - presence: true, - }, - }, - }, - }) - ) - await config.api.row.save( - table._id!, - {}, - { - status: 500, - body: { - validationErrors: { - required: ["can't be blank"], - }, - }, - } - ) - await assertRowUsage(rowUsage) - }) - - isInternal && - it("increment row autoId per create row request", async () => { - const rowUsage = await getRowUsage() - - const newTable = await config.api.table.save( - saveTableRequest({ - schema: { - "Row ID": { - name: "Row ID", - type: FieldType.NUMBER, - subtype: AutoFieldSubType.AUTO_ID, - icon: "ri-magic-line", - autocolumn: true, - constraints: { - type: "number", - presence: true, - numericality: { - greaterThanOrEqualTo: "", - lessThanOrEqualTo: "", - }, - }, - }, - }, - }) - ) - - let previousId = 0 - for (let i = 0; i < 10; i++) { - const row = await config.api.row.save(newTable._id!, {}) - expect(row["Row ID"]).toBeGreaterThan(previousId) - previousId = row["Row ID"] - } - await assertRowUsage(isInternal ? rowUsage + 10 : rowUsage) - }) - - isInternal && - it("should increment auto ID correctly when creating rows in parallel", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - "Row ID": { - name: "Row ID", - type: FieldType.NUMBER, - subtype: AutoFieldSubType.AUTO_ID, - icon: "ri-magic-line", - autocolumn: true, - constraints: { - type: "number", - presence: true, - numericality: { - greaterThanOrEqualTo: "", - lessThanOrEqualTo: "", - }, - }, - }, - }, - }) - ) - - const sequence = Array(50) - .fill(0) - .map((_, i) => i + 1) - - // This block of code is simulating users creating auto ID rows at the - // same time. It's expected that this operation will sometimes return - // a document conflict error (409), but the idea is to retry in those - // situations. The code below does this a large number of times with - // small, random delays between them to try and get through the list - // as quickly as possible. - await Promise.all( - sequence.map(async () => { - const attempts = 30 - for (let attempt = 0; attempt < attempts; attempt++) { - try { - await config.api.row.save(table._id!, {}) - return - } catch (e) { - await new Promise(r => setTimeout(r, Math.random() * 50)) - } - } - throw new Error(`Failed to create row after ${attempts} attempts`) - }) - ) - - const rows = await config.api.row.fetch(table._id!) - expect(rows).toHaveLength(50) - - // The main purpose of this test is to ensure that even under pressure, - // we maintain data integrity. An auto ID column should hand out - // monotonically increasing unique integers no matter what. - const ids = rows.map(r => r["Row ID"]) - expect(ids).toEqual(expect.arrayContaining(sequence)) - }) - - isLucene && - it("row values are coerced", async () => { - const str: FieldSchema = { - type: FieldType.STRING, - name: "str", - constraints: { type: "string", presence: false }, - } - const singleAttachment: FieldSchema = { - type: FieldType.ATTACHMENT_SINGLE, - name: "single attachment", - constraints: { presence: false }, - } - const attachmentList: AttachmentFieldMetadata = { - type: FieldType.ATTACHMENTS, - name: "attachments", - constraints: { type: "array", presence: false }, - } - const signature: FieldSchema = { - type: FieldType.SIGNATURE_SINGLE, - name: "signature", - constraints: { presence: false }, - } - const bool: FieldSchema = { - type: FieldType.BOOLEAN, - name: "boolean", - constraints: { type: "boolean", presence: false }, - } - const number: NumberFieldMetadata = { - type: FieldType.NUMBER, - name: "str", - constraints: { type: "number", presence: false }, - } - const datetime: DateFieldMetadata = { - type: FieldType.DATETIME, - name: "datetime", - constraints: { - type: "string", - presence: false, - datetime: { earliest: "", latest: "" }, - }, - } - const arrayField: FieldSchema = { - type: FieldType.ARRAY, - constraints: { - type: JsonFieldSubType.ARRAY, - presence: false, - inclusion: ["One", "Two", "Three"], - }, - name: "Sample Tags", - sortable: false, - } - const optsField: FieldSchema = { - name: "Sample Opts", - type: FieldType.OPTIONS, - constraints: { - type: "string", - presence: false, - inclusion: ["Alpha", "Beta", "Gamma"], - }, - } - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: str, - stringUndefined: str, - stringNull: str, - stringString: str, - numberEmptyString: number, - numberNull: number, - numberUndefined: number, - numberString: number, - numberNumber: number, - datetimeEmptyString: datetime, - datetimeNull: datetime, - datetimeUndefined: datetime, - datetimeString: datetime, - datetimeDate: datetime, - boolNull: bool, - boolEmpty: bool, - boolUndefined: bool, - boolString: bool, - boolBool: bool, - singleAttachmentNull: singleAttachment, - singleAttachmentUndefined: singleAttachment, - attachmentListNull: attachmentList, - attachmentListUndefined: attachmentList, - attachmentListEmpty: attachmentList, - attachmentListEmptyArrayStr: attachmentList, - signatureNull: signature, - signatureUndefined: signature, - arrayFieldEmptyArrayStr: arrayField, - arrayFieldArrayStrKnown: arrayField, - arrayFieldNull: arrayField, - arrayFieldUndefined: arrayField, - optsFieldEmptyStr: optsField, - optsFieldUndefined: optsField, - optsFieldNull: optsField, - optsFieldStrKnown: optsField, - }, - }) - ) - - const datetimeStr = "1984-04-20T00:00:00.000Z" - - const row = await config.api.row.save(table._id!, { - name: "Test Row", - stringUndefined: undefined, - stringNull: null, - stringString: "i am a string", - numberEmptyString: "", - numberNull: null, - numberUndefined: undefined, - numberString: "123", - numberNumber: 123, - datetimeEmptyString: "", - datetimeNull: null, - datetimeUndefined: undefined, - datetimeString: datetimeStr, - datetimeDate: new Date(datetimeStr), - boolNull: null, - boolEmpty: "", - boolUndefined: undefined, - boolString: "true", - boolBool: true, - tableId: table._id, - singleAttachmentNull: null, - singleAttachmentUndefined: undefined, - attachmentListNull: null, - attachmentListUndefined: undefined, - attachmentListEmpty: "", - attachmentListEmptyArrayStr: "[]", - signatureNull: null, - signatureUndefined: undefined, - arrayFieldEmptyArrayStr: "[]", - arrayFieldUndefined: undefined, - arrayFieldNull: null, - arrayFieldArrayStrKnown: "['One']", - optsFieldEmptyStr: "", - optsFieldUndefined: undefined, - optsFieldNull: null, - optsFieldStrKnown: "Alpha", - }) - - expect(row.stringUndefined).toBe(undefined) - expect(row.stringNull).toBe(null) - expect(row.stringString).toBe("i am a string") - expect(row.numberEmptyString).toBe(null) - expect(row.numberNull).toBe(null) - expect(row.numberUndefined).toBe(undefined) - expect(row.numberString).toBe(123) - expect(row.numberNumber).toBe(123) - expect(row.datetimeEmptyString).toBe(null) - expect(row.datetimeNull).toBe(null) - expect(row.datetimeUndefined).toBe(undefined) - expect(row.datetimeString).toBe(new Date(datetimeStr).toISOString()) - expect(row.datetimeDate).toBe(new Date(datetimeStr).toISOString()) - expect(row.boolNull).toBe(null) - expect(row.boolEmpty).toBe(null) - expect(row.boolUndefined).toBe(undefined) - expect(row.boolString).toBe(true) - expect(row.boolBool).toBe(true) - expect(row.singleAttachmentNull).toEqual(null) - expect(row.singleAttachmentUndefined).toBe(undefined) - expect(row.attachmentListNull).toEqual([]) - expect(row.attachmentListUndefined).toBe(undefined) - expect(row.attachmentListEmpty).toEqual([]) - expect(row.attachmentListEmptyArrayStr).toEqual([]) - expect(row.signatureNull).toEqual(null) - expect(row.signatureUndefined).toBe(undefined) - expect(row.arrayFieldEmptyArrayStr).toEqual([]) - expect(row.arrayFieldNull).toEqual([]) - expect(row.arrayFieldUndefined).toEqual(undefined) - expect(row.optsFieldEmptyStr).toEqual(null) - expect(row.optsFieldUndefined).toEqual(undefined) - expect(row.optsFieldNull).toEqual(null) - expect(row.arrayFieldArrayStrKnown).toEqual(["One"]) - expect(row.optsFieldStrKnown).toEqual("Alpha") - }) - - isInternal && - it("doesn't allow creating in user table", async () => { - const response = await config.api.row.save( - InternalTable.USER_METADATA, - { - firstName: "Joe", - lastName: "Joe", - email: "joe@joe.com", - roles: {}, - }, - { status: 400 } - ) - expect(response.message).toBe("Cannot create new user entry.") - }) - - it("should not mis-parse date string out of JSON", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - type: FieldType.STRING, - name: "name", - }, - }, - }) - ) - - const row = await config.api.row.save(table._id!, { - name: `{ "foo": "2023-01-26T11:48:57.000Z" }`, - }) - - expect(row.name).toEqual(`{ "foo": "2023-01-26T11:48:57.000Z" }`) - }) - - describe("default values", () => { +if (descriptions.length) { + describe.each(descriptions)( + "/rows ($dbName)", + ({ config, dsProvider, isInternal, isMSSQL, isOracle }) => { let table: Table + let datasource: Datasource | undefined + let client: Knex | undefined - describe("string column", () => { - beforeAll(async () => { - table = await config.api.table.save( + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource + client = ds.client + }) + + afterAll(async () => { + setup.afterAll() + }) + + function saveTableRequest( + // We omit the name field here because it's generated in the function with a + // high likelihood to be unique. Tests should not have any reason to control + // the table name they're writing to. + ...overrides: Partial>[] + ): SaveTableRequest { + const defaultSchema: TableSchema = { + id: { + type: FieldType.NUMBER, + name: "id", + autocolumn: true, + constraints: { + presence: true, + }, + }, + } + + for (const override of overrides) { + if (override.primary) { + delete defaultSchema.id + } + } + + const req: SaveTableRequest = { + name: uuid.v4().substring(0, 10), + type: "table", + sourceType: datasource + ? TableSourceType.EXTERNAL + : TableSourceType.INTERNAL, + sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID, + primary: ["id"], + schema: defaultSchema, + } + const merged = merge(req, ...overrides) + return merged + } + + function defaultTable( + // We omit the name field here because it's generated in the function with a + // high likelihood to be unique. Tests should not have any reason to control + // the table name they're writing to. + ...overrides: Partial>[] + ): SaveTableRequest { + return saveTableRequest( + { + primaryDisplay: "name", + schema: { + name: { + type: FieldType.STRING, + name: "name", + constraints: { + type: "string", + }, + }, + description: { + type: FieldType.STRING, + name: "description", + constraints: { + type: "string", + }, + }, + }, + }, + ...overrides + ) + } + + beforeEach(async () => { + mocks.licenses.useCloudFree() + }) + + const getRowUsage = async () => { + const { total } = await config.doInContext(undefined, () => + quotas.getCurrentUsageValues( + QuotaUsageType.STATIC, + StaticQuotaName.ROWS + ) + ) + return total + } + + const assertRowUsage = async (expected: number) => { + const usage = await getRowUsage() + + // Because our quota tracking is not perfect, we allow a 10% margin of + // error. This is to account for the fact that parallel writes can result + // in some quota updates getting lost. We don't have any need to solve this + // right now, so we just allow for some error. + if (expected === 0) { + expect(usage).toEqual(0) + return + } + expect(usage).toBeGreaterThan(expected * 0.9) + expect(usage).toBeLessThan(expected * 1.1) + } + + const defaultRowFields = isInternal + ? { + type: "row", + createdAt: timestamp, + updatedAt: timestamp, + } + : undefined + + beforeAll(async () => { + table = await config.api.table.save(defaultTable()) + }) + + describe("create", () => { + it("creates a new row successfully", async () => { + const rowUsage = await getRowUsage() + const row = await config.api.row.save(table._id!, { + name: "Test Contact", + }) + expect(row.name).toEqual("Test Contact") + expect(row._rev).toBeDefined() + await assertRowUsage(isInternal ? rowUsage + 1 : rowUsage) + }) + + it("fails to create a row for a table that does not exist", async () => { + const rowUsage = await getRowUsage() + await config.api.row.save("1234567", {}, { status: 404 }) + await assertRowUsage(rowUsage) + }) + + it("fails to create a row if required fields are missing", async () => { + const rowUsage = await getRowUsage() + const table = await config.api.table.save( saveTableRequest({ schema: { - description: { - name: "description", + required: { type: FieldType.STRING, - default: "default description", - }, - }, - }) - ) - }) - - it("creates a new row with a default value successfully", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.description).toEqual("default description") - }) - - it("does not use default value if value specified", async () => { - const row = await config.api.row.save(table._id!, { - description: "specified description", - }) - expect(row.description).toEqual("specified description") - }) - - it("uses the default value if value is null", async () => { - const row = await config.api.row.save(table._id!, { - description: null, - }) - expect(row.description).toEqual("default description") - }) - - it("uses the default value if value is undefined", async () => { - const row = await config.api.row.save(table._id!, { - description: undefined, - }) - expect(row.description).toEqual("default description") - }) - }) - - describe("number column", () => { - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - age: { - name: "age", - type: FieldType.NUMBER, - default: "25", - }, - }, - }) - ) - }) - - it("creates a new row with a default value successfully", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.age).toEqual(25) - }) - - it("does not use default value if value specified", async () => { - const row = await config.api.row.save(table._id!, { - age: 30, - }) - expect(row.age).toEqual(30) - }) - }) - - describe("date column", () => { - it("creates a row with a default value successfully", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - date: { - name: "date", - type: FieldType.DATETIME, - default: "2023-01-26T11:48:57.000Z", - }, - }, - }) - ) - const row = await config.api.row.save(table._id!, {}) - expect(row.date).toEqual("2023-01-26T11:48:57.000Z") - }) - - it("gives an error if the default value is invalid", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - date: { - name: "date", - type: FieldType.DATETIME, - default: "invalid", + name: "required", + constraints: { + type: "string", + presence: true, + }, }, }, }) @@ -700,297 +250,193 @@ describe.each([ table._id!, {}, { - status: 400, + status: 500, body: { - message: `Invalid default value for field 'date' - Invalid date value: "invalid"`, + validationErrors: { + required: ["can't be blank"], + }, }, } ) - }) - }) - - describe("options column", () => { - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - status: { - name: "status", - type: FieldType.OPTIONS, - default: "requested", - constraints: { - inclusion: ["requested", "approved"], - }, - }, - }, - }) - ) + await assertRowUsage(rowUsage) }) - it("creates a new row with a default value successfully", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.status).toEqual("requested") - }) + isInternal && + it("increment row autoId per create row request", async () => { + const rowUsage = await getRowUsage() - it("does not use default value if value specified", async () => { - const row = await config.api.row.save(table._id!, { - status: "approved", - }) - expect(row.status).toEqual("approved") - }) - }) - - describe("array column", () => { - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - food: { - name: "food", - type: FieldType.ARRAY, - default: ["apple", "orange"], - constraints: { - type: JsonFieldSubType.ARRAY, - inclusion: ["apple", "orange", "banana"], - }, - }, - }, - }) - ) - }) - - it("creates a new row with a default value successfully", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.food).toEqual(["apple", "orange"]) - }) - - it("creates a new row with a default value when given an empty list", async () => { - const row = await config.api.row.save(table._id!, { food: [] }) - expect(row.food).toEqual(["apple", "orange"]) - }) - - it("does not use default value if value specified", async () => { - const row = await config.api.row.save(table._id!, { - food: ["orange"], - }) - expect(row.food).toEqual(["orange"]) - }) - - it("resets back to its default value when empty", async () => { - let row = await config.api.row.save(table._id!, { - food: ["orange"], - }) - row = await config.api.row.save(table._id!, { ...row, food: [] }) - expect(row.food).toEqual(["apple", "orange"]) - }) - }) - - describe("user column", () => { - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - user: { - name: "user", - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - default: "{{ [Current User]._id }}", - }, - }, - }) - ) - }) - - it("creates a new row with a default value successfully", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.user._id).toEqual(config.getUser()._id) - }) - - it("does not use default value if value specified", async () => { - const id = `us_${utils.newid()}` - await config.createUser({ _id: id }) - const row = await config.api.row.save(table._id!, { - user: id, - }) - expect(row.user._id).toEqual(id) - }) - }) - - describe("multi-user column", () => { - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - users: { - name: "users", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - default: ["{{ [Current User]._id }}"], - }, - }, - }) - ) - }) - - it("creates a new row with a default value successfully", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.users).toHaveLength(1) - expect(row.users[0]._id).toEqual(config.getUser()._id) - }) - - it("does not use default value if value specified", async () => { - const id = `us_${utils.newid()}` - await config.createUser({ _id: id }) - const row = await config.api.row.save(table._id!, { - users: [id], - }) - expect(row.users).toHaveLength(1) - expect(row.users[0]._id).toEqual(id) - }) - }) - - describe("boolean column", () => { - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - active: { - name: "active", - type: FieldType.BOOLEAN, - default: "true", - }, - }, - }) - ) - }) - - it("creates a new row with a default value successfully", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.active).toEqual(true) - }) - - it("does not use default value if value specified", async () => { - const row = await config.api.row.save(table._id!, { - active: false, - }) - expect(row.active).toEqual(false) - }) - }) - - describe("bigint column", () => { - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - bigNumber: { - name: "bigNumber", - type: FieldType.BIGINT, - default: "1234567890", - }, - }, - }) - ) - }) - - it("creates a new row with a default value successfully", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.bigNumber).toEqual("1234567890") - }) - - it("does not use default value if value specified", async () => { - const row = await config.api.row.save(table._id!, { - bigNumber: "9876543210", - }) - expect(row.bigNumber).toEqual("9876543210") - }) - }) - - describe("bindings", () => { - describe("string column", () => { - beforeAll(async () => { - table = await config.api.table.save( + const newTable = await config.api.table.save( saveTableRequest({ schema: { - description: { - name: "description", - type: FieldType.STRING, - default: `{{ date now "YYYY-MM-DDTHH:mm:ss" }}`, - }, - }, - }) - ) - }) - - it("can use bindings in default values", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.description).toMatch( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ - ) - }) - - it("does not use default value if value specified", async () => { - const row = await config.api.row.save(table._id!, { - description: "specified description", - }) - expect(row.description).toEqual("specified description") - }) - - it("can bind the current user", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - user: { - name: "user", - type: FieldType.STRING, - default: `{{ [Current User]._id }}`, - }, - }, - }) - ) - const row = await config.api.row.save(table._id!, {}) - expect(row.user).toEqual(config.getUser()._id) - }) - - it("cannot access current user password", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - user: { - name: "user", - type: FieldType.STRING, - default: `{{ user.password }}`, - }, - }, - }) - ) - const row = await config.api.row.save(table._id!, {}) - // For some reason it's null for internal tables, and undefined for - // external. - expect(row.user == null).toBe(true) - }) - }) - - describe("number column", () => { - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - age: { - name: "age", + "Row ID": { + name: "Row ID", type: FieldType.NUMBER, - default: `{{ sum 10 10 5 }}`, + subtype: AutoFieldSubType.AUTO_ID, + icon: "ri-magic-line", + autocolumn: true, + constraints: { + type: "number", + presence: true, + numericality: { + greaterThanOrEqualTo: "", + lessThanOrEqualTo: "", + }, + }, }, }, }) ) + + let previousId = 0 + for (let i = 0; i < 10; i++) { + const row = await config.api.row.save(newTable._id!, {}) + expect(row["Row ID"]).toBeGreaterThan(previousId) + previousId = row["Row ID"] + } + await assertRowUsage(isInternal ? rowUsage + 10 : rowUsage) }) - it("can use bindings in default values", async () => { - const row = await config.api.row.save(table._id!, {}) - expect(row.age).toEqual(25) + isInternal && + it("should increment auto ID correctly when creating rows in parallel", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + "Row ID": { + name: "Row ID", + type: FieldType.NUMBER, + subtype: AutoFieldSubType.AUTO_ID, + icon: "ri-magic-line", + autocolumn: true, + constraints: { + type: "number", + presence: true, + numericality: { + greaterThanOrEqualTo: "", + lessThanOrEqualTo: "", + }, + }, + }, + }, + }) + ) + + const sequence = Array(50) + .fill(0) + .map((_, i) => i + 1) + + // This block of code is simulating users creating auto ID rows at the + // same time. It's expected that this operation will sometimes return + // a document conflict error (409), but the idea is to retry in those + // situations. The code below does this a large number of times with + // small, random delays between them to try and get through the list + // as quickly as possible. + await Promise.all( + sequence.map(async () => { + const attempts = 30 + for (let attempt = 0; attempt < attempts; attempt++) { + try { + await config.api.row.save(table._id!, {}) + return + } catch (e) { + await new Promise(r => setTimeout(r, Math.random() * 50)) + } + } + throw new Error( + `Failed to create row after ${attempts} attempts` + ) + }) + ) + + const rows = await config.api.row.fetch(table._id!) + expect(rows).toHaveLength(50) + + // The main purpose of this test is to ensure that even under pressure, + // we maintain data integrity. An auto ID column should hand out + // monotonically increasing unique integers no matter what. + const ids = rows.map(r => r["Row ID"]) + expect(ids).toEqual(expect.arrayContaining(sequence)) }) - describe("invalid default value", () => { + isInternal && + it("doesn't allow creating in user table", async () => { + const response = await config.api.row.save( + InternalTable.USER_METADATA, + { + firstName: "Joe", + lastName: "Joe", + email: "joe@joe.com", + roles: {}, + }, + { status: 400 } + ) + expect(response.message).toBe("Cannot create new user entry.") + }) + + it("should not mis-parse date string out of JSON", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + type: FieldType.STRING, + name: "name", + }, + }, + }) + ) + + const row = await config.api.row.save(table._id!, { + name: `{ "foo": "2023-01-26T11:48:57.000Z" }`, + }) + + expect(row.name).toEqual(`{ "foo": "2023-01-26T11:48:57.000Z" }`) + }) + + describe("default values", () => { + let table: Table + + describe("string column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + description: { + name: "description", + type: FieldType.STRING, + default: "default description", + }, + }, + }) + ) + }) + + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.description).toEqual("default description") + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + description: "specified description", + }) + expect(row.description).toEqual("specified description") + }) + + it("uses the default value if value is null", async () => { + const row = await config.api.row.save(table._id!, { + description: null, + }) + expect(row.description).toEqual("default description") + }) + + it("uses the default value if value is undefined", async () => { + const row = await config.api.row.save(table._id!, { + description: undefined, + }) + expect(row.description).toEqual("default description") + }) + }) + + describe("number column", () => { beforeAll(async () => { table = await config.api.table.save( saveTableRequest({ @@ -998,2307 +444,2674 @@ describe.each([ age: { name: "age", type: FieldType.NUMBER, - default: `{{ capitalize "invalid" }}`, + default: "25", }, }, }) ) }) - it("throws an error when invalid default value", async () => { + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.age).toEqual(25) + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + age: 30, + }) + expect(row.age).toEqual(30) + }) + }) + + describe("date column", () => { + it("creates a row with a default value successfully", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + date: { + name: "date", + type: FieldType.DATETIME, + default: "2023-01-26T11:48:57.000Z", + }, + }, + }) + ) + const row = await config.api.row.save(table._id!, {}) + expect(row.date).toEqual("2023-01-26T11:48:57.000Z") + }) + + it("gives an error if the default value is invalid", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + date: { + name: "date", + type: FieldType.DATETIME, + default: "invalid", + }, + }, + }) + ) await config.api.row.save( table._id!, {}, { status: 400, body: { - message: - "Invalid default value for field 'age' - Invalid number value \"Invalid\"", + message: `Invalid default value for field 'date' - Invalid date value: "invalid"`, }, } ) }) }) - }) - }) - }) - !isLucene && - describe("relations to same table", () => { - let relatedRows: Row[] - - beforeAll(async () => { - const relatedTable = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - }, - }) - ) - const relatedTableId = relatedTable._id! - table = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }, - }) - ) - relatedRows = await Promise.all([ - config.api.row.save(relatedTableId, { name: "foo" }), - config.api.row.save(relatedTableId, { name: "bar" }), - config.api.row.save(relatedTableId, { name: "baz" }), - config.api.row.save(relatedTableId, { name: "boo" }), - ]) - }) - - it("can create rows with both relationships", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], - }) - - expect(row).toEqual( - expect.objectContaining({ - name: "test", - related1: [ - { - _id: relatedRows[0]._id, - primaryDisplay: relatedRows[0].name, - }, - ], - related2: [ - { - _id: relatedRows[1]._id, - primaryDisplay: relatedRows[1].name, - }, - ], - }) - ) - }) - - it("can create rows with no relationships", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - }) - - expect(row.related1).toBeUndefined() - expect(row.related2).toBeUndefined() - }) - - it("can create rows with only one relationships field", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - related1: [], - related2: [relatedRows[1]._id!], - }) - - expect(row).toEqual( - expect.objectContaining({ - name: "test", - related2: [ - { - _id: relatedRows[1]._id, - primaryDisplay: relatedRows[1].name, - }, - ], - }) - ) - expect(row.related1).toBeUndefined() - }) - }) - }) - - describe("get", () => { - it("reads an existing row successfully", async () => { - const existing = await config.api.row.save(table._id!, {}) - - const res = await config.api.row.get(table._id!, existing._id!) - - expect(res).toEqual({ - ...existing, - ...defaultRowFields, - }) - }) - - it("returns 404 when row does not exist", async () => { - const table = await config.api.table.save(defaultTable()) - await config.api.row.save(table._id!, {}) - await config.api.row.get(table._id!, "1234567", { - status: 404, - }) - }) - - isInternal && - it("can search row from user table", async () => { - const res = await config.api.row.get( - InternalTables.USER_METADATA, - config.userMetadataId! - ) - - expect(res).toEqual({ - ...config.getUser(), - _id: config.userMetadataId!, - _rev: expect.any(String), - roles: undefined, - roleId: "ADMIN", - tableId: InternalTables.USER_METADATA, - }) - }) - }) - - describe("fetch", () => { - it("fetches all rows for given tableId", async () => { - const table = await config.api.table.save(defaultTable()) - const rows = await Promise.all([ - config.api.row.save(table._id!, {}), - config.api.row.save(table._id!, {}), - ]) - - const res = await config.api.row.fetch(table._id!) - expect(res.map(r => r._id)).toEqual( - expect.arrayContaining(rows.map(r => r._id)) - ) - }) - - it("returns 404 when table does not exist", async () => { - await config.api.row.fetch("1234567", { status: 404 }) - }) - }) - - describe("update", () => { - it("updates an existing row successfully", async () => { - const existing = await config.api.row.save(table._id!, {}) - const rowUsage = await getRowUsage() - - const res = await config.api.row.save(table._id!, { - _id: existing._id, - _rev: existing._rev, - name: "Updated Name", - }) - - expect(res.name).toEqual("Updated Name") - await assertRowUsage(rowUsage) - }) - - !isInternal && - it("can update a row on an external table with a primary key", async () => { - const tableName = uuid.v4().substring(0, 10) - await client!.schema.createTable(tableName, table => { - table.increments("id").primary() - table.string("name") - }) - - const res = await config.api.datasource.fetchSchema({ - datasourceId: datasource!._id!, - }) - const table = res.datasource.entities![tableName] - - const row = await config.api.row.save(table._id!, { - id: 1, - name: "Row 1", - }) - - const updatedRow = await config.api.row.save(table._id!, { - _id: row._id!, - name: "Row 1 Updated", - }) - - expect(updatedRow.name).toEqual("Row 1 Updated") - - const rows = await config.api.row.fetch(table._id!) - expect(rows).toHaveLength(1) - }) - - !isLucene && - describe("relations to same table", () => { - let relatedRows: Row[] - - beforeAll(async () => { - const relatedTable = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - }, - }) - ) - const relatedTableId = relatedTable._id! - table = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }, - }) - ) - relatedRows = await Promise.all([ - config.api.row.save(relatedTableId, { name: "foo" }), - config.api.row.save(relatedTableId, { name: "bar" }), - config.api.row.save(relatedTableId, { name: "baz" }), - config.api.row.save(relatedTableId, { name: "boo" }), - ]) - }) - - it("can edit rows with both relationships", async () => { - let row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], - }) - - row = await config.api.row.save(table._id!, { - ...row, - related1: [relatedRows[0]._id!, relatedRows[1]._id!], - related2: [relatedRows[2]._id!], - }) - - expect(row).toEqual( - expect.objectContaining({ - name: "test", - related1: expect.arrayContaining([ - { - _id: relatedRows[0]._id, - primaryDisplay: relatedRows[0].name, - }, - { - _id: relatedRows[1]._id, - primaryDisplay: relatedRows[1].name, - }, - ]), - related2: [ - { - _id: relatedRows[2]._id, - primaryDisplay: relatedRows[2].name, - }, - ], - }) - ) - }) - - it("can drop existing relationship", async () => { - let row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], - }) - - row = await config.api.row.save(table._id!, { - ...row, - related1: [], - related2: [relatedRows[2]._id!], - }) - - expect(row).toEqual( - expect.objectContaining({ - name: "test", - related2: [ - { - _id: relatedRows[2]._id, - primaryDisplay: relatedRows[2].name, - }, - ], - }) - ) - expect(row.related1).toBeUndefined() - }) - - it("can drop both relationships", async () => { - let row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], - }) - - row = await config.api.row.save(table._id!, { - ...row, - related1: [], - related2: [], - }) - - expect(row).toEqual( - expect.objectContaining({ - name: "test", - }) - ) - expect(row.related1).toBeUndefined() - expect(row.related2).toBeUndefined() - }) - }) - }) - - describe("patch", () => { - let otherTable: Table - - beforeAll(async () => { - table = await config.api.table.save(defaultTable()) - otherTable = await config.api.table.save( - defaultTable({ - schema: { - relationship: { - name: "relationship", - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: table._id!, - fieldName: "relationship", - }, - }, - }) - ) - }) - - it("should update only the fields that are supplied", async () => { - const existing = await config.api.row.save(table._id!, {}) - - const rowUsage = await getRowUsage() - - const row = await config.api.row.patch(table._id!, { - _id: existing._id!, - _rev: existing._rev!, - tableId: table._id!, - name: "Updated Name", - }) - - expect(row.name).toEqual("Updated Name") - expect(row.description).toEqual(existing.description) - - const savedRow = await config.api.row.get(table._id!, row._id!) - - expect(savedRow.description).toEqual(existing.description) - expect(savedRow.name).toEqual("Updated Name") - await assertRowUsage(rowUsage) - }) - - it("should update only the fields that are supplied and emit the correct oldRow", async () => { - let beforeRow = await config.api.row.save(table._id!, { - name: "test", - description: "test", - }) - const opts = { - name: "row:update", - matchFn: (event: UpdatedRowEventEmitter) => - event.row._id === beforeRow._id, - } - const event = await waitForEvent(opts, async () => { - await config.api.row.patch(table._id!, { - _id: beforeRow._id!, - _rev: beforeRow._rev!, - tableId: table._id!, - name: "Updated Name", - }) - }) - - expect(event.oldRow).toBeDefined() - expect(event.oldRow.name).toEqual("test") - expect(event.row.name).toEqual("Updated Name") - expect(event.oldRow.description).toEqual(beforeRow.description) - expect(event.row.description).toEqual(beforeRow.description) - }) - - it("should throw an error when given improper types", async () => { - const existing = await config.api.row.save(table._id!, {}) - const rowUsage = await getRowUsage() - - await config.api.row.patch( - table._id!, - { - _id: existing._id!, - _rev: existing._rev!, - tableId: table._id!, - name: 1, - }, - { status: 400 } - ) - - await assertRowUsage(rowUsage) - }) - - it("should not overwrite links if those links are not set", async () => { - let linkField: FieldSchema = { - type: FieldType.LINK, - name: "", - fieldName: "", - constraints: { - type: "array", - presence: false, - }, - relationshipType: RelationshipType.ONE_TO_MANY, - tableId: InternalTable.USER_METADATA, - } - - let table = await config.api.table.save({ - name: "TestTable", - type: "table", - sourceType: TableSourceType.INTERNAL, - sourceId: INTERNAL_TABLE_SOURCE_ID, - schema: { - user1: { ...linkField, name: "user1", fieldName: "user1" }, - user2: { ...linkField, name: "user2", fieldName: "user2" }, - }, - }) - - let user1 = await config.createUser() - let user2 = await config.createUser() - - let row = await config.api.row.save(table._id!, { - user1: [{ _id: user1._id }], - user2: [{ _id: user2._id }], - }) - - let getResp = await config.api.row.get(table._id!, row._id!) - expect(getResp.user1[0]._id).toEqual(user1._id) - expect(getResp.user2[0]._id).toEqual(user2._id) - - let patchResp = await config.api.row.patch(table._id!, { - _id: row._id!, - _rev: row._rev!, - tableId: table._id!, - user1: [{ _id: user2._id }], - }) - expect(patchResp.user1[0]._id).toEqual(user2._id) - expect(patchResp.user2[0]._id).toEqual(user2._id) - - getResp = await config.api.row.get(table._id!, row._id!) - expect(getResp.user1[0]._id).toEqual(user2._id) - expect(getResp.user2[0]._id).toEqual(user2._id) - }) - - it("should be able to remove a relationship from many side", async () => { - const row = await config.api.row.save(otherTable._id!, { - name: "test", - description: "test", - }) - const row2 = await config.api.row.save(otherTable._id!, { - name: "test", - description: "test", - }) - const { _id } = await config.api.row.save(table._id!, { - relationship: [{ _id: row._id }, { _id: row2._id }], - }) - const relatedRow = await config.api.row.get(table._id!, _id!, { - status: 200, - }) - expect(relatedRow.relationship.length).toEqual(2) - await config.api.row.save(table._id!, { - ...relatedRow, - relationship: [{ _id: row._id }], - }) - const afterRelatedRow = await config.api.row.get(table._id!, _id!, { - status: 200, - }) - expect(afterRelatedRow.relationship.length).toEqual(1) - expect(afterRelatedRow.relationship[0]._id).toEqual(row._id) - }) - - it("should be able to update relationships when both columns are same name", async () => { - let row = await config.api.row.save(table._id!, { - name: "test", - description: "test", - }) - let row2 = await config.api.row.save(otherTable._id!, { - name: "test", - description: "test", - relationship: [row._id], - }) - row = await config.api.row.get(table._id!, row._id!) - expect(row.relationship.length).toBe(1) - const resp = await config.api.row.patch(table._id!, { - _id: row._id!, - _rev: row._rev!, - tableId: row.tableId!, - name: "test2", - relationship: [row2._id], - }) - expect(resp.relationship.length).toBe(1) - }) - - !isInternal && - // MSSQL needs a setting called IDENTITY_INSERT to be set to ON to allow writing - // to identity columns. This is not something Budibase does currently. - providerType !== DatabaseName.SQL_SERVER && - it("should support updating fields that are part of a composite key", async () => { - const tableRequest = saveTableRequest({ - primary: ["number", "string"], - schema: { - string: { - type: FieldType.STRING, - name: "string", - }, - number: { - type: FieldType.NUMBER, - name: "number", - }, - }, - }) - - delete tableRequest.schema.id - - const table = await config.api.table.save(tableRequest) - - const stringValue = generator.word() - - // MySQL and MariaDB auto-increment fields have a minimum value of 1. If - // you try to save a row with a value of 0 it will use 1 instead. - const naturalValue = generator.integer({ min: 1, max: 1000 }) - - const existing = await config.api.row.save(table._id!, { - string: stringValue, - number: naturalValue, - }) - - expect(existing._id).toEqual(`%5B${naturalValue}%2C'${stringValue}'%5D`) - - const row = await config.api.row.patch(table._id!, { - _id: existing._id!, - _rev: existing._rev!, - tableId: table._id!, - string: stringValue, - number: 1500, - }) - - expect(row._id).toEqual(`%5B${"1500"}%2C'${stringValue}'%5D`) - }) - }) - - describe("destroy", () => { - beforeAll(async () => { - table = await config.api.table.save(defaultTable()) - }) - - it("should be able to delete a row", async () => { - const createdRow = await config.api.row.save(table._id!, {}) - const rowUsage = await getRowUsage() - - const res = await config.api.row.bulkDelete(table._id!, { - rows: [createdRow], - }) - expect(res[0]._id).toEqual(createdRow._id) - await assertRowUsage(isInternal ? rowUsage - 1 : rowUsage) - }) - - it("should be able to delete a row with ID only", async () => { - const createdRow = await config.api.row.save(table._id!, {}) - const rowUsage = await getRowUsage() - - const res = await config.api.row.bulkDelete(table._id!, { - rows: [createdRow._id!], - }) - expect(res[0]._id).toEqual(createdRow._id) - expect(res[0].tableId).toEqual(table._id!) - await assertRowUsage(isInternal ? rowUsage - 1 : rowUsage) - }) - - it("should be able to bulk delete rows, including a row that doesn't exist", async () => { - const createdRow = await config.api.row.save(table._id!, {}) - const createdRow2 = await config.api.row.save(table._id!, {}) - - const res = await config.api.row.bulkDelete(table._id!, { - rows: [createdRow, createdRow2, { _id: "9999999" }], - }) - - expect(res.map(r => r._id)).toEqual( - expect.arrayContaining([createdRow._id, createdRow2._id]) - ) - expect(res.length).toEqual(2) - }) - - !isLucene && - describe("relations to same table", () => { - let relatedRows: Row[] - - beforeAll(async () => { - const relatedTable = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - }, - }) - ) - const relatedTableId = relatedTable._id! - table = await config.api.table.save( - defaultTable({ - schema: { - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTableId, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }, - }) - ) - relatedRows = await Promise.all([ - config.api.row.save(relatedTableId, { name: "foo" }), - config.api.row.save(relatedTableId, { name: "bar" }), - config.api.row.save(relatedTableId, { name: "baz" }), - config.api.row.save(relatedTableId, { name: "boo" }), - ]) - }) - - it("can delete rows with both relationships", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], - }) - - await config.api.row.delete(table._id!, { _id: row._id! }) - - await config.api.row.get(table._id!, row._id!, { status: 404 }) - }) - - it("can delete rows with empty relationships", async () => { - const row = await config.api.row.save(table._id!, { - name: "test", - related1: [], - related2: [], - }) - - await config.api.row.delete(table._id!, { _id: row._id! }) - - await config.api.row.get(table._id!, row._id!, { status: 404 }) - }) - }) - }) - - describe("validate", () => { - beforeAll(async () => { - table = await config.api.table.save(defaultTable()) - }) - - it("should return no errors on valid row", async () => { - const rowUsage = await getRowUsage() - - const res = await config.api.row.validate(table._id!, { name: "ivan" }) - - expect(res.valid).toBe(true) - expect(Object.keys(res.errors)).toEqual([]) - await assertRowUsage(rowUsage) - }) - - it("should errors on invalid row", async () => { - const rowUsage = await getRowUsage() - - const res = await config.api.row.validate(table._id!, { name: 1 }) - - if (isInternal) { - expect(res.valid).toBe(false) - expect(Object.keys(res.errors)).toEqual(["name"]) - } else { - // Validation for external is not implemented, so it will always return valid - expect(res.valid).toBe(true) - expect(Object.keys(res.errors)).toEqual([]) - } - await assertRowUsage(rowUsage) - }) - }) - - describe("bulkDelete", () => { - beforeAll(async () => { - table = await config.api.table.save(defaultTable()) - }) - - it("should be able to delete a bulk set of rows", async () => { - const row1 = await config.api.row.save(table._id!, {}) - const row2 = await config.api.row.save(table._id!, {}) - const rowUsage = await getRowUsage() - - const res = await config.api.row.bulkDelete(table._id!, { - rows: [row1, row2], - }) - - expect(res.length).toEqual(2) - await config.api.row.get(table._id!, row1._id!, { status: 404 }) - await assertRowUsage(isInternal ? rowUsage - 2 : rowUsage) - }) - - it("should be able to delete a variety of row set types", async () => { - const [row1, row2, row3] = await Promise.all([ - config.api.row.save(table._id!, {}), - config.api.row.save(table._id!, {}), - config.api.row.save(table._id!, {}), - ]) - const rowUsage = await getRowUsage() - - const res = await config.api.row.bulkDelete(table._id!, { - rows: [row1, row2._id!, { _id: row3._id }], - }) - - expect(res.length).toEqual(3) - await config.api.row.get(table._id!, row1._id!, { status: 404 }) - await assertRowUsage(isInternal ? rowUsage - 3 : rowUsage) - }) - - it("should accept a valid row object and delete the row", async () => { - const row1 = await config.api.row.save(table._id!, {}) - const rowUsage = await getRowUsage() - - const res = await config.api.row.delete(table._id!, row1 as DeleteRow) - - expect(res.id).toEqual(row1._id) - await config.api.row.get(table._id!, row1._id!, { status: 404 }) - await assertRowUsage(isInternal ? rowUsage - 1 : rowUsage) - }) - - it.each([{ not: "valid" }, { rows: 123 }, "invalid"])( - "should ignore malformed/invalid delete request: %s", - async (request: any) => { - const rowUsage = await getRowUsage() - - await config.api.row.delete(table._id!, request, { - status: 400, - body: { - message: "Invalid delete rows request", - }, - }) - - await assertRowUsage(rowUsage) - } - ) - }) - - describe("bulkImport", () => { - isInternal && - it("should update Auto ID field after bulk import", async () => { - const table = await config.api.table.save( - saveTableRequest({ - primary: ["autoId"], - schema: { - autoId: { - name: "autoId", - type: FieldType.NUMBER, - subtype: AutoFieldSubType.AUTO_ID, - autocolumn: true, - constraints: { - type: "number", - presence: false, - }, - }, - }, - }) - ) - - let row = await config.api.row.save(table._id!, {}) - expect(row.autoId).toEqual(1) - - await config.api.row.bulkImport(table._id!, { - rows: [{ autoId: 2 }], - }) - - row = await config.api.row.save(table._id!, {}) - expect(row.autoId).toEqual(3) - }) - - isInternal && - it("should reject bulkImporting relationship fields", async () => { - const table1 = await config.api.table.save(saveTableRequest()) - const table2 = await config.api.table.save( - saveTableRequest({ - schema: { - relationship: { - name: "relationship", - type: FieldType.LINK, - tableId: table1._id!, - relationshipType: RelationshipType.ONE_TO_MANY, - fieldName: "relationship", - }, - }, - }) - ) - - const table1Row1 = await config.api.row.save(table1._id!, {}) - await config.api.row.bulkImport( - table2._id!, - { - rows: [{ relationship: [table1Row1._id!] }], - }, - { - status: 400, - body: { - message: - 'Can\'t bulk import relationship fields for internal databases, found value in field "relationship"', - }, - } - ) - }) - - it("should be able to bulkImport rows", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - type: FieldType.STRING, - name: "name", - }, - description: { - type: FieldType.STRING, - name: "description", - }, - }, - }) - ) - - const rowUsage = await getRowUsage() - - await config.api.row.bulkImport(table._id!, { - rows: [ - { - name: "Row 1", - description: "Row 1 description", - }, - { - name: "Row 2", - description: "Row 2 description", - }, - ], - }) - - const rows = await config.api.row.fetch(table._id!) - expect(rows.length).toEqual(2) - - rows.sort((a, b) => a.name.localeCompare(b.name)) - expect(rows[0].name).toEqual("Row 1") - expect(rows[0].description).toEqual("Row 1 description") - expect(rows[1].name).toEqual("Row 2") - expect(rows[1].description).toEqual("Row 2 description") - - await assertRowUsage(isInternal ? rowUsage + 2 : rowUsage) - }) - - isInternal && - it("should be able to update existing rows on bulkImport", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - type: FieldType.STRING, - name: "name", - }, - description: { - type: FieldType.STRING, - name: "description", - }, - }, - }) - ) - - const existingRow = await config.api.row.save(table._id!, { - name: "Existing row", - description: "Existing description", - }) - - const rowUsage = await getRowUsage() - - await config.api.row.bulkImport(table._id!, { - rows: [ - { - name: "Row 1", - description: "Row 1 description", - }, - { ...existingRow, name: "Updated existing row" }, - { - name: "Row 2", - description: "Row 2 description", - }, - ], - identifierFields: ["_id"], - }) - - const rows = await config.api.row.fetch(table._id!) - expect(rows.length).toEqual(3) - - rows.sort((a, b) => a.name.localeCompare(b.name)) - expect(rows[0].name).toEqual("Row 1") - expect(rows[0].description).toEqual("Row 1 description") - expect(rows[1].name).toEqual("Row 2") - expect(rows[1].description).toEqual("Row 2 description") - expect(rows[2].name).toEqual("Updated existing row") - expect(rows[2].description).toEqual("Existing description") - - await assertRowUsage(rowUsage + 2) - }) - - isInternal && - it("should create new rows if not identifierFields are provided", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - type: FieldType.STRING, - name: "name", - }, - description: { - type: FieldType.STRING, - name: "description", - }, - }, - }) - ) - - const existingRow = await config.api.row.save(table._id!, { - name: "Existing row", - description: "Existing description", - }) - - const rowUsage = await getRowUsage() - - await config.api.row.bulkImport(table._id!, { - rows: [ - { - name: "Row 1", - description: "Row 1 description", - }, - { ...existingRow, name: "Updated existing row" }, - { - name: "Row 2", - description: "Row 2 description", - }, - ], - }) - - const rows = await config.api.row.fetch(table._id!) - expect(rows.length).toEqual(4) - - rows.sort((a, b) => a.name.localeCompare(b.name)) - expect(rows[0].name).toEqual("Existing row") - expect(rows[0].description).toEqual("Existing description") - expect(rows[1].name).toEqual("Row 1") - expect(rows[1].description).toEqual("Row 1 description") - expect(rows[2].name).toEqual("Row 2") - expect(rows[2].description).toEqual("Row 2 description") - expect(rows[3].name).toEqual("Updated existing row") - expect(rows[3].description).toEqual("Existing description") - - await assertRowUsage(rowUsage + 3) - }) - - // Upserting isn't yet supported in MSSQL / Oracle, see: - // https://github.com/knex/knex/pull/6050 - !isMSSQL && - !isOracle && - it("should be able to update existing rows with bulkImport", async () => { - const table = await config.api.table.save( - saveTableRequest({ - primary: ["userId"], - schema: { - userId: { - type: FieldType.NUMBER, - name: "userId", - constraints: { - presence: true, - }, - }, - name: { - type: FieldType.STRING, - name: "name", - }, - description: { - type: FieldType.STRING, - name: "description", - }, - }, - }) - ) - - const row1 = await config.api.row.save(table._id!, { - userId: 1, - name: "Row 1", - description: "Row 1 description", - }) - - const row2 = await config.api.row.save(table._id!, { - userId: 2, - name: "Row 2", - description: "Row 2 description", - }) - - await config.api.row.bulkImport(table._id!, { - identifierFields: ["userId"], - rows: [ - { - userId: row1.userId, - name: "Row 1 updated", - description: "Row 1 description updated", - }, - { - userId: row2.userId, - name: "Row 2 updated", - description: "Row 2 description updated", - }, - { - userId: 3, - name: "Row 3", - description: "Row 3 description", - }, - ], - }) - - const rows = await config.api.row.fetch(table._id!) - expect(rows.length).toEqual(3) - - rows.sort((a, b) => a.name.localeCompare(b.name)) - expect(rows[0].name).toEqual("Row 1 updated") - expect(rows[0].description).toEqual("Row 1 description updated") - expect(rows[1].name).toEqual("Row 2 updated") - expect(rows[1].description).toEqual("Row 2 description updated") - expect(rows[2].name).toEqual("Row 3") - expect(rows[2].description).toEqual("Row 3 description") - }) - - // Upserting isn't yet supported in MSSQL or Oracle, see: - // https://github.com/knex/knex/pull/6050 - !isMSSQL && - !isOracle && - !isInternal && - it("should be able to update existing rows with composite primary keys with bulkImport", async () => { - const tableName = uuid.v4() - await client?.schema.createTable(tableName, table => { - table.integer("companyId") - table.integer("userId") - table.string("name") - table.string("description") - table.primary(["companyId", "userId"]) - }) - - const resp = await config.api.datasource.fetchSchema({ - datasourceId: datasource!._id!, - }) - const table = resp.datasource.entities![tableName] - - const row1 = await config.api.row.save(table._id!, { - companyId: 1, - userId: 1, - name: "Row 1", - description: "Row 1 description", - }) - - const row2 = await config.api.row.save(table._id!, { - companyId: 1, - userId: 2, - name: "Row 2", - description: "Row 2 description", - }) - - await config.api.row.bulkImport(table._id!, { - identifierFields: ["companyId", "userId"], - rows: [ - { - companyId: 1, - userId: row1.userId, - name: "Row 1 updated", - description: "Row 1 description updated", - }, - { - companyId: 1, - userId: row2.userId, - name: "Row 2 updated", - description: "Row 2 description updated", - }, - { - companyId: 1, - userId: 3, - name: "Row 3", - description: "Row 3 description", - }, - ], - }) - - const rows = await config.api.row.fetch(table._id!) - expect(rows.length).toEqual(3) - - rows.sort((a, b) => a.name.localeCompare(b.name)) - expect(rows[0].name).toEqual("Row 1 updated") - expect(rows[0].description).toEqual("Row 1 description updated") - expect(rows[1].name).toEqual("Row 2 updated") - expect(rows[1].description).toEqual("Row 2 description updated") - expect(rows[2].name).toEqual("Row 3") - expect(rows[2].description).toEqual("Row 3 description") - }) - - // Upserting isn't yet supported in MSSQL/Oracle, see: - // https://github.com/knex/knex/pull/6050 - !isMSSQL && - !isOracle && - !isInternal && - it("should be able to update existing rows an autoID primary key", async () => { - const tableName = uuid.v4() - await client!.schema.createTable(tableName, table => { - table.increments("userId").primary() - table.string("name") - }) - - const resp = await config.api.datasource.fetchSchema({ - datasourceId: datasource!._id!, - }) - const table = resp.datasource.entities![tableName] - - const row1 = await config.api.row.save(table._id!, { - name: "Clare", - }) - - const row2 = await config.api.row.save(table._id!, { - name: "Jeff", - }) - - await config.api.row.bulkImport(table._id!, { - identifierFields: ["userId"], - rows: [ - { - userId: row1.userId, - name: "Clare updated", - }, - { - userId: row2.userId, - name: "Jeff updated", - }, - ], - }) - - const rows = await config.api.row.fetch(table._id!) - expect(rows.length).toEqual(2) - - rows.sort((a, b) => a.name.localeCompare(b.name)) - expect(rows[0].name).toEqual("Clare updated") - expect(rows[1].name).toEqual("Jeff updated") - }) - }) - - describe("enrich", () => { - beforeAll(async () => { - table = await config.api.table.save(defaultTable()) - }) - - it("should allow enriching some linked rows", async () => { - const { linkedTable, firstRow, secondRow } = await tenancy.doInTenant( - config.getTenantId(), - async () => { - const linkedTable = await config.api.table.save( - defaultTable({ - schema: { - link: { - name: "link", - fieldName: "link", - type: FieldType.LINK, - relationshipType: RelationshipType.ONE_TO_MANY, - tableId: table._id!, - }, - }, - }) - ) - const firstRow = await config.api.row.save(table._id!, { - name: "Test Contact", - description: "original description", - }) - const secondRow = await config.api.row.save(linkedTable._id!, { - name: "Test 2", - description: "og desc", - link: [{ _id: firstRow._id }], - }) - return { linkedTable, firstRow, secondRow } - } - ) - const rowUsage = await getRowUsage() - - // test basic enrichment - const resBasic = await config.api.row.get( - linkedTable._id!, - secondRow._id! - ) - expect(resBasic.link.length).toBe(1) - expect(resBasic.link[0]).toEqual({ - _id: firstRow._id, - primaryDisplay: firstRow.name, - }) - - // test full enrichment - const resEnriched = await config.api.row.getEnriched( - linkedTable._id!, - secondRow._id! - ) - expect(resEnriched.link.length).toBe(1) - expect(resEnriched.link[0]._id).toBe(firstRow._id) - expect(resEnriched.link[0].name).toBe("Test Contact") - expect(resEnriched.link[0].description).toBe("original description") - await assertRowUsage(rowUsage) - }) - }) - - isInternal && - describe("attachments and signatures", () => { - const coreAttachmentEnrichment = async ( - schema: TableSchema, - field: string, - attachmentCfg: string | string[] - ) => { - const testTable = await config.api.table.save( - defaultTable({ - schema, - }) - ) - const attachmentToStoreKey = (attachmentId: string) => { - return { - key: `${config.getAppId()}/attachments/${attachmentId}`, - } - } - const draftRow = { - name: "test", - description: "test", - [field]: - typeof attachmentCfg === "string" - ? attachmentToStoreKey(attachmentCfg) - : attachmentCfg.map(attachmentToStoreKey), - tableId: testTable._id, - } - const row = await config.api.row.save(testTable._id!, draftRow) - - await withEnv({ SELF_HOSTED: "true" }, async () => { - return context.doInAppContext(config.getAppId(), async () => { - const enriched: Row[] = await outputProcessing(testTable, [row]) - const [targetRow] = enriched - const attachmentEntries = Array.isArray(targetRow[field]) - ? targetRow[field] - : [targetRow[field]] - - for (const entry of attachmentEntries) { - const attachmentId = entry.key.split("/").pop() - expect(entry.url.split("?")[0]).toBe( - `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}` + describe("options column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + status: { + name: "status", + type: FieldType.OPTIONS, + default: "requested", + constraints: { + inclusion: ["requested", "approved"], + }, + }, + }, + }) ) + }) + + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.status).toEqual("requested") + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + status: "approved", + }) + expect(row.status).toEqual("approved") + }) + }) + + describe("array column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + food: { + name: "food", + type: FieldType.ARRAY, + default: ["apple", "orange"], + constraints: { + type: JsonFieldSubType.ARRAY, + inclusion: ["apple", "orange", "banana"], + }, + }, + }, + }) + ) + }) + + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.food).toEqual(["apple", "orange"]) + }) + + it("creates a new row with a default value when given an empty list", async () => { + const row = await config.api.row.save(table._id!, { food: [] }) + expect(row.food).toEqual(["apple", "orange"]) + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + food: ["orange"], + }) + expect(row.food).toEqual(["orange"]) + }) + + it("resets back to its default value when empty", async () => { + let row = await config.api.row.save(table._id!, { + food: ["orange"], + }) + row = await config.api.row.save(table._id!, { ...row, food: [] }) + expect(row.food).toEqual(["apple", "orange"]) + }) + }) + + describe("user column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + default: "{{ [Current User]._id }}", + }, + }, + }) + ) + }) + + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.user._id).toEqual(config.getUser()._id) + }) + + it("does not use default value if value specified", async () => { + const id = `us_${utils.newid()}` + await config.createUser({ _id: id }) + const row = await config.api.row.save(table._id!, { + user: id, + }) + expect(row.user._id).toEqual(id) + }) + }) + + describe("multi-user column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + users: { + name: "users", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + default: ["{{ [Current User]._id }}"], + }, + }, + }) + ) + }) + + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.users).toHaveLength(1) + expect(row.users[0]._id).toEqual(config.getUser()._id) + }) + + it("does not use default value if value specified", async () => { + const id = `us_${utils.newid()}` + await config.createUser({ _id: id }) + const row = await config.api.row.save(table._id!, { + users: [id], + }) + expect(row.users).toHaveLength(1) + expect(row.users[0]._id).toEqual(id) + }) + }) + + describe("boolean column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + active: { + name: "active", + type: FieldType.BOOLEAN, + default: "true", + }, + }, + }) + ) + }) + + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.active).toEqual(true) + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + active: false, + }) + expect(row.active).toEqual(false) + }) + }) + + describe("bigint column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + bigNumber: { + name: "bigNumber", + type: FieldType.BIGINT, + default: "1234567890", + }, + }, + }) + ) + }) + + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.bigNumber).toEqual("1234567890") + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + bigNumber: "9876543210", + }) + expect(row.bigNumber).toEqual("9876543210") + }) + }) + + describe("bindings", () => { + describe("string column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + description: { + name: "description", + type: FieldType.STRING, + default: `{{ date now "YYYY-MM-DDTHH:mm:ss" }}`, + }, + }, + }) + ) + }) + + it("can use bindings in default values", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.description).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ + ) + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + description: "specified description", + }) + expect(row.description).toEqual("specified description") + }) + + it("can bind the current user", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + user: { + name: "user", + type: FieldType.STRING, + default: `{{ [Current User]._id }}`, + }, + }, + }) + ) + const row = await config.api.row.save(table._id!, {}) + expect(row.user).toEqual(config.getUser()._id) + }) + + it("cannot access current user password", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + user: { + name: "user", + type: FieldType.STRING, + default: `{{ user.password }}`, + }, + }, + }) + ) + const row = await config.api.row.save(table._id!, {}) + // For some reason it's null for internal tables, and undefined for + // external. + expect(row.user == null).toBe(true) + }) + }) + + describe("number column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + age: { + name: "age", + type: FieldType.NUMBER, + default: `{{ sum 10 10 5 }}`, + }, + }, + }) + ) + }) + + it("can use bindings in default values", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.age).toEqual(25) + }) + + describe("invalid default value", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + age: { + name: "age", + type: FieldType.NUMBER, + default: `{{ capitalize "invalid" }}`, + }, + }, + }) + ) + }) + + it("throws an error when invalid default value", async () => { + await config.api.row.save( + table._id!, + {}, + { + status: 400, + body: { + message: + "Invalid default value for field 'age' - Invalid number value \"Invalid\"", + }, + } + ) + }) + }) + }) + }) + }) + + describe("relations to same table", () => { + let relatedRows: Row[] + + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + }, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }, + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) + }) + + it("can create rows with both relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related1: [ + { + _id: relatedRows[0]._id, + primaryDisplay: relatedRows[0].name, + }, + ], + related2: [ + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + ], + }) + ) + }) + + it("can create rows with no relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + }) + + expect(row.related1).toBeUndefined() + expect(row.related2).toBeUndefined() + }) + + it("can create rows with only one relationships field", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [], + related2: [relatedRows[1]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related2: [ + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + ], + }) + ) + expect(row.related1).toBeUndefined() + }) + }) + }) + + describe("get", () => { + it("reads an existing row successfully", async () => { + const existing = await config.api.row.save(table._id!, {}) + + const res = await config.api.row.get(table._id!, existing._id!) + + expect(res).toEqual({ + ...existing, + ...defaultRowFields, + }) + }) + + it("returns 404 when row does not exist", async () => { + const table = await config.api.table.save(defaultTable()) + await config.api.row.save(table._id!, {}) + await config.api.row.get(table._id!, "1234567", { + status: 404, + }) + }) + + isInternal && + it("can search row from user table", async () => { + const res = await config.api.row.get( + InternalTables.USER_METADATA, + config.userMetadataId! + ) + + expect(res).toEqual({ + ...config.getUser(), + _id: config.userMetadataId!, + _rev: expect.any(String), + roles: undefined, + roleId: "ADMIN", + tableId: InternalTables.USER_METADATA, + }) + }) + }) + + describe("fetch", () => { + it("fetches all rows for given tableId", async () => { + const table = await config.api.table.save(defaultTable()) + const rows = await Promise.all([ + config.api.row.save(table._id!, {}), + config.api.row.save(table._id!, {}), + ]) + + const res = await config.api.row.fetch(table._id!) + expect(res.map(r => r._id)).toEqual( + expect.arrayContaining(rows.map(r => r._id)) + ) + }) + + it("returns 404 when table does not exist", async () => { + await config.api.row.fetch("1234567", { status: 404 }) + }) + }) + + describe("update", () => { + it("updates an existing row successfully", async () => { + const existing = await config.api.row.save(table._id!, {}) + const rowUsage = await getRowUsage() + + const res = await config.api.row.save(table._id!, { + _id: existing._id, + _rev: existing._rev, + name: "Updated Name", + }) + + expect(res.name).toEqual("Updated Name") + await assertRowUsage(rowUsage) + }) + + !isInternal && + it("can update a row on an external table with a primary key", async () => { + const tableName = uuid.v4().substring(0, 10) + await client!.schema.createTable(tableName, table => { + table.increments("id").primary() + table.string("name") + }) + + const res = await config.api.datasource.fetchSchema({ + datasourceId: datasource!._id!, + }) + const table = res.datasource.entities![tableName] + + const row = await config.api.row.save(table._id!, { + id: 1, + name: "Row 1", + }) + + const updatedRow = await config.api.row.save(table._id!, { + _id: row._id!, + name: "Row 1 Updated", + }) + + expect(updatedRow.name).toEqual("Row 1 Updated") + + const rows = await config.api.row.fetch(table._id!) + expect(rows).toHaveLength(1) + }) + + describe("relations to same table", () => { + let relatedRows: Row[] + + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + }, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }, + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) + }) + + it("can edit rows with both relationships", async () => { + let row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + row = await config.api.row.save(table._id!, { + ...row, + related1: [relatedRows[0]._id!, relatedRows[1]._id!], + related2: [relatedRows[2]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related1: expect.arrayContaining([ + { + _id: relatedRows[0]._id, + primaryDisplay: relatedRows[0].name, + }, + { + _id: relatedRows[1]._id, + primaryDisplay: relatedRows[1].name, + }, + ]), + related2: [ + { + _id: relatedRows[2]._id, + primaryDisplay: relatedRows[2].name, + }, + ], + }) + ) + }) + + it("can drop existing relationship", async () => { + let row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + row = await config.api.row.save(table._id!, { + ...row, + related1: [], + related2: [relatedRows[2]._id!], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + related2: [ + { + _id: relatedRows[2]._id, + primaryDisplay: relatedRows[2].name, + }, + ], + }) + ) + expect(row.related1).toBeUndefined() + }) + + it("can drop both relationships", async () => { + let row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + row = await config.api.row.save(table._id!, { + ...row, + related1: [], + related2: [], + }) + + expect(row).toEqual( + expect.objectContaining({ + name: "test", + }) + ) + expect(row.related1).toBeUndefined() + expect(row.related2).toBeUndefined() + }) + }) + }) + + describe("patch", () => { + let otherTable: Table + + beforeAll(async () => { + table = await config.api.table.save(defaultTable()) + otherTable = await config.api.table.save( + defaultTable({ + schema: { + relationship: { + name: "relationship", + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: table._id!, + fieldName: "relationship", + }, + }, + }) + ) + }) + + it("should update only the fields that are supplied", async () => { + const existing = await config.api.row.save(table._id!, {}) + + const rowUsage = await getRowUsage() + + const row = await config.api.row.patch(table._id!, { + _id: existing._id!, + _rev: existing._rev!, + tableId: table._id!, + name: "Updated Name", + }) + + expect(row.name).toEqual("Updated Name") + expect(row.description).toEqual(existing.description) + + const savedRow = await config.api.row.get(table._id!, row._id!) + + expect(savedRow.description).toEqual(existing.description) + expect(savedRow.name).toEqual("Updated Name") + await assertRowUsage(rowUsage) + }) + + it("should update only the fields that are supplied and emit the correct oldRow", async () => { + let beforeRow = await config.api.row.save(table._id!, { + name: "test", + description: "test", + }) + const opts = { + name: "row:update", + matchFn: (event: UpdatedRowEventEmitter) => + event.row._id === beforeRow._id, + } + const event = await waitForEvent(opts, async () => { + await config.api.row.patch(table._id!, { + _id: beforeRow._id!, + _rev: beforeRow._rev!, + tableId: table._id!, + name: "Updated Name", + }) + }) + + expect(event.oldRow).toBeDefined() + expect(event.oldRow.name).toEqual("test") + expect(event.row.name).toEqual("Updated Name") + expect(event.oldRow.description).toEqual(beforeRow.description) + expect(event.row.description).toEqual(beforeRow.description) + }) + + it("should throw an error when given improper types", async () => { + const existing = await config.api.row.save(table._id!, {}) + const rowUsage = await getRowUsage() + + await config.api.row.patch( + table._id!, + { + _id: existing._id!, + _rev: existing._rev!, + tableId: table._id!, + name: 1, + }, + { status: 400 } + ) + + await assertRowUsage(rowUsage) + }) + + it("should not overwrite links if those links are not set", async () => { + let linkField: FieldSchema = { + type: FieldType.LINK, + name: "", + fieldName: "", + constraints: { + type: "array", + presence: false, + }, + relationshipType: RelationshipType.ONE_TO_MANY, + tableId: InternalTable.USER_METADATA, + } + + let table = await config.api.table.save({ + name: "TestTable", + type: "table", + sourceType: TableSourceType.INTERNAL, + sourceId: INTERNAL_TABLE_SOURCE_ID, + schema: { + user1: { ...linkField, name: "user1", fieldName: "user1" }, + user2: { ...linkField, name: "user2", fieldName: "user2" }, + }, + }) + + let user1 = await config.createUser() + let user2 = await config.createUser() + + let row = await config.api.row.save(table._id!, { + user1: [{ _id: user1._id }], + user2: [{ _id: user2._id }], + }) + + let getResp = await config.api.row.get(table._id!, row._id!) + expect(getResp.user1[0]._id).toEqual(user1._id) + expect(getResp.user2[0]._id).toEqual(user2._id) + + let patchResp = await config.api.row.patch(table._id!, { + _id: row._id!, + _rev: row._rev!, + tableId: table._id!, + user1: [{ _id: user2._id }], + }) + expect(patchResp.user1[0]._id).toEqual(user2._id) + expect(patchResp.user2[0]._id).toEqual(user2._id) + + getResp = await config.api.row.get(table._id!, row._id!) + expect(getResp.user1[0]._id).toEqual(user2._id) + expect(getResp.user2[0]._id).toEqual(user2._id) + }) + + it("should be able to remove a relationship from many side", async () => { + const row = await config.api.row.save(otherTable._id!, { + name: "test", + description: "test", + }) + const row2 = await config.api.row.save(otherTable._id!, { + name: "test", + description: "test", + }) + const { _id } = await config.api.row.save(table._id!, { + relationship: [{ _id: row._id }, { _id: row2._id }], + }) + const relatedRow = await config.api.row.get(table._id!, _id!, { + status: 200, + }) + expect(relatedRow.relationship.length).toEqual(2) + await config.api.row.save(table._id!, { + ...relatedRow, + relationship: [{ _id: row._id }], + }) + const afterRelatedRow = await config.api.row.get(table._id!, _id!, { + status: 200, + }) + expect(afterRelatedRow.relationship.length).toEqual(1) + expect(afterRelatedRow.relationship[0]._id).toEqual(row._id) + }) + + it("should be able to update relationships when both columns are same name", async () => { + let row = await config.api.row.save(table._id!, { + name: "test", + description: "test", + }) + let row2 = await config.api.row.save(otherTable._id!, { + name: "test", + description: "test", + relationship: [row._id], + }) + row = await config.api.row.get(table._id!, row._id!) + expect(row.relationship.length).toBe(1) + const resp = await config.api.row.patch(table._id!, { + _id: row._id!, + _rev: row._rev!, + tableId: row.tableId!, + name: "test2", + relationship: [row2._id], + }) + expect(resp.relationship.length).toBe(1) + }) + + !isInternal && + // MSSQL needs a setting called IDENTITY_INSERT to be set to ON to allow writing + // to identity columns. This is not something Budibase does currently. + !isMSSQL && + it("should support updating fields that are part of a composite key", async () => { + const tableRequest = saveTableRequest({ + primary: ["number", "string"], + schema: { + string: { + type: FieldType.STRING, + name: "string", + }, + number: { + type: FieldType.NUMBER, + name: "number", + }, + }, + }) + + delete tableRequest.schema.id + + const table = await config.api.table.save(tableRequest) + + const stringValue = generator.word() + + // MySQL and MariaDB auto-increment fields have a minimum value of 1. If + // you try to save a row with a value of 0 it will use 1 instead. + const naturalValue = generator.integer({ min: 1, max: 1000 }) + + const existing = await config.api.row.save(table._id!, { + string: stringValue, + number: naturalValue, + }) + + expect(existing._id).toEqual( + `%5B${naturalValue}%2C'${stringValue}'%5D` + ) + + const row = await config.api.row.patch(table._id!, { + _id: existing._id!, + _rev: existing._rev!, + tableId: table._id!, + string: stringValue, + number: 1500, + }) + + expect(row._id).toEqual(`%5B${"1500"}%2C'${stringValue}'%5D`) + }) + }) + + describe("destroy", () => { + beforeAll(async () => { + table = await config.api.table.save(defaultTable()) + }) + + it("should be able to delete a row", async () => { + const createdRow = await config.api.row.save(table._id!, {}) + const rowUsage = await getRowUsage() + + const res = await config.api.row.bulkDelete(table._id!, { + rows: [createdRow], + }) + expect(res[0]._id).toEqual(createdRow._id) + await assertRowUsage(isInternal ? rowUsage - 1 : rowUsage) + }) + + it("should be able to delete a row with ID only", async () => { + const createdRow = await config.api.row.save(table._id!, {}) + const rowUsage = await getRowUsage() + + const res = await config.api.row.bulkDelete(table._id!, { + rows: [createdRow._id!], + }) + expect(res[0]._id).toEqual(createdRow._id) + expect(res[0].tableId).toEqual(table._id!) + await assertRowUsage(isInternal ? rowUsage - 1 : rowUsage) + }) + + it("should be able to bulk delete rows, including a row that doesn't exist", async () => { + const createdRow = await config.api.row.save(table._id!, {}) + const createdRow2 = await config.api.row.save(table._id!, {}) + + const res = await config.api.row.bulkDelete(table._id!, { + rows: [createdRow, createdRow2, { _id: "9999999" }], + }) + + expect(res.map(r => r._id)).toEqual( + expect.arrayContaining([createdRow._id, createdRow2._id]) + ) + expect(res.length).toEqual(2) + }) + + describe("relations to same table", () => { + let relatedRows: Row[] + + beforeAll(async () => { + const relatedTable = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + }, + }) + ) + const relatedTableId = relatedTable._id! + table = await config.api.table.save( + defaultTable({ + schema: { + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTableId, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }, + }) + ) + relatedRows = await Promise.all([ + config.api.row.save(relatedTableId, { name: "foo" }), + config.api.row.save(relatedTableId, { name: "bar" }), + config.api.row.save(relatedTableId, { name: "baz" }), + config.api.row.save(relatedTableId, { name: "boo" }), + ]) + }) + + it("can delete rows with both relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }) + + await config.api.row.delete(table._id!, { _id: row._id! }) + + await config.api.row.get(table._id!, row._id!, { status: 404 }) + }) + + it("can delete rows with empty relationships", async () => { + const row = await config.api.row.save(table._id!, { + name: "test", + related1: [], + related2: [], + }) + + await config.api.row.delete(table._id!, { _id: row._id! }) + + await config.api.row.get(table._id!, row._id!, { status: 404 }) + }) + }) + }) + + describe("validate", () => { + beforeAll(async () => { + table = await config.api.table.save(defaultTable()) + }) + + it("should return no errors on valid row", async () => { + const rowUsage = await getRowUsage() + + const res = await config.api.row.validate(table._id!, { + name: "ivan", + }) + + expect(res.valid).toBe(true) + expect(Object.keys(res.errors)).toEqual([]) + await assertRowUsage(rowUsage) + }) + + it("should errors on invalid row", async () => { + const rowUsage = await getRowUsage() + + const res = await config.api.row.validate(table._id!, { name: 1 }) + + if (isInternal) { + expect(res.valid).toBe(false) + expect(Object.keys(res.errors)).toEqual(["name"]) + } else { + // Validation for external is not implemented, so it will always return valid + expect(res.valid).toBe(true) + expect(Object.keys(res.errors)).toEqual([]) + } + await assertRowUsage(rowUsage) + }) + }) + + describe("bulkDelete", () => { + beforeAll(async () => { + table = await config.api.table.save(defaultTable()) + }) + + it("should be able to delete a bulk set of rows", async () => { + const row1 = await config.api.row.save(table._id!, {}) + const row2 = await config.api.row.save(table._id!, {}) + const rowUsage = await getRowUsage() + + const res = await config.api.row.bulkDelete(table._id!, { + rows: [row1, row2], + }) + + expect(res.length).toEqual(2) + await config.api.row.get(table._id!, row1._id!, { status: 404 }) + await assertRowUsage(isInternal ? rowUsage - 2 : rowUsage) + }) + + it("should be able to delete a variety of row set types", async () => { + const [row1, row2, row3] = await Promise.all([ + config.api.row.save(table._id!, {}), + config.api.row.save(table._id!, {}), + config.api.row.save(table._id!, {}), + ]) + const rowUsage = await getRowUsage() + + const res = await config.api.row.bulkDelete(table._id!, { + rows: [row1, row2._id!, { _id: row3._id }], + }) + + expect(res.length).toEqual(3) + await config.api.row.get(table._id!, row1._id!, { status: 404 }) + await assertRowUsage(isInternal ? rowUsage - 3 : rowUsage) + }) + + it("should accept a valid row object and delete the row", async () => { + const row1 = await config.api.row.save(table._id!, {}) + const rowUsage = await getRowUsage() + + const res = await config.api.row.delete(table._id!, row1 as DeleteRow) + + expect(res.id).toEqual(row1._id) + await config.api.row.get(table._id!, row1._id!, { status: 404 }) + await assertRowUsage(isInternal ? rowUsage - 1 : rowUsage) + }) + + it.each([{ not: "valid" }, { rows: 123 }, "invalid"])( + "should ignore malformed/invalid delete request: %s", + async (request: any) => { + const rowUsage = await getRowUsage() + + await config.api.row.delete(table._id!, request, { + status: 400, + body: { + message: "Invalid delete rows request", + }, + }) + + await assertRowUsage(rowUsage) + } + ) + }) + + describe("bulkImport", () => { + isInternal && + it("should update Auto ID field after bulk import", async () => { + const table = await config.api.table.save( + saveTableRequest({ + primary: ["autoId"], + schema: { + autoId: { + name: "autoId", + type: FieldType.NUMBER, + subtype: AutoFieldSubType.AUTO_ID, + autocolumn: true, + constraints: { + type: "number", + presence: false, + }, + }, + }, + }) + ) + + let row = await config.api.row.save(table._id!, {}) + expect(row.autoId).toEqual(1) + + await config.api.row.bulkImport(table._id!, { + rows: [{ autoId: 2 }], + }) + + row = await config.api.row.save(table._id!, {}) + expect(row.autoId).toEqual(3) + }) + + isInternal && + it("should reject bulkImporting relationship fields", async () => { + const table1 = await config.api.table.save(saveTableRequest()) + const table2 = await config.api.table.save( + saveTableRequest({ + schema: { + relationship: { + name: "relationship", + type: FieldType.LINK, + tableId: table1._id!, + relationshipType: RelationshipType.ONE_TO_MANY, + fieldName: "relationship", + }, + }, + }) + ) + + const table1Row1 = await config.api.row.save(table1._id!, {}) + await config.api.row.bulkImport( + table2._id!, + { + rows: [{ relationship: [table1Row1._id!] }], + }, + { + status: 400, + body: { + message: + 'Can\'t bulk import relationship fields for internal databases, found value in field "relationship"', + }, + } + ) + }) + + it("should be able to bulkImport rows", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + type: FieldType.STRING, + name: "name", + }, + description: { + type: FieldType.STRING, + name: "description", + }, + }, + }) + ) + + const rowUsage = await getRowUsage() + + await config.api.row.bulkImport(table._id!, { + rows: [ + { + name: "Row 1", + description: "Row 1 description", + }, + { + name: "Row 2", + description: "Row 2 description", + }, + ], + }) + + const rows = await config.api.row.fetch(table._id!) + expect(rows.length).toEqual(2) + + rows.sort((a, b) => a.name.localeCompare(b.name)) + expect(rows[0].name).toEqual("Row 1") + expect(rows[0].description).toEqual("Row 1 description") + expect(rows[1].name).toEqual("Row 2") + expect(rows[1].description).toEqual("Row 2 description") + + await assertRowUsage(isInternal ? rowUsage + 2 : rowUsage) + }) + + isInternal && + it("should be able to update existing rows on bulkImport", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + type: FieldType.STRING, + name: "name", + }, + description: { + type: FieldType.STRING, + name: "description", + }, + }, + }) + ) + + const existingRow = await config.api.row.save(table._id!, { + name: "Existing row", + description: "Existing description", + }) + + const rowUsage = await getRowUsage() + + await config.api.row.bulkImport(table._id!, { + rows: [ + { + name: "Row 1", + description: "Row 1 description", + }, + { ...existingRow, name: "Updated existing row" }, + { + name: "Row 2", + description: "Row 2 description", + }, + ], + identifierFields: ["_id"], + }) + + const rows = await config.api.row.fetch(table._id!) + expect(rows.length).toEqual(3) + + rows.sort((a, b) => a.name.localeCompare(b.name)) + expect(rows[0].name).toEqual("Row 1") + expect(rows[0].description).toEqual("Row 1 description") + expect(rows[1].name).toEqual("Row 2") + expect(rows[1].description).toEqual("Row 2 description") + expect(rows[2].name).toEqual("Updated existing row") + expect(rows[2].description).toEqual("Existing description") + + await assertRowUsage(rowUsage + 2) + }) + + isInternal && + it("should create new rows if not identifierFields are provided", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + type: FieldType.STRING, + name: "name", + }, + description: { + type: FieldType.STRING, + name: "description", + }, + }, + }) + ) + + const existingRow = await config.api.row.save(table._id!, { + name: "Existing row", + description: "Existing description", + }) + + const rowUsage = await getRowUsage() + + await config.api.row.bulkImport(table._id!, { + rows: [ + { + name: "Row 1", + description: "Row 1 description", + }, + { ...existingRow, name: "Updated existing row" }, + { + name: "Row 2", + description: "Row 2 description", + }, + ], + }) + + const rows = await config.api.row.fetch(table._id!) + expect(rows.length).toEqual(4) + + rows.sort((a, b) => a.name.localeCompare(b.name)) + expect(rows[0].name).toEqual("Existing row") + expect(rows[0].description).toEqual("Existing description") + expect(rows[1].name).toEqual("Row 1") + expect(rows[1].description).toEqual("Row 1 description") + expect(rows[2].name).toEqual("Row 2") + expect(rows[2].description).toEqual("Row 2 description") + expect(rows[3].name).toEqual("Updated existing row") + expect(rows[3].description).toEqual("Existing description") + + await assertRowUsage(rowUsage + 3) + }) + + // Upserting isn't yet supported in MSSQL / Oracle, see: + // https://github.com/knex/knex/pull/6050 + !isMSSQL && + !isOracle && + it("should be able to update existing rows with bulkImport", async () => { + const table = await config.api.table.save( + saveTableRequest({ + primary: ["userId"], + schema: { + userId: { + type: FieldType.NUMBER, + name: "userId", + constraints: { + presence: true, + }, + }, + name: { + type: FieldType.STRING, + name: "name", + }, + description: { + type: FieldType.STRING, + name: "description", + }, + }, + }) + ) + + const row1 = await config.api.row.save(table._id!, { + userId: 1, + name: "Row 1", + description: "Row 1 description", + }) + + const row2 = await config.api.row.save(table._id!, { + userId: 2, + name: "Row 2", + description: "Row 2 description", + }) + + await config.api.row.bulkImport(table._id!, { + identifierFields: ["userId"], + rows: [ + { + userId: row1.userId, + name: "Row 1 updated", + description: "Row 1 description updated", + }, + { + userId: row2.userId, + name: "Row 2 updated", + description: "Row 2 description updated", + }, + { + userId: 3, + name: "Row 3", + description: "Row 3 description", + }, + ], + }) + + const rows = await config.api.row.fetch(table._id!) + expect(rows.length).toEqual(3) + + rows.sort((a, b) => a.name.localeCompare(b.name)) + expect(rows[0].name).toEqual("Row 1 updated") + expect(rows[0].description).toEqual("Row 1 description updated") + expect(rows[1].name).toEqual("Row 2 updated") + expect(rows[1].description).toEqual("Row 2 description updated") + expect(rows[2].name).toEqual("Row 3") + expect(rows[2].description).toEqual("Row 3 description") + }) + + // Upserting isn't yet supported in MSSQL or Oracle, see: + // https://github.com/knex/knex/pull/6050 + !isMSSQL && + !isOracle && + !isInternal && + it("should be able to update existing rows with composite primary keys with bulkImport", async () => { + const tableName = uuid.v4() + await client?.schema.createTable(tableName, table => { + table.integer("companyId") + table.integer("userId") + table.string("name") + table.string("description") + table.primary(["companyId", "userId"]) + }) + + const resp = await config.api.datasource.fetchSchema({ + datasourceId: datasource!._id!, + }) + const table = resp.datasource.entities![tableName] + + const row1 = await config.api.row.save(table._id!, { + companyId: 1, + userId: 1, + name: "Row 1", + description: "Row 1 description", + }) + + const row2 = await config.api.row.save(table._id!, { + companyId: 1, + userId: 2, + name: "Row 2", + description: "Row 2 description", + }) + + await config.api.row.bulkImport(table._id!, { + identifierFields: ["companyId", "userId"], + rows: [ + { + companyId: 1, + userId: row1.userId, + name: "Row 1 updated", + description: "Row 1 description updated", + }, + { + companyId: 1, + userId: row2.userId, + name: "Row 2 updated", + description: "Row 2 description updated", + }, + { + companyId: 1, + userId: 3, + name: "Row 3", + description: "Row 3 description", + }, + ], + }) + + const rows = await config.api.row.fetch(table._id!) + expect(rows.length).toEqual(3) + + rows.sort((a, b) => a.name.localeCompare(b.name)) + expect(rows[0].name).toEqual("Row 1 updated") + expect(rows[0].description).toEqual("Row 1 description updated") + expect(rows[1].name).toEqual("Row 2 updated") + expect(rows[1].description).toEqual("Row 2 description updated") + expect(rows[2].name).toEqual("Row 3") + expect(rows[2].description).toEqual("Row 3 description") + }) + + // Upserting isn't yet supported in MSSQL/Oracle, see: + // https://github.com/knex/knex/pull/6050 + !isMSSQL && + !isOracle && + !isInternal && + it("should be able to update existing rows an autoID primary key", async () => { + const tableName = uuid.v4() + await client!.schema.createTable(tableName, table => { + table.increments("userId").primary() + table.string("name") + }) + + const resp = await config.api.datasource.fetchSchema({ + datasourceId: datasource!._id!, + }) + const table = resp.datasource.entities![tableName] + + const row1 = await config.api.row.save(table._id!, { + name: "Clare", + }) + + const row2 = await config.api.row.save(table._id!, { + name: "Jeff", + }) + + await config.api.row.bulkImport(table._id!, { + identifierFields: ["userId"], + rows: [ + { + userId: row1.userId, + name: "Clare updated", + }, + { + userId: row2.userId, + name: "Jeff updated", + }, + ], + }) + + const rows = await config.api.row.fetch(table._id!) + expect(rows.length).toEqual(2) + + rows.sort((a, b) => a.name.localeCompare(b.name)) + expect(rows[0].name).toEqual("Clare updated") + expect(rows[1].name).toEqual("Jeff updated") + }) + }) + + describe("enrich", () => { + beforeAll(async () => { + table = await config.api.table.save(defaultTable()) + }) + + it("should allow enriching some linked rows", async () => { + const { linkedTable, firstRow, secondRow } = await tenancy.doInTenant( + config.getTenantId(), + async () => { + const linkedTable = await config.api.table.save( + defaultTable({ + schema: { + link: { + name: "link", + fieldName: "link", + type: FieldType.LINK, + relationshipType: RelationshipType.ONE_TO_MANY, + tableId: table._id!, + }, + }, + }) + ) + const firstRow = await config.api.row.save(table._id!, { + name: "Test Contact", + description: "original description", + }) + const secondRow = await config.api.row.save(linkedTable._id!, { + name: "Test 2", + description: "og desc", + link: [{ _id: firstRow._id }], + }) + return { linkedTable, firstRow, secondRow } + } + ) + const rowUsage = await getRowUsage() + + // test basic enrichment + const resBasic = await config.api.row.get( + linkedTable._id!, + secondRow._id! + ) + expect(resBasic.link.length).toBe(1) + expect(resBasic.link[0]).toEqual({ + _id: firstRow._id, + primaryDisplay: firstRow.name, + }) + + // test full enrichment + const resEnriched = await config.api.row.getEnriched( + linkedTable._id!, + secondRow._id! + ) + expect(resEnriched.link.length).toBe(1) + expect(resEnriched.link[0]._id).toBe(firstRow._id) + expect(resEnriched.link[0].name).toBe("Test Contact") + expect(resEnriched.link[0].description).toBe("original description") + await assertRowUsage(rowUsage) + }) + }) + + isInternal && + describe("attachments and signatures", () => { + const coreAttachmentEnrichment = async ( + schema: TableSchema, + field: string, + attachmentCfg: string | string[] + ) => { + const testTable = await config.api.table.save( + defaultTable({ + schema, + }) + ) + const attachmentToStoreKey = (attachmentId: string) => { + return { + key: `${config.getAppId()}/attachments/${attachmentId}`, + } + } + const draftRow = { + name: "test", + description: "test", + [field]: + typeof attachmentCfg === "string" + ? attachmentToStoreKey(attachmentCfg) + : attachmentCfg.map(attachmentToStoreKey), + tableId: testTable._id, + } + const row = await config.api.row.save(testTable._id!, draftRow) + + await withEnv({ SELF_HOSTED: "true" }, async () => { + return context.doInAppContext(config.getAppId(), async () => { + const enriched: Row[] = await outputProcessing(testTable, [row]) + const [targetRow] = enriched + const attachmentEntries = Array.isArray(targetRow[field]) + ? targetRow[field] + : [targetRow[field]] + + for (const entry of attachmentEntries) { + const attachmentId = entry.key.split("/").pop() + expect(entry.url.split("?")[0]).toBe( + `/files/signed/prod-budi-app-assets/${config.getProdAppId()}/attachments/${attachmentId}` + ) + } + }) + }) + } + + it("should allow enriching single attachment rows", async () => { + await coreAttachmentEnrichment( + { + attachment: { + type: FieldType.ATTACHMENT_SINGLE, + name: "attachment", + constraints: { presence: false }, + }, + }, + "attachment", + `${uuid.v4()}.csv` + ) + }) + + it("should allow enriching attachment list rows", async () => { + await coreAttachmentEnrichment( + { + attachments: { + type: FieldType.ATTACHMENTS, + name: "attachments", + constraints: { type: "array", presence: false }, + }, + }, + "attachments", + [`${uuid.v4()}.csv`] + ) + }) + + it("should allow enriching signature rows", async () => { + await coreAttachmentEnrichment( + { + signature: { + type: FieldType.SIGNATURE_SINGLE, + name: "signature", + constraints: { presence: false }, + }, + }, + "signature", + `${uuid.v4()}.png` + ) + }) + }) + + describe("exportRows", () => { + beforeEach(async () => { + table = await config.api.table.save(defaultTable()) + }) + + isInternal && + it("should not export internal couchdb fields", async () => { + const existing = await config.api.row.save(table._id!, { + name: generator.guid(), + description: generator.paragraph(), + }) + const res = await config.api.row.exportRows(table._id!, { + rows: [existing._id!], + }) + const results = JSON.parse(res) + expect(results.length).toEqual(1) + const row = results[0] + + expect(Object.keys(row)).toEqual(["_id", "name", "description"]) + }) + + !isInternal && + it("should allow exporting all columns", async () => { + const existing = await config.api.row.save(table._id!, {}) + const res = await config.api.row.exportRows(table._id!, { + rows: [existing._id!], + }) + const results = JSON.parse(res) + expect(results.length).toEqual(1) + const row = results[0] + + // Ensure all original columns were exported + expect(Object.keys(row).length).toBe(Object.keys(existing).length) + Object.keys(existing).forEach(key => { + expect(row[key]).toEqual(existing[key]) + }) + }) + + it("should allow exporting without filtering", async () => { + const existing = await config.api.row.save(table._id!, {}) + const res = await config.api.row.exportRows(table._id!) + const results = JSON.parse(res) + expect(results.length).toEqual(1) + const row = results[0] + + expect(row._id).toEqual(existing._id) + }) + + it("should allow exporting only certain columns", async () => { + const existing = await config.api.row.save(table._id!, {}) + const res = await config.api.row.exportRows(table._id!, { + rows: [existing._id!], + columns: ["_id"], + }) + const results = JSON.parse(res) + expect(results.length).toEqual(1) + const row = results[0] + + // Ensure only the _id column was exported + expect(Object.keys(row).length).toEqual(1) + expect(row._id).toEqual(existing._id) + }) + + it("should handle single quotes in row filtering", async () => { + const existing = await config.api.row.save(table._id!, {}) + const res = await config.api.row.exportRows(table._id!, { + rows: [`['${existing._id!}']`], + }) + const results = JSON.parse(res) + expect(results.length).toEqual(1) + const row = results[0] + expect(row._id).toEqual(existing._id) + }) + + it("should return an error if no table is found", async () => { + const existing = await config.api.row.save(table._id!, {}) + await config.api.row.exportRows( + "1234567", + { rows: [existing._id!] }, + RowExportFormat.JSON, + { status: 404 } + ) + }) + + // MSSQL needs a setting called IDENTITY_INSERT to be set to ON to allow writing + // to identity columns. This is not something Budibase does currently. + !isMSSQL && + it("should handle filtering by composite primary keys", async () => { + const tableRequest = saveTableRequest({ + primary: ["number", "string"], + schema: { + string: { + type: FieldType.STRING, + name: "string", + }, + number: { + type: FieldType.NUMBER, + name: "number", + }, + }, + }) + delete tableRequest.schema.id + + const table = await config.api.table.save(tableRequest) + const toCreate = generator + .unique(() => generator.integer({ min: 0, max: 10000 }), 10) + .map(number => ({ + number, + string: generator.word({ length: 30 }), + })) + + const rows = await Promise.all( + toCreate.map(d => config.api.row.save(table._id!, d)) + ) + + const res = await config.api.row.exportRows(table._id!, { + rows: _.sampleSize(rows, 3).map(r => r._id!), + }) + const results = JSON.parse(res) + expect(results.length).toEqual(3) + }) + + describe("should allow exporting all column types", () => { + let tableId: string + let expectedRowData: Row + + beforeAll(async () => { + const fullSchema = setup.structures.fullSchemaWithoutLinks({ + allRequired: true, + }) + + const table = await config.api.table.save( + saveTableRequest({ + ...setup.structures.basicTable(), + schema: fullSchema, + primary: ["string"], + }) + ) + tableId = table._id! + + const rowValues: Record = { + [FieldType.STRING]: generator.guid(), + [FieldType.LONGFORM]: generator.paragraph(), + [FieldType.OPTIONS]: "option 2", + [FieldType.ARRAY]: ["options 2", "options 4"], + [FieldType.NUMBER]: generator.natural(), + [FieldType.BOOLEAN]: generator.bool(), + [FieldType.DATETIME]: generator.date().toISOString(), + [FieldType.ATTACHMENTS]: [setup.structures.basicAttachment()], + [FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(), + [FieldType.FORMULA]: undefined, // generated field + [FieldType.AUTO]: undefined, // generated field + [FieldType.AI]: "LLM Output", + [FieldType.JSON]: { name: generator.guid() }, + [FieldType.INTERNAL]: generator.guid(), + [FieldType.BARCODEQR]: generator.guid(), + [FieldType.SIGNATURE_SINGLE]: setup.structures.basicAttachment(), + [FieldType.BIGINT]: generator.integer().toString(), + [FieldType.BB_REFERENCE]: [{ _id: config.getUser()._id }], + [FieldType.BB_REFERENCE_SINGLE]: { _id: config.getUser()._id }, + } + const row = await config.api.row.save(table._id!, rowValues) + expectedRowData = { + _id: row._id, + [FieldType.STRING]: rowValues[FieldType.STRING], + [FieldType.LONGFORM]: rowValues[FieldType.LONGFORM], + [FieldType.OPTIONS]: rowValues[FieldType.OPTIONS], + [FieldType.ARRAY]: rowValues[FieldType.ARRAY], + [FieldType.NUMBER]: rowValues[FieldType.NUMBER], + [FieldType.BOOLEAN]: rowValues[FieldType.BOOLEAN], + [FieldType.DATETIME]: rowValues[FieldType.DATETIME], + [FieldType.ATTACHMENTS]: rowValues[FieldType.ATTACHMENTS].map( + (a: any) => + expect.objectContaining({ + ...a, + url: expect.any(String), + }) + ), + [FieldType.ATTACHMENT_SINGLE]: expect.objectContaining({ + ...rowValues[FieldType.ATTACHMENT_SINGLE], + url: expect.any(String), + }), + [FieldType.FORMULA]: fullSchema[FieldType.FORMULA].formula, + [FieldType.AUTO]: expect.any(Number), + [FieldType.AI]: expect.any(String), + [FieldType.JSON]: rowValues[FieldType.JSON], + [FieldType.INTERNAL]: rowValues[FieldType.INTERNAL], + [FieldType.BARCODEQR]: rowValues[FieldType.BARCODEQR], + [FieldType.SIGNATURE_SINGLE]: expect.objectContaining({ + ...rowValues[FieldType.SIGNATURE_SINGLE], + url: expect.any(String), + }), + [FieldType.BIGINT]: rowValues[FieldType.BIGINT], + [FieldType.BB_REFERENCE]: rowValues[FieldType.BB_REFERENCE].map( + expect.objectContaining + ), + [FieldType.BB_REFERENCE_SINGLE]: expect.objectContaining( + rowValues[FieldType.BB_REFERENCE_SINGLE] + ), } }) - }) - } - it("should allow enriching single attachment rows", async () => { - await coreAttachmentEnrichment( - { - attachment: { - type: FieldType.ATTACHMENT_SINGLE, - name: "attachment", - constraints: { presence: false }, - }, - }, - "attachment", - `${uuid.v4()}.csv` - ) - }) + it("as csv", async () => { + const exportedValue = await config.api.row.exportRows( + tableId, + { query: {} }, + RowExportFormat.CSV + ) - it("should allow enriching attachment list rows", async () => { - await coreAttachmentEnrichment( - { - attachments: { - type: FieldType.ATTACHMENTS, - name: "attachments", - constraints: { type: "array", presence: false }, - }, - }, - "attachments", - [`${uuid.v4()}.csv`] - ) - }) + const jsonResult = await config.api.table.csvToJson({ + csvString: exportedValue, + }) - it("should allow enriching signature rows", async () => { - await coreAttachmentEnrichment( - { - signature: { - type: FieldType.SIGNATURE_SINGLE, - name: "signature", - constraints: { presence: false }, - }, - }, - "signature", - `${uuid.v4()}.png` - ) - }) - }) + const stringified = (value: string) => + JSON.stringify(value).replace(/"/g, "'") - describe("exportRows", () => { - beforeEach(async () => { - table = await config.api.table.save(defaultTable()) - }) + const matchingObject = ( + key: string, + value: any, + isArray: boolean + ) => { + const objectMatcher = `{'${key}':'${value[key]}'.*?}` + if (isArray) { + return expect.stringMatching( + new RegExp(`^\\[${objectMatcher}\\]$`) + ) + } + return expect.stringMatching(new RegExp(`^${objectMatcher}$`)) + } - isInternal && - it("should not export internal couchdb fields", async () => { - const existing = await config.api.row.save(table._id!, { - name: generator.guid(), - description: generator.paragraph(), - }) - const res = await config.api.row.exportRows(table._id!, { - rows: [existing._id!], - }) - const results = JSON.parse(res) - expect(results.length).toEqual(1) - const row = results[0] - - expect(Object.keys(row)).toEqual(["_id", "name", "description"]) - }) - - !isInternal && - it("should allow exporting all columns", async () => { - const existing = await config.api.row.save(table._id!, {}) - const res = await config.api.row.exportRows(table._id!, { - rows: [existing._id!], - }) - const results = JSON.parse(res) - expect(results.length).toEqual(1) - const row = results[0] - - // Ensure all original columns were exported - expect(Object.keys(row).length).toBe(Object.keys(existing).length) - Object.keys(existing).forEach(key => { - expect(row[key]).toEqual(existing[key]) - }) - }) - - it("should allow exporting without filtering", async () => { - const existing = await config.api.row.save(table._id!, {}) - const res = await config.api.row.exportRows(table._id!) - const results = JSON.parse(res) - expect(results.length).toEqual(1) - const row = results[0] - - expect(row._id).toEqual(existing._id) - }) - - it("should allow exporting only certain columns", async () => { - const existing = await config.api.row.save(table._id!, {}) - const res = await config.api.row.exportRows(table._id!, { - rows: [existing._id!], - columns: ["_id"], - }) - const results = JSON.parse(res) - expect(results.length).toEqual(1) - const row = results[0] - - // Ensure only the _id column was exported - expect(Object.keys(row).length).toEqual(1) - expect(row._id).toEqual(existing._id) - }) - - it("should handle single quotes in row filtering", async () => { - const existing = await config.api.row.save(table._id!, {}) - const res = await config.api.row.exportRows(table._id!, { - rows: [`['${existing._id!}']`], - }) - const results = JSON.parse(res) - expect(results.length).toEqual(1) - const row = results[0] - expect(row._id).toEqual(existing._id) - }) - - it("should return an error if no table is found", async () => { - const existing = await config.api.row.save(table._id!, {}) - await config.api.row.exportRows( - "1234567", - { rows: [existing._id!] }, - RowExportFormat.JSON, - { status: 404 } - ) - }) - - // MSSQL needs a setting called IDENTITY_INSERT to be set to ON to allow writing - // to identity columns. This is not something Budibase does currently. - providerType !== DatabaseName.SQL_SERVER && - it("should handle filtering by composite primary keys", async () => { - const tableRequest = saveTableRequest({ - primary: ["number", "string"], - schema: { - string: { - type: FieldType.STRING, - name: "string", - }, - number: { - type: FieldType.NUMBER, - name: "number", - }, - }, - }) - delete tableRequest.schema.id - - const table = await config.api.table.save(tableRequest) - const toCreate = generator - .unique(() => generator.integer({ min: 0, max: 10000 }), 10) - .map(number => ({ number, string: generator.word({ length: 30 }) })) - - const rows = await Promise.all( - toCreate.map(d => config.api.row.save(table._id!, d)) - ) - - const res = await config.api.row.exportRows(table._id!, { - rows: _.sampleSize(rows, 3).map(r => r._id!), - }) - const results = JSON.parse(res) - expect(results.length).toEqual(3) - }) - - describe("should allow exporting all column types", () => { - let tableId: string - let expectedRowData: Row - - beforeAll(async () => { - const fullSchema = setup.structures.fullSchemaWithoutLinks({ - allRequired: true, - }) - - const table = await config.api.table.save( - saveTableRequest({ - ...setup.structures.basicTable(), - schema: fullSchema, - primary: ["string"], + expect(jsonResult).toEqual([ + { + ...expectedRowData, + auto: expect.any(String), + array: stringified(expectedRowData["array"]), + attachment: matchingObject( + "key", + expectedRowData["attachment"][0].sample, + true + ), + attachment_single: matchingObject( + "key", + expectedRowData["attachment_single"].sample, + false + ), + boolean: stringified(expectedRowData["boolean"]), + json: stringified(expectedRowData["json"]), + number: stringified(expectedRowData["number"]), + signature_single: matchingObject( + "key", + expectedRowData["signature_single"].sample, + false + ), + bb_reference: matchingObject( + "_id", + expectedRowData["bb_reference"][0].sample, + true + ), + bb_reference_single: matchingObject( + "_id", + expectedRowData["bb_reference_single"].sample, + false + ), + ai: "LLM Output", + }, + ]) }) - ) - tableId = table._id! - const rowValues: Record = { - [FieldType.STRING]: generator.guid(), - [FieldType.LONGFORM]: generator.paragraph(), - [FieldType.OPTIONS]: "option 2", - [FieldType.ARRAY]: ["options 2", "options 4"], - [FieldType.NUMBER]: generator.natural(), - [FieldType.BOOLEAN]: generator.bool(), - [FieldType.DATETIME]: generator.date().toISOString(), - [FieldType.ATTACHMENTS]: [setup.structures.basicAttachment()], - [FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(), - [FieldType.FORMULA]: undefined, // generated field - [FieldType.AUTO]: undefined, // generated field - [FieldType.AI]: "LLM Output", - [FieldType.JSON]: { name: generator.guid() }, - [FieldType.INTERNAL]: generator.guid(), - [FieldType.BARCODEQR]: generator.guid(), - [FieldType.SIGNATURE_SINGLE]: setup.structures.basicAttachment(), - [FieldType.BIGINT]: generator.integer().toString(), - [FieldType.BB_REFERENCE]: [{ _id: config.getUser()._id }], - [FieldType.BB_REFERENCE_SINGLE]: { _id: config.getUser()._id }, - } - const row = await config.api.row.save(table._id!, rowValues) - expectedRowData = { - _id: row._id, - [FieldType.STRING]: rowValues[FieldType.STRING], - [FieldType.LONGFORM]: rowValues[FieldType.LONGFORM], - [FieldType.OPTIONS]: rowValues[FieldType.OPTIONS], - [FieldType.ARRAY]: rowValues[FieldType.ARRAY], - [FieldType.NUMBER]: rowValues[FieldType.NUMBER], - [FieldType.BOOLEAN]: rowValues[FieldType.BOOLEAN], - [FieldType.DATETIME]: rowValues[FieldType.DATETIME], - [FieldType.ATTACHMENTS]: rowValues[FieldType.ATTACHMENTS].map( - (a: any) => - expect.objectContaining({ - ...a, - url: expect.any(String), + it("as json", async () => { + const exportedValue = await config.api.row.exportRows( + tableId, + { query: {} }, + RowExportFormat.JSON + ) + + const json = JSON.parse(exportedValue) + expect(json).toEqual([expectedRowData]) + }) + + it("as json with schema", async () => { + const exportedValue = await config.api.row.exportRows( + tableId, + { query: {} }, + RowExportFormat.JSON_WITH_SCHEMA + ) + + const json = JSON.parse(exportedValue) + expect(json).toEqual({ + schema: expect.any(Object), + rows: [expectedRowData], + }) + }) + + it("can handle csv-special characters in strings", async () => { + const badString = 'test":, wow", "test": "wow"' + const table = await config.api.table.save( + saveTableRequest({ + schema: { + string: { + type: FieldType.STRING, + name: "string", + }, + }, }) - ), - [FieldType.ATTACHMENT_SINGLE]: expect.objectContaining({ - ...rowValues[FieldType.ATTACHMENT_SINGLE], - url: expect.any(String), - }), - [FieldType.FORMULA]: fullSchema[FieldType.FORMULA].formula, - [FieldType.AUTO]: expect.any(Number), - [FieldType.AI]: expect.any(String), - [FieldType.JSON]: rowValues[FieldType.JSON], - [FieldType.INTERNAL]: rowValues[FieldType.INTERNAL], - [FieldType.BARCODEQR]: rowValues[FieldType.BARCODEQR], - [FieldType.SIGNATURE_SINGLE]: expect.objectContaining({ - ...rowValues[FieldType.SIGNATURE_SINGLE], - url: expect.any(String), - }), - [FieldType.BIGINT]: rowValues[FieldType.BIGINT], - [FieldType.BB_REFERENCE]: rowValues[FieldType.BB_REFERENCE].map( - expect.objectContaining - ), - [FieldType.BB_REFERENCE_SINGLE]: expect.objectContaining( - rowValues[FieldType.BB_REFERENCE_SINGLE] - ), - } - }) + ) - it("as csv", async () => { - const exportedValue = await config.api.row.exportRows( - tableId, - { query: {} }, - RowExportFormat.CSV - ) + await config.api.row.save(table._id!, { string: badString }) - const jsonResult = await config.api.table.csvToJson({ - csvString: exportedValue, - }) + const exportedValue = await config.api.row.exportRows( + table._id!, + { query: {} }, + RowExportFormat.CSV + ) - const stringified = (value: string) => - JSON.stringify(value).replace(/"/g, "'") - - const matchingObject = (key: string, value: any, isArray: boolean) => { - const objectMatcher = `{'${key}':'${value[key]}'.*?}` - if (isArray) { - return expect.stringMatching(new RegExp(`^\\[${objectMatcher}\\]$`)) - } - return expect.stringMatching(new RegExp(`^${objectMatcher}$`)) - } - - expect(jsonResult).toEqual([ - { - ...expectedRowData, - auto: expect.any(String), - array: stringified(expectedRowData["array"]), - attachment: matchingObject( - "key", - expectedRowData["attachment"][0].sample, - true - ), - attachment_single: matchingObject( - "key", - expectedRowData["attachment_single"].sample, - false - ), - boolean: stringified(expectedRowData["boolean"]), - json: stringified(expectedRowData["json"]), - number: stringified(expectedRowData["number"]), - signature_single: matchingObject( - "key", - expectedRowData["signature_single"].sample, - false - ), - bb_reference: matchingObject( - "_id", - expectedRowData["bb_reference"][0].sample, - true - ), - bb_reference_single: matchingObject( - "_id", - expectedRowData["bb_reference_single"].sample, - false - ), - ai: "LLM Output", - }, - ]) - }) - - it("as json", async () => { - const exportedValue = await config.api.row.exportRows( - tableId, - { query: {} }, - RowExportFormat.JSON - ) - - const json = JSON.parse(exportedValue) - expect(json).toEqual([expectedRowData]) - }) - - it("as json with schema", async () => { - const exportedValue = await config.api.row.exportRows( - tableId, - { query: {} }, - RowExportFormat.JSON_WITH_SCHEMA - ) - - const json = JSON.parse(exportedValue) - expect(json).toEqual({ - schema: expect.any(Object), - rows: [expectedRowData], - }) - }) - - it("can handle csv-special characters in strings", async () => { - const badString = 'test":, wow", "test": "wow"' - const table = await config.api.table.save( - saveTableRequest({ - schema: { - string: { - type: FieldType.STRING, - name: "string", + const json = await config.api.table.csvToJson( + { + csvString: exportedValue, }, - }, + { + status: 200, + } + ) + + expect(json).toHaveLength(1) + expect(json[0].string).toEqual(badString) }) - ) - await config.api.row.save(table._id!, { string: badString }) + it("exported data can be re-imported", async () => { + // export all + const exportedValue = await config.api.row.exportRows( + tableId, + { query: {} }, + RowExportFormat.CSV + ) - const exportedValue = await config.api.row.exportRows( - table._id!, - { query: {} }, - RowExportFormat.CSV - ) + // import all twice + const rows = await config.api.table.csvToJson({ + csvString: exportedValue, + }) + await config.api.row.bulkImport(tableId, { + rows, + }) + await config.api.row.bulkImport(tableId, { + rows, + }) - const json = await config.api.table.csvToJson( - { - csvString: exportedValue, - }, - { - status: 200, - } - ) + const { rows: allRows } = await config.api.row.search(tableId) - expect(json).toHaveLength(1) - expect(json[0].string).toEqual(badString) - }) - - it("exported data can be re-imported", async () => { - // export all - const exportedValue = await config.api.row.exportRows( - tableId, - { query: {} }, - RowExportFormat.CSV - ) - - // import all twice - const rows = await config.api.table.csvToJson({ - csvString: exportedValue, + const expectedRow = { + ...expectedRowData, + _id: expect.any(String), + _rev: expect.any(String), + type: "row", + tableId: tableId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + expect(allRows).toEqual([expectedRow, expectedRow, expectedRow]) + }) }) - await config.api.row.bulkImport(tableId, { - rows, - }) - await config.api.row.bulkImport(tableId, { - rows, - }) - - const { rows: allRows } = await config.api.row.search(tableId) - - const expectedRow = { - ...expectedRowData, - _id: expect.any(String), - _rev: expect.any(String), - type: "row", - tableId: tableId, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - } - expect(allRows).toEqual([expectedRow, expectedRow, expectedRow]) - }) - }) - }) - - let o2mTable: Table - let m2mTable: Table - beforeAll(async () => { - o2mTable = await config.api.table.save(defaultTable()) - m2mTable = await config.api.table.save(defaultTable()) - }) - - describe.each([ - [ - "relationship fields", - (): Record => ({ - user: { - name: "user", - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: o2mTable._id!, - fieldName: "fk_o2m", - }, - users: { - name: "users", - relationshipType: RelationshipType.MANY_TO_MANY, - type: FieldType.LINK, - tableId: m2mTable._id!, - fieldName: "fk_m2m", - }, - }), - (tableId: string) => - config.api.row.save(tableId, { - name: uuid.v4(), - description: generator.paragraph(), - tableId, - }), - (row: Row) => ({ - _id: row._id, - primaryDisplay: row.name, - }), - ], - [ - "bb reference fields", - (): Record => ({ - user: { - name: "user", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - }, - users: { - name: "users", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USERS, - }, - }), - () => config.createUser(), - (row: Row) => ({ - _id: row._id, - primaryDisplay: row.email, - email: row.email, - firstName: row.firstName, - lastName: row.lastName, - }), - ], - ])("links - %s", (__, relSchema, dataGenerator, resultMapper) => { - let tableId: string - let o2mData: Row[] - let m2mData: Row[] - - beforeAll(async () => { - const table = await config.api.table.save( - defaultTable({ schema: relSchema() }) - ) - tableId = table._id! - - o2mData = [ - await dataGenerator(o2mTable._id!), - await dataGenerator(o2mTable._id!), - await dataGenerator(o2mTable._id!), - await dataGenerator(o2mTable._id!), - ] - - m2mData = [ - await dataGenerator(m2mTable._id!), - await dataGenerator(m2mTable._id!), - await dataGenerator(m2mTable._id!), - await dataGenerator(m2mTable._id!), - ] - }) - - it("can save a row when relationship fields are empty", async () => { - const row = await config.api.row.save(tableId, { - name: "foo", - description: "bar", }) - expect(row).toEqual({ - _id: expect.any(String), - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - type: isInternal ? "row" : undefined, - name: "foo", - description: "bar", - tableId, - }) - }) - - it("can save a row with a single relationship field", async () => { - const user = _.sample(o2mData)! - const row = await config.api.row.save(tableId, { - name: "foo", - description: "bar", - user: [user], - }) - - expect(row).toEqual({ - name: "foo", - description: "bar", - tableId, - user: [user].map(u => resultMapper(u)), - _id: expect.any(String), - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - type: isInternal ? "row" : undefined, - [`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id, - }) - }) - - it("can save a row with a multiple relationship field", async () => { - const selectedUsers = _.sampleSize(m2mData, 2) - const row = await config.api.row.save(tableId, { - name: "foo", - description: "bar", - users: selectedUsers, - }) - - expect(row).toEqual({ - name: "foo", - description: "bar", - tableId, - users: expect.arrayContaining(selectedUsers.map(u => resultMapper(u))), - _id: expect.any(String), - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - type: isInternal ? "row" : undefined, - }) - }) - - it("can retrieve rows with no populated relationships", async () => { - const row = await config.api.row.save(tableId, { - name: "foo", - description: "bar", - }) - - const retrieved = await config.api.row.get(tableId, row._id!) - expect(retrieved).toEqual({ - name: "foo", - description: "bar", - tableId, - user: undefined, - users: undefined, - _id: row._id, - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - ...defaultRowFields, - }) - }) - - it("can retrieve rows with populated relationships", async () => { - const user1 = _.sample(o2mData)! - const [user2, user3] = _.sampleSize(m2mData, 2) - - const row = await config.api.row.save(tableId, { - name: "foo", - description: "bar", - users: [user2, user3], - user: [user1], - }) - - const retrieved = await config.api.row.get(tableId, row._id!) - expect(retrieved).toEqual({ - name: "foo", - description: "bar", - tableId, - user: expect.arrayContaining([user1].map(u => resultMapper(u))), - users: expect.arrayContaining([user2, user3].map(u => resultMapper(u))), - _id: row._id, - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - [`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user1.id, - ...defaultRowFields, - }) - }) - - it("can update an existing populated row", async () => { - const user = _.sample(o2mData)! - const [users1, users2, users3] = _.sampleSize(m2mData, 3) - - const row = await config.api.row.save(tableId, { - name: "foo", - description: "bar", - users: [users1, users2], - }) - - const updatedRow = await config.api.row.save(tableId, { - ...row, - user: [user], - users: [users3, users1], - }) - expect(updatedRow).toEqual({ - name: "foo", - description: "bar", - tableId, - user: expect.arrayContaining([user].map(u => resultMapper(u))), - users: expect.arrayContaining( - [users3, users1].map(u => resultMapper(u)) - ), - _id: row._id, - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - type: isInternal ? "row" : undefined, - [`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id, - }) - }) - - it("can wipe an existing populated relationships in row", async () => { - const [user1, user2] = _.sampleSize(m2mData, 2) - const row = await config.api.row.save(tableId, { - name: "foo", - description: "bar", - users: [user1, user2], - }) - - const updatedRow = await config.api.row.save(tableId, { - ...row, - user: null, - users: null, - }) - expect(updatedRow).toEqual({ - name: "foo", - description: "bar", - tableId, - _id: row._id, - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - type: isInternal ? "row" : undefined, - }) - }) - - it("fetch all will populate the relationships", async () => { - const [user1] = _.sampleSize(o2mData, 1) - const [users1, users2, users3] = _.sampleSize(m2mData, 3) - - const rows = [ - { - name: generator.name(), - description: generator.name(), - users: [users1, users2], - }, - { - name: generator.name(), - description: generator.name(), - user: [user1], - users: [users1, users3], - }, - { - name: generator.name(), - description: generator.name(), - users: [users3], - }, - ] - - await config.api.row.save(tableId, rows[0]) - await config.api.row.save(tableId, rows[1]) - await config.api.row.save(tableId, rows[2]) - - const res = await config.api.row.fetch(tableId) - - expect(res).toEqual( - expect.arrayContaining( - rows.map(r => ({ - name: r.name, - description: r.description, - tableId, - user: r.user?.map(u => resultMapper(u)), - users: r.users?.length - ? expect.arrayContaining(r.users?.map(u => resultMapper(u))) - : undefined, - _id: expect.any(String), - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - [`fk_${o2mTable.name}_fk_o2m`]: - isInternal || !r.user?.length ? undefined : r.user[0].id, - ...defaultRowFields, - })) - ) - ) - }) - - it("search all will populate the relationships", async () => { - const [user1] = _.sampleSize(o2mData, 1) - const [users1, users2, users3] = _.sampleSize(m2mData, 3) - - const rows = [ - { - name: generator.name(), - description: generator.name(), - users: [users1, users2], - }, - { - name: generator.name(), - description: generator.name(), - user: [user1], - users: [users1, users3], - }, - { - name: generator.name(), - description: generator.name(), - users: [users3], - }, - ] - - await config.api.row.save(tableId, rows[0]) - await config.api.row.save(tableId, rows[1]) - await config.api.row.save(tableId, rows[2]) - - const res = await config.api.row.search(tableId) - - expect(res).toEqual({ - rows: expect.arrayContaining( - rows.map(r => ({ - name: r.name, - description: r.description, - tableId, - user: r.user?.map(u => resultMapper(u)), - users: r.users?.length - ? expect.arrayContaining(r.users?.map(u => resultMapper(u))) - : undefined, - _id: expect.any(String), - _rev: expect.any(String), - id: isInternal ? undefined : expect.any(Number), - [`fk_${o2mTable.name}_fk_o2m`]: - isInternal || !r.user?.length ? undefined : r.user[0].id, - ...defaultRowFields, - })) - ), - ...(isInternal - ? {} - : { - hasNextPage: false, - }), - }) - }) - }) - - // Upserting isn't yet supported in MSSQL or Oracle, see: - // https://github.com/knex/knex/pull/6050 - !isMSSQL && - !isOracle && - describe("relationships", () => { - let tableId: string - let viewId: string - - let auxData: Row[] = [] - - let flagCleanup: (() => void) | undefined - + let o2mTable: Table + let m2mTable: Table beforeAll(async () => { - flagCleanup = features.testutils.setFeatureFlags("*", { - ENRICHED_RELATIONSHIPS: true, - }) - - const aux2Table = await config.api.table.save(saveTableRequest()) - const aux2Data = await config.api.row.save(aux2Table._id!, {}) - - const auxTable = await config.api.table.save( - saveTableRequest({ - primaryDisplay: "name", - schema: { - name: { - name: "name", - type: FieldType.STRING, - constraints: { presence: true }, - }, - age: { - name: "age", - type: FieldType.NUMBER, - constraints: { presence: true }, - }, - address: { - name: "address", - type: FieldType.STRING, - constraints: { presence: true }, - visible: false, - }, - link: { - name: "link", - type: FieldType.LINK, - tableId: aux2Table._id!, - relationshipType: RelationshipType.MANY_TO_MANY, - fieldName: "fk_aux", - constraints: { presence: true }, - }, - formula: { - name: "formula", - type: FieldType.FORMULA, - formula: "{{ any }}", - constraints: { presence: true }, - }, - }, - }) - ) - const auxTableId = auxTable._id! - - for (const name of generator.unique(() => generator.name(), 10)) { - auxData.push( - await config.api.row.save(auxTableId, { - name, - age: generator.age(), - address: generator.address(), - link: [aux2Data], - }) - ) - } - - const table = await config.api.table.save( - saveTableRequest({ - schema: { - title: { - name: "title", - type: FieldType.STRING, - constraints: { presence: true }, - }, - relWithNoSchema: { - name: "relWithNoSchema", - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: auxTableId, - fieldName: "fk_relWithNoSchema", - constraints: { presence: true }, - }, - relWithEmptySchema: { - name: "relWithEmptySchema", - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: auxTableId, - fieldName: "fk_relWithEmptySchema", - constraints: { presence: true }, - }, - relWithFullSchema: { - name: "relWithFullSchema", - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: auxTableId, - fieldName: "fk_relWithFullSchema", - constraints: { presence: true }, - }, - relWithHalfSchema: { - name: "relWithHalfSchema", - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: auxTableId, - fieldName: "fk_relWithHalfSchema", - constraints: { presence: true }, - }, - relWithIllegalSchema: { - name: "relWithIllegalSchema", - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: auxTableId, - fieldName: "fk_relWithIllegalSchema", - constraints: { presence: true }, - }, - }, - }) - ) - tableId = table._id! - const view = await config.api.viewV2.create({ - name: generator.guid(), - tableId, - schema: { - title: { - visible: true, - }, - relWithNoSchema: { - visible: true, - }, - relWithEmptySchema: { - visible: true, - columns: {}, - }, - relWithFullSchema: { - visible: true, - columns: Object.keys(auxTable.schema).reduce< - Record - >((acc, c) => ({ ...acc, [c]: { visible: true } }), {}), - }, - relWithHalfSchema: { - visible: true, - columns: { - name: { visible: true }, - age: { visible: false, readonly: true }, - }, - }, - relWithIllegalSchema: { - visible: true, - columns: { - name: { visible: true }, - address: { visible: true }, - unexisting: { visible: true }, - }, - }, - }, - }) - - viewId = view.id + o2mTable = await config.api.table.save(defaultTable()) + m2mTable = await config.api.table.save(defaultTable()) }) - afterAll(() => { - flagCleanup?.() - }) - - const testScenarios: [string, (row: Row) => Promise | Row][] = [ - ["get row", (row: Row) => config.api.row.get(viewId, row._id!)], + describe.each([ [ - "from view search", - async (row: Row) => { - const { rows } = await config.api.viewV2.search(viewId) - return rows.find(r => r._id === row._id!) - }, + "relationship fields", + (): Record => ({ + user: { + name: "user", + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: o2mTable._id!, + fieldName: "fk_o2m", + }, + users: { + name: "users", + relationshipType: RelationshipType.MANY_TO_MANY, + type: FieldType.LINK, + tableId: m2mTable._id!, + fieldName: "fk_m2m", + }, + }), + (tableId: string) => + config.api.row.save(tableId, { + name: uuid.v4(), + description: generator.paragraph(), + tableId, + }), + (row: Row) => ({ + _id: row._id, + primaryDisplay: row.name, + }), ], - ["from original saved row", (row: Row) => row], - ["from updated row", (row: Row) => config.api.row.save(viewId, row)], - ] + [ + "bb reference fields", + (): Record => ({ + user: { + name: "user", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + }, + users: { + name: "users", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USERS, + }, + }), + () => config.createUser(), + (row: Row) => ({ + _id: row._id, + primaryDisplay: row.email, + email: row.email, + firstName: row.firstName, + lastName: row.lastName, + }), + ], + ])("links - %s", (__, relSchema, dataGenerator, resultMapper) => { + let tableId: string + let o2mData: Row[] + let m2mData: Row[] - it.each(testScenarios)( - "can retrieve rows with populated relationships (via %s)", - async (__, retrieveDelegate) => { - const otherRows = _.sampleSize(auxData, 5) + beforeAll(async () => { + const table = await config.api.table.save( + defaultTable({ schema: relSchema() }) + ) + tableId = table._id! - const row = await config.api.row.save(viewId, { - title: generator.word(), - relWithNoSchema: [otherRows[0]], - relWithEmptySchema: [otherRows[1]], - relWithFullSchema: [otherRows[2]], - relWithHalfSchema: [otherRows[3]], - relWithIllegalSchema: [otherRows[4]], + o2mData = [ + await dataGenerator(o2mTable._id!), + await dataGenerator(o2mTable._id!), + await dataGenerator(o2mTable._id!), + await dataGenerator(o2mTable._id!), + ] + + m2mData = [ + await dataGenerator(m2mTable._id!), + await dataGenerator(m2mTable._id!), + await dataGenerator(m2mTable._id!), + await dataGenerator(m2mTable._id!), + ] + }) + + it("can save a row when relationship fields are empty", async () => { + const row = await config.api.row.save(tableId, { + name: "foo", + description: "bar", }) - const retrieved = await retrieveDelegate(row) + expect(row).toEqual({ + _id: expect.any(String), + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + type: isInternal ? "row" : undefined, + name: "foo", + description: "bar", + tableId, + }) + }) - expect(retrieved).toEqual( - expect.objectContaining({ - title: row.title, - relWithNoSchema: [ - { - _id: otherRows[0]._id, - primaryDisplay: otherRows[0].name, - }, - ], - relWithEmptySchema: [ - { - _id: otherRows[1]._id, - primaryDisplay: otherRows[1].name, - }, - ], - relWithFullSchema: [ - { - _id: otherRows[2]._id, - primaryDisplay: otherRows[2].name, - name: otherRows[2].name, - age: otherRows[2].age, - id: otherRows[2].id, - }, - ], - relWithHalfSchema: [ - { - _id: otherRows[3]._id, - primaryDisplay: otherRows[3].name, - name: otherRows[3].name, - }, - ], - relWithIllegalSchema: [ - { - _id: otherRows[4]._id, - primaryDisplay: otherRows[4].name, - name: otherRows[4].name, - }, - ], - }) - ) - } - ) + it("can save a row with a single relationship field", async () => { + const user = _.sample(o2mData)! + const row = await config.api.row.save(tableId, { + name: "foo", + description: "bar", + user: [user], + }) - it.each(testScenarios)( - "does not enrich relationships when not enabled (via %s)", - async (__, retrieveDelegate) => { - await features.testutils.withFeatureFlags( - "*", + expect(row).toEqual({ + name: "foo", + description: "bar", + tableId, + user: [user].map(u => resultMapper(u)), + _id: expect.any(String), + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + type: isInternal ? "row" : undefined, + [`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id, + }) + }) + + it("can save a row with a multiple relationship field", async () => { + const selectedUsers = _.sampleSize(m2mData, 2) + const row = await config.api.row.save(tableId, { + name: "foo", + description: "bar", + users: selectedUsers, + }) + + expect(row).toEqual({ + name: "foo", + description: "bar", + tableId, + users: expect.arrayContaining( + selectedUsers.map(u => resultMapper(u)) + ), + _id: expect.any(String), + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + type: isInternal ? "row" : undefined, + }) + }) + + it("can retrieve rows with no populated relationships", async () => { + const row = await config.api.row.save(tableId, { + name: "foo", + description: "bar", + }) + + const retrieved = await config.api.row.get(tableId, row._id!) + expect(retrieved).toEqual({ + name: "foo", + description: "bar", + tableId, + user: undefined, + users: undefined, + _id: row._id, + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + ...defaultRowFields, + }) + }) + + it("can retrieve rows with populated relationships", async () => { + const user1 = _.sample(o2mData)! + const [user2, user3] = _.sampleSize(m2mData, 2) + + const row = await config.api.row.save(tableId, { + name: "foo", + description: "bar", + users: [user2, user3], + user: [user1], + }) + + const retrieved = await config.api.row.get(tableId, row._id!) + expect(retrieved).toEqual({ + name: "foo", + description: "bar", + tableId, + user: expect.arrayContaining([user1].map(u => resultMapper(u))), + users: expect.arrayContaining( + [user2, user3].map(u => resultMapper(u)) + ), + _id: row._id, + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + [`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user1.id, + ...defaultRowFields, + }) + }) + + it("can update an existing populated row", async () => { + const user = _.sample(o2mData)! + const [users1, users2, users3] = _.sampleSize(m2mData, 3) + + const row = await config.api.row.save(tableId, { + name: "foo", + description: "bar", + users: [users1, users2], + }) + + const updatedRow = await config.api.row.save(tableId, { + ...row, + user: [user], + users: [users3, users1], + }) + expect(updatedRow).toEqual({ + name: "foo", + description: "bar", + tableId, + user: expect.arrayContaining([user].map(u => resultMapper(u))), + users: expect.arrayContaining( + [users3, users1].map(u => resultMapper(u)) + ), + _id: row._id, + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + type: isInternal ? "row" : undefined, + [`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id, + }) + }) + + it("can wipe an existing populated relationships in row", async () => { + const [user1, user2] = _.sampleSize(m2mData, 2) + const row = await config.api.row.save(tableId, { + name: "foo", + description: "bar", + users: [user1, user2], + }) + + const updatedRow = await config.api.row.save(tableId, { + ...row, + user: null, + users: null, + }) + expect(updatedRow).toEqual({ + name: "foo", + description: "bar", + tableId, + _id: row._id, + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + type: isInternal ? "row" : undefined, + }) + }) + + it("fetch all will populate the relationships", async () => { + const [user1] = _.sampleSize(o2mData, 1) + const [users1, users2, users3] = _.sampleSize(m2mData, 3) + + const rows = [ { - ENRICHED_RELATIONSHIPS: false, + name: generator.name(), + description: generator.name(), + users: [users1, users2], }, - async () => { + { + name: generator.name(), + description: generator.name(), + user: [user1], + users: [users1, users3], + }, + { + name: generator.name(), + description: generator.name(), + users: [users3], + }, + ] + + await config.api.row.save(tableId, rows[0]) + await config.api.row.save(tableId, rows[1]) + await config.api.row.save(tableId, rows[2]) + + const res = await config.api.row.fetch(tableId) + + expect(res).toEqual( + expect.arrayContaining( + rows.map(r => ({ + name: r.name, + description: r.description, + tableId, + user: r.user?.map(u => resultMapper(u)), + users: r.users?.length + ? expect.arrayContaining(r.users?.map(u => resultMapper(u))) + : undefined, + _id: expect.any(String), + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + [`fk_${o2mTable.name}_fk_o2m`]: + isInternal || !r.user?.length ? undefined : r.user[0].id, + ...defaultRowFields, + })) + ) + ) + }) + + it("search all will populate the relationships", async () => { + const [user1] = _.sampleSize(o2mData, 1) + const [users1, users2, users3] = _.sampleSize(m2mData, 3) + + const rows = [ + { + name: generator.name(), + description: generator.name(), + users: [users1, users2], + }, + { + name: generator.name(), + description: generator.name(), + user: [user1], + users: [users1, users3], + }, + { + name: generator.name(), + description: generator.name(), + users: [users3], + }, + ] + + await config.api.row.save(tableId, rows[0]) + await config.api.row.save(tableId, rows[1]) + await config.api.row.save(tableId, rows[2]) + + const res = await config.api.row.search(tableId) + + expect(res).toEqual({ + rows: expect.arrayContaining( + rows.map(r => ({ + name: r.name, + description: r.description, + tableId, + user: r.user?.map(u => resultMapper(u)), + users: r.users?.length + ? expect.arrayContaining(r.users?.map(u => resultMapper(u))) + : undefined, + _id: expect.any(String), + _rev: expect.any(String), + id: isInternal ? undefined : expect.any(Number), + [`fk_${o2mTable.name}_fk_o2m`]: + isInternal || !r.user?.length ? undefined : r.user[0].id, + ...defaultRowFields, + })) + ), + ...(isInternal + ? {} + : { + hasNextPage: false, + }), + }) + }) + }) + + // Upserting isn't yet supported in MSSQL or Oracle, see: + // https://github.com/knex/knex/pull/6050 + !isMSSQL && + !isOracle && + describe("relationships", () => { + let tableId: string + let viewId: string + + let auxData: Row[] = [] + + beforeAll(async () => { + const aux2Table = await config.api.table.save(saveTableRequest()) + const aux2Data = await config.api.row.save(aux2Table._id!, {}) + + const auxTable = await config.api.table.save( + saveTableRequest({ + primaryDisplay: "name", + schema: { + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: true }, + }, + age: { + name: "age", + type: FieldType.NUMBER, + constraints: { presence: true }, + }, + address: { + name: "address", + type: FieldType.STRING, + constraints: { presence: true }, + visible: false, + }, + link: { + name: "link", + type: FieldType.LINK, + tableId: aux2Table._id!, + relationshipType: RelationshipType.MANY_TO_MANY, + fieldName: "fk_aux", + constraints: { presence: true }, + }, + formula: { + name: "formula", + type: FieldType.FORMULA, + formula: "{{ any }}", + constraints: { presence: true }, + }, + }, + }) + ) + const auxTableId = auxTable._id! + + for (const name of generator.unique(() => generator.name(), 10)) { + auxData.push( + await config.api.row.save(auxTableId, { + name, + age: generator.age(), + address: generator.address(), + link: [aux2Data], + }) + ) + } + + const table = await config.api.table.save( + saveTableRequest({ + schema: { + title: { + name: "title", + type: FieldType.STRING, + constraints: { presence: true }, + }, + relWithNoSchema: { + name: "relWithNoSchema", + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: auxTableId, + fieldName: "fk_relWithNoSchema", + constraints: { presence: true }, + }, + relWithEmptySchema: { + name: "relWithEmptySchema", + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: auxTableId, + fieldName: "fk_relWithEmptySchema", + constraints: { presence: true }, + }, + relWithFullSchema: { + name: "relWithFullSchema", + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: auxTableId, + fieldName: "fk_relWithFullSchema", + constraints: { presence: true }, + }, + relWithHalfSchema: { + name: "relWithHalfSchema", + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: auxTableId, + fieldName: "fk_relWithHalfSchema", + constraints: { presence: true }, + }, + relWithIllegalSchema: { + name: "relWithIllegalSchema", + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: auxTableId, + fieldName: "fk_relWithIllegalSchema", + constraints: { presence: true }, + }, + }, + }) + ) + tableId = table._id! + const view = await config.api.viewV2.create({ + name: generator.guid(), + tableId, + schema: { + title: { + visible: true, + }, + relWithNoSchema: { + visible: true, + }, + relWithEmptySchema: { + visible: true, + columns: {}, + }, + relWithFullSchema: { + visible: true, + columns: Object.keys(auxTable.schema).reduce< + Record + >((acc, c) => ({ ...acc, [c]: { visible: true } }), {}), + }, + relWithHalfSchema: { + visible: true, + columns: { + name: { visible: true }, + age: { visible: false, readonly: true }, + }, + }, + relWithIllegalSchema: { + visible: true, + columns: { + name: { visible: true }, + address: { visible: true }, + unexisting: { visible: true }, + }, + }, + }, + }) + + viewId = view.id + }) + + const testScenarios: [string, (row: Row) => Promise | Row][] = [ + ["get row", (row: Row) => config.api.row.get(viewId, row._id!)], + [ + "from view search", + async (row: Row) => { + const { rows } = await config.api.viewV2.search(viewId) + return rows.find(r => r._id === row._id!) + }, + ], + ["from original saved row", (row: Row) => row], + [ + "from updated row", + (row: Row) => config.api.row.save(viewId, row), + ], + ] + + it.each(testScenarios)( + "can retrieve rows with populated relationships (via %s)", + async (__, retrieveDelegate) => { + const otherRows = _.sampleSize(auxData, 5) + + const row = await config.api.row.save(viewId, { + title: generator.word(), + relWithNoSchema: [otherRows[0]], + relWithEmptySchema: [otherRows[1]], + relWithFullSchema: [otherRows[2]], + relWithHalfSchema: [otherRows[3]], + relWithIllegalSchema: [otherRows[4]], + }) + + const retrieved = await retrieveDelegate(row) + + expect(retrieved).toEqual( + expect.objectContaining({ + title: row.title, + relWithNoSchema: [ + { + _id: otherRows[0]._id, + primaryDisplay: otherRows[0].name, + }, + ], + relWithEmptySchema: [ + { + _id: otherRows[1]._id, + primaryDisplay: otherRows[1].name, + }, + ], + relWithFullSchema: [ + { + _id: otherRows[2]._id, + primaryDisplay: otherRows[2].name, + name: otherRows[2].name, + age: otherRows[2].age, + id: otherRows[2].id, + }, + ], + relWithHalfSchema: [ + { + _id: otherRows[3]._id, + primaryDisplay: otherRows[3].name, + name: otherRows[3].name, + }, + ], + relWithIllegalSchema: [ + { + _id: otherRows[4]._id, + primaryDisplay: otherRows[4].name, + name: otherRows[4].name, + }, + ], + }) + ) + } + ) + + it.each([ + [ + "from table fetch", + async (row: Row) => { + const rows = await config.api.row.fetch(tableId) + return rows.find(r => r._id === row._id!) + }, + ], + [ + "from table search", + async (row: Row) => { + const { rows } = await config.api.row.search(tableId) + return rows.find(r => r._id === row._id!) + }, + ], + ])( + "does not enrich when fetching from the table (via %s)", + async (__, retrieveDelegate) => { const otherRows = _.sampleSize(auxData, 5) const row = await config.api.row.save(viewId, { @@ -3349,234 +3162,298 @@ describe.each([ ) } ) - } - ) + }) - it.each([ - [ - "from table fetch", - async (row: Row) => { - const rows = await config.api.row.fetch(tableId) - return rows.find(r => r._id === row._id!) - }, - ], - [ - "from table search", - async (row: Row) => { - const { rows } = await config.api.row.search(tableId) - return rows.find(r => r._id === row._id!) - }, - ], - ])( - "does not enrich when fetching from the table (via %s)", - async (__, retrieveDelegate) => { - const otherRows = _.sampleSize(auxData, 5) + isInternal && + describe("AI fields", () => { + let table: Table - const row = await config.api.row.save(viewId, { - title: generator.word(), - relWithNoSchema: [otherRows[0]], - relWithEmptySchema: [otherRows[1]], - relWithFullSchema: [otherRows[2]], - relWithHalfSchema: [otherRows[3]], - relWithIllegalSchema: [otherRows[4]], + beforeAll(async () => { + mocks.licenses.useBudibaseAI() + mocks.licenses.useAICustomConfigs() + table = await config.api.table.save( + saveTableRequest({ + schema: { + ai: { + name: "ai", + type: FieldType.AI, + operation: AIOperationEnum.PROMPT, + prompt: "Convert the following to German: '{{ product }}'", + }, + product: { + name: "product", + type: FieldType.STRING, + }, + }, + }) + ) + + await config.api.row.save(table._id!, { + product: generator.word(), + }) }) - const retrieved = await retrieveDelegate(row) + afterAll(() => { + jest.unmock("@budibase/pro") + }) - expect(retrieved).toEqual( - expect.objectContaining({ - title: row.title, - relWithNoSchema: [ - { - _id: otherRows[0]._id, - primaryDisplay: otherRows[0].name, + it("should be able to save a row with an AI column", async () => { + const { rows } = await config.api.row.search(table._id!) + expect(rows.length).toBe(1) + expect(rows[0].ai).toEqual("Mock LLM Response") + }) + + it("should be able to update a row with an AI column", async () => { + const { rows } = await config.api.row.search(table._id!) + expect(rows.length).toBe(1) + await config.api.row.save(table._id!, { + product: generator.word(), + ...rows[0], + }) + expect(rows.length).toBe(1) + expect(rows[0].ai).toEqual("Mock LLM Response") + }) + }) + + describe("Formula fields", () => { + let table: Table + let otherTable: Table + let relatedRow: Row, mainRow: Row + + beforeAll(async () => { + otherTable = await config.api.table.save(defaultTable()) + table = await config.api.table.save( + saveTableRequest({ + schema: { + links: { + name: "links", + fieldName: "links", + type: FieldType.LINK, + tableId: otherTable._id!, + relationshipType: RelationshipType.ONE_TO_MANY, }, - ], - relWithEmptySchema: [ - { - _id: otherRows[1]._id, - primaryDisplay: otherRows[1].name, + formula: { + name: "formula", + type: FieldType.FORMULA, + formula: "{{ links.0.name }}", + formulaType: FormulaType.DYNAMIC, }, - ], - relWithFullSchema: [ - { - _id: otherRows[2]._id, - primaryDisplay: otherRows[2].name, - }, - ], - relWithHalfSchema: [ - { - _id: otherRows[3]._id, - primaryDisplay: otherRows[3].name, - }, - ], - relWithIllegalSchema: [ - { - _id: otherRows[4]._id, - primaryDisplay: otherRows[4].name, - }, - ], + }, }) ) - } - ) - }) - isSqs && - describe("AI fields", () => { - let table: Table - - beforeAll(async () => { - mocks.licenses.useBudibaseAI() - mocks.licenses.useAICustomConfigs() - table = await config.api.table.save( - saveTableRequest({ - schema: { - ai: { - name: "ai", - type: FieldType.AI, - operation: AIOperationEnum.PROMPT, - prompt: "Convert the following to German: '{{ product }}'", - }, - product: { - name: "product", - type: FieldType.STRING, - }, - }, + relatedRow = await config.api.row.save(otherTable._id!, { + name: generator.word(), + description: generator.paragraph(), + }) + mainRow = await config.api.row.save(table._id!, { + name: generator.word(), + description: generator.paragraph(), + tableId: table._id!, + links: [relatedRow._id], }) - ) - - await config.api.row.save(table._id!, { - product: generator.word(), }) - }) - afterAll(() => { - jest.unmock("@budibase/pro") - }) - - it("should be able to save a row with an AI column", async () => { - const { rows } = await config.api.row.search(table._id!) - expect(rows.length).toBe(1) - expect(rows[0].ai).toEqual("Mock LLM Response") - }) - - it("should be able to update a row with an AI column", async () => { - const { rows } = await config.api.row.search(table._id!) - expect(rows.length).toBe(1) - await config.api.row.save(table._id!, { - product: generator.word(), - ...rows[0], - }) - expect(rows.length).toBe(1) - expect(rows[0].ai).toEqual("Mock LLM Response") - }) - }) - - describe("Formula fields", () => { - let table: Table - let otherTable: Table - let relatedRow: Row - - beforeAll(async () => { - otherTable = await config.api.table.save(defaultTable()) - table = await config.api.table.save( - saveTableRequest({ - schema: { - links: { - name: "links", - fieldName: "links", - type: FieldType.LINK, - tableId: otherTable._id!, - relationshipType: RelationshipType.ONE_TO_MANY, - }, - formula: { - name: "formula", - type: FieldType.FORMULA, - formula: "{{ links.0.name }}", - formulaType: FormulaType.DYNAMIC, - }, - }, - }) - ) - - relatedRow = await config.api.row.save(otherTable._id!, { - name: generator.word(), - description: generator.paragraph(), - }) - await config.api.row.save(table._id!, { - name: generator.word(), - description: generator.paragraph(), - tableId: table._id!, - links: [relatedRow._id], - }) - }) - - it("should be able to search for rows containing formulas", async () => { - const { rows } = await config.api.row.search(table._id!) - expect(rows.length).toBe(1) - expect(rows[0].links.length).toBe(1) - const row = rows[0] - expect(row.formula).toBe(relatedRow.name) - }) - }) - - describe("Formula JS protection", () => { - it("should time out JS execution if a single cell takes too long", async () => { - await withEnv({ JS_PER_INVOCATION_TIMEOUT_MS: 40 }, async () => { - const js = Buffer.from( - ` - let i = 0; - while (true) { - i++; - } - return i; - ` - ).toString("base64") - - const table = await config.api.table.save( - saveTableRequest({ + async function updateFormulaColumn( + formula: string, + opts?: { + responseType?: FormulaResponseType + formulaType?: FormulaType + } + ) { + table = await config.api.table.save({ + ...table, schema: { - text: { - name: "text", - type: FieldType.STRING, - }, + ...table.schema, formula: { name: "formula", type: FieldType.FORMULA, - formula: `{{ js "${js}"}}`, - formulaType: FormulaType.DYNAMIC, + formula: formula, + responseType: opts?.responseType, + formulaType: opts?.formulaType || FormulaType.DYNAMIC, }, }, }) - ) + } - await config.api.row.save(table._id!, { text: "foo" }) - const { rows } = await config.api.row.search(table._id!) - expect(rows).toHaveLength(1) - const row = rows[0] - expect(row.text).toBe("foo") - expect(row.formula).toBe("Timed out while executing JS") + it("should be able to search for rows containing formulas", async () => { + const { rows } = await config.api.row.search(table._id!) + expect(rows.length).toBe(1) + expect(rows[0].links.length).toBe(1) + const row = rows[0] + expect(row.formula).toBe(relatedRow.name) + }) + + it("should coerce - number response type", async () => { + await updateFormulaColumn(encodeJS("return 1"), { + responseType: FieldType.NUMBER, + }) + const { rows } = await config.api.row.search(table._id!) + expect(rows[0].formula).toBe(1) + }) + + it("should coerce - boolean response type", async () => { + await updateFormulaColumn(encodeJS("return true"), { + responseType: FieldType.BOOLEAN, + }) + const { rows } = await config.api.row.search(table._id!) + expect(rows[0].formula).toBe(true) + }) + + it("should coerce - datetime response type", async () => { + await updateFormulaColumn(encodeJS("return new Date()"), { + responseType: FieldType.DATETIME, + }) + const { rows } = await config.api.row.search(table._id!) + expect(isDate(rows[0].formula)).toBe(true) + }) + + it("should coerce - datetime with invalid value", async () => { + await updateFormulaColumn(encodeJS("return 'a'"), { + responseType: FieldType.DATETIME, + }) + const { rows } = await config.api.row.search(table._id!) + expect(rows[0].formula).toBeUndefined() + }) + + it("should coerce handlebars", async () => { + await updateFormulaColumn("{{ add 1 1 }}", { + responseType: FieldType.NUMBER, + }) + const { rows } = await config.api.row.search(table._id!) + expect(rows[0].formula).toBe(2) + }) + + it("should coerce handlebars to string (default)", async () => { + await updateFormulaColumn("{{ add 1 1 }}", { + responseType: FieldType.STRING, + }) + const { rows } = await config.api.row.search(table._id!) + expect(rows[0].formula).toBe("2") + }) + + isInternal && + it("should coerce a static handlebars formula", async () => { + await updateFormulaColumn(encodeJS("return 1"), { + responseType: FieldType.NUMBER, + formulaType: FormulaType.STATIC, + }) + // save the row to store the static value + await config.api.row.save(table._id!, mainRow) + const { rows } = await config.api.row.search(table._id!) + expect(rows[0].formula).toBe(1) + }) }) - }) - it("should time out JS execution if a multiple cells take too long", async () => { - await withEnv( - { - JS_PER_INVOCATION_TIMEOUT_MS: 40, - JS_PER_REQUEST_TIMEOUT_MS: 80, - }, - async () => { - const js = Buffer.from( - ` + describe("Formula JS protection", () => { + it("should time out JS execution if a single cell takes too long", async () => { + await withEnv({ JS_PER_INVOCATION_TIMEOUT_MS: 40 }, async () => { + const js = encodeJS( + ` let i = 0; while (true) { i++; } return i; ` - ).toString("base64") + ) + const table = await config.api.table.save( + saveTableRequest({ + schema: { + text: { + name: "text", + type: FieldType.STRING, + }, + formula: { + name: "formula", + type: FieldType.FORMULA, + formula: js, + formulaType: FormulaType.DYNAMIC, + }, + }, + }) + ) + + await config.api.row.save(table._id!, { text: "foo" }) + const { rows } = await config.api.row.search(table._id!) + expect(rows).toHaveLength(1) + const row = rows[0] + expect(row.text).toBe("foo") + expect(row.formula).toBe("Timed out while executing JS") + }) + }) + + it("should time out JS execution if a multiple cells take too long", async () => { + await withEnv( + { + JS_PER_INVOCATION_TIMEOUT_MS: 40, + JS_PER_REQUEST_TIMEOUT_MS: 80, + }, + async () => { + const js = encodeJS( + ` + let i = 0; + while (true) { + i++; + } + return i; + ` + ) + + const table = await config.api.table.save( + saveTableRequest({ + schema: { + text: { + name: "text", + type: FieldType.STRING, + }, + formula: { + name: "formula", + type: FieldType.FORMULA, + formula: js, + formulaType: FormulaType.DYNAMIC, + }, + }, + }) + ) + + for (let i = 0; i < 10; i++) { + await config.api.row.save(table._id!, { text: "foo" }) + } + + // Run this test 3 times to make sure that there's no cross-request + // pollution of the execution time tracking. + for (let reqs = 0; reqs < 3; reqs++) { + const { rows } = await config.api.row.search(table._id!) + expect(rows).toHaveLength(10) + + let i = 0 + for (; i < 10; i++) { + const row = rows[i] + if (row.formula !== JsTimeoutError.message) { + break + } + } + + // Given the execution times are not deterministic, we can't be sure + // of the exact number of rows that were executed before the timeout + // but it should absolutely be at least 1. + expect(i).toBeGreaterThan(0) + expect(i).toBeLessThan(5) + + for (; i < 10; i++) { + const row = rows[i] + expect(row.text).toBe("foo") + expect(row.formula).toStartWith("CPU time limit exceeded ") + } + } + } + ) + }) + + it("should not carry over context between formulas", async () => { + const js = encodeJS(`return $("[text]");`) const table = await config.api.table.save( saveTableRequest({ schema: { @@ -3587,7 +3464,7 @@ describe.each([ formula: { name: "formula", type: FieldType.FORMULA, - formula: `{{ js "${js}"}}`, + formula: js, formulaType: FormulaType.DYNAMIC, }, }, @@ -3595,82 +3472,29 @@ describe.each([ ) for (let i = 0; i < 10; i++) { - await config.api.row.save(table._id!, { text: "foo" }) + await config.api.row.save(table._id!, { text: `foo${i}` }) } - // Run this test 3 times to make sure that there's no cross-request - // pollution of the execution time tracking. - for (let reqs = 0; reqs < 3; reqs++) { - const { rows } = await config.api.row.search(table._id!) - expect(rows).toHaveLength(10) + const { rows } = await config.api.row.search(table._id!) + expect(rows).toHaveLength(10) - let i = 0 - for (; i < 10; i++) { - const row = rows[i] - if (row.formula !== JsTimeoutError.message) { - break - } - } - - // Given the execution times are not deterministic, we can't be sure - // of the exact number of rows that were executed before the timeout - // but it should absolutely be at least 1. - expect(i).toBeGreaterThan(0) - expect(i).toBeLessThan(5) - - for (; i < 10; i++) { - const row = rows[i] - expect(row.text).toBe("foo") - expect(row.formula).toStartWith("CPU time limit exceeded ") - } - } - } - ) - }) - - it("should not carry over context between formulas", async () => { - const js = Buffer.from(`return $("[text]");`).toString("base64") - const table = await config.api.table.save( - saveTableRequest({ - schema: { - text: { - name: "text", - type: FieldType.STRING, - }, - formula: { - name: "formula", - type: FieldType.FORMULA, - formula: `{{ js "${js}"}}`, - formulaType: FormulaType.DYNAMIC, - }, - }, + const formulaValues = rows.map(r => r.formula) + expect(formulaValues).toEqual( + expect.arrayContaining([ + "foo0", + "foo1", + "foo2", + "foo3", + "foo4", + "foo5", + "foo6", + "foo7", + "foo8", + "foo9", + ]) + ) }) - ) - - for (let i = 0; i < 10; i++) { - await config.api.row.save(table._id!, { text: `foo${i}` }) - } - - const { rows } = await config.api.row.search(table._id!) - expect(rows).toHaveLength(10) - - const formulaValues = rows.map(r => r.formula) - expect(formulaValues).toEqual( - expect.arrayContaining([ - "foo0", - "foo1", - "foo2", - "foo3", - "foo4", - "foo5", - "foo6", - "foo7", - "foo8", - "foo9", - ]) - ) - }) - }) -}) - -// todo: remove me + }) + } + ) +} diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 14a1812195..76046c06ea 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -9,15 +9,20 @@ import { import { automations } from "@budibase/pro" import { CreateRowActionRequest, + Datasource, DocumentType, PermissionLevel, RowActionResponse, + Table, TableRowActions, } from "@budibase/types" import * as setup from "./utilities" import { generator, mocks } from "@budibase/backend-core/tests" import { Expectations } from "../../../tests/utilities/api/base" -import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" +import { + DatabaseName, + datasourceDescribe, +} from "../../../integrations/tests/utils" import { generateRowActionsID } from "../../../db/utils" const expectAutomationId = () => @@ -969,36 +974,38 @@ describe("/rowsActions", () => { status: 200, }) }) + }) +}) - it.each([ - [ - "internal", - async () => { - await config.newTenant() +const descriptions = datasourceDescribe({ + only: [DatabaseName.SQS, DatabaseName.POSTGRES], +}) + +if (descriptions.length) { + describe.each(descriptions)( + "row actions ($dbName)", + ({ config, dsProvider, isInternal }) => { + let datasource: Datasource | undefined + + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource + }) + + async function getTable(): Promise
{ + if (isInternal) { await config.api.application.addSampleData(config.getAppId()) const tables = await config.api.table.fetch() - const table = tables.find( - t => t.sourceId === DEFAULT_BB_DATASOURCE_ID - )! - return table - }, - ], - [ - "external", - async () => { - await config.newTenant() - const ds = await config.createDatasource({ - datasource: await getDatasource(DatabaseName.POSTGRES), - }) + return tables.find(t => t.sourceId === DEFAULT_BB_DATASOURCE_ID)! + } else { const table = await config.api.table.save( - setup.structures.tableForDatasource(ds) + setup.structures.tableForDatasource(datasource!) ) return table - }, - ], - ])( - "should delete all the row actions (and automations) for its tables when a datasource is deleted", - async (_, getTable) => { + } + } + + it("should delete all the row actions (and automations) for its tables when a datasource is deleted", async () => { async function getRowActionsFromDb(tableId: string) { return await context.doInAppContext(config.getAppId(), async () => { const db = context.getAppDB() @@ -1032,7 +1039,7 @@ describe("/rowsActions", () => { expect(automationsResp.automations).toHaveLength(0) expect(await getRowActionsFromDb(tableId)).toBeUndefined() - } - ) - }) -}) + }) + } + ) +} diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 2da213cd38..5384444067 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1,14 +1,12 @@ import { tableForDatasource } from "../../../tests/utilities/structures" import { DatabaseName, - getDatasource, - knexClient, + datasourceDescribe, } from "../../../integrations/tests/utils" import { context, db as dbCore, docIds, - features, MAX_VALID_DATE, MIN_VALID_DATE, SQLITE_DESIGN_DOC_ID, @@ -16,7 +14,6 @@ import { withEnv as withCoreEnv, } from "@budibase/backend-core" -import * as setup from "./utilities" import { AIOperationEnum, AutoFieldSubType, @@ -62,2436 +59,2486 @@ jest.mock("@budibase/pro", () => ({ }, })) -describe.each([ - ["in-memory", undefined], - ["lucene", undefined], - ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], - [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], -])("search (%s)", (name, dsProvider) => { - const isSqs = name === "sqs" - const isLucene = name === "lucene" - const isInMemory = name === "in-memory" - const isInternal = isSqs || isLucene || isInMemory - const isOracle = name === DatabaseName.ORACLE - const isSql = !isInMemory && !isLucene - const config = setup.getConfig() - - let envCleanup: (() => void) | undefined - let datasource: Datasource | undefined - let client: Knex | undefined - let tableOrViewId: string - let rows: Row[] - - async function basicRelationshipTables(type: RelationshipType) { - const relatedTable = await createTable({ - name: { name: "name", type: FieldType.STRING }, - }) - const tableId = await createTable({ - name: { name: "name", type: FieldType.STRING }, - //@ts-ignore - API accepts this structure, will build out rest of definition - productCat: { - type: FieldType.LINK, - relationshipType: type, - name: "productCat", - fieldName: "product", - tableId: relatedTable, - constraints: { - type: "array", - }, - }, - }) - return { - relatedTable: await config.api.table.get(relatedTable), - tableId, - } - } - - beforeAll(async () => { - await features.testutils.withFeatureFlags("*", { SQS: true }, () => - config.init() - ) - envCleanup = features.testutils.setFeatureFlags("*", { - SQS: isSqs, - }) - - if (config.app?.appId) { - config.app = await config.api.application.update(config.app?.appId, { - snippets: [ - { - name: "WeeksAgo", - code: `return function (weeks) {\n const currentTime = new Date(${Date.now()});\n currentTime.setDate(currentTime.getDate()-(7 * (weeks || 1)));\n return currentTime.toISOString();\n}`, - }, - ], - }) - } - - if (dsProvider) { - const rawDatasource = await dsProvider - client = await knexClient(rawDatasource) - datasource = await config.createDatasource({ - datasource: rawDatasource, - }) - } - }) - - afterAll(async () => { - setup.afterAll() - if (envCleanup) { - envCleanup() - } - }) - - async function createTable(schema?: TableSchema) { - const table = await config.api.table.save( - tableForDatasource(datasource, { schema }) - ) - return table._id! - } - - async function createView(tableId: string, schema?: ViewV2Schema) { - const view = await config.api.viewV2.create({ - tableId: tableId, - name: generator.guid(), - schema, - }) - return view.id - } - - async function createRows(arr: Record[]) { - // Shuffling to avoid false positives given a fixed order - for (const row of _.shuffle(arr)) { - await config.api.row.save(tableOrViewId, row) - } - rows = await config.api.row.fetch(tableOrViewId) - } - - async function getTable(tableOrViewId: string): Promise
{ - if (docIds.isViewId(tableOrViewId)) { - const view = await config.api.viewV2.get(tableOrViewId) - return await config.api.table.get(view.tableId) - } else { - return await config.api.table.get(tableOrViewId) - } - } - - async function assertTableExists(nameOrTable: string | Table) { - const name = - typeof nameOrTable === "string" ? nameOrTable : nameOrTable.name - expect(await client!.schema.hasTable(name)).toBeTrue() - } - - async function assertTableNumRows( - nameOrTable: string | Table, - numRows: number - ) { - const name = - typeof nameOrTable === "string" ? nameOrTable : nameOrTable.name - const row = await client!.from(name).count() - const count = parseInt(Object.values(row[0])[0] as string) - expect(count).toEqual(numRows) - } - - describe.each([ - ["table", createTable], - [ - "view", - async (schema?: TableSchema) => { - const tableId = await createTable(schema) - const viewId = await createView( - tableId, - Object.keys(schema || {}).reduce( - (viewSchema, fieldName) => { - const field = schema![fieldName] - viewSchema[fieldName] = { - visible: field.visible ?? true, - readonly: false, - } - return viewSchema - }, - {} - ) - ) - return viewId - }, - ], - ])("from %s", (sourceType, createTableOrView) => { - const isView = sourceType === "view" - - if (isView && isLucene) { - // Some tests don't have the expected result in views via lucene, and given that it is getting deprecated, we exclude them from the tests - return - } - - class SearchAssertion { - constructor(private readonly query: SearchRowRequest) {} - - private async performSearch(): Promise> { - if (isInMemory) { - return dataFilters.search(_.cloneDeep(rows), { - ...this.query, - }) - } else { - return config.api.row.search(tableOrViewId, this.query) - } - } - - // We originally used _.isMatch to compare rows, but found that when - // comparing arrays it would return true if the source array was a subset of - // the target array. This would sometimes create false matches. This - // function is a more strict version of _.isMatch that only returns true if - // the source array is an exact match of the target. - // - // _.isMatch("100", "1") also returns true which is not what we want. - private isMatch>(expected: T, found: T) { - if (!expected) { - throw new Error("Expected is undefined") - } - if (!found) { - return false - } - - for (const key of Object.keys(expected)) { - if (Array.isArray(expected[key])) { - if (!Array.isArray(found[key])) { - return false - } - if (expected[key].length !== found[key].length) { - return false - } - if (!_.isMatch(found[key], expected[key])) { - return false - } - } else if (typeof expected[key] === "object") { - if (!this.isMatch(expected[key], found[key])) { - return false - } - } else { - if (expected[key] !== found[key]) { - return false - } - } - } - return true - } - - // This function exists to ensure that the same row is not matched twice. - // When a row gets matched, we make sure to remove it from the list of rows - // we're matching against. - private popRow( - expectedRow: T, - foundRows: T[] - ): NonNullable { - const row = foundRows.find(row => this.isMatch(expectedRow, row)) - if (!row) { - const fields = Object.keys(expectedRow) - // To make the error message more readable, we only include the fields - // that are present in the expected row. - const searchedObjects = foundRows.map(row => _.pick(row, fields)) - throw new Error( - `Failed to find row:\n\n${JSON.stringify( - expectedRow, - null, - 2 - )}\n\nin\n\n${JSON.stringify(searchedObjects, null, 2)}` - ) - } - - foundRows.splice(foundRows.indexOf(row), 1) - return row - } - - // Asserts that the query returns rows matching exactly the set of rows - // passed in. The order of the rows matters. Rows returned in an order - // different to the one passed in will cause the assertion to fail. Extra - // rows returned by the query will also cause the assertion to fail. - async toMatchExactly(expectedRows: any[]) { - const response = await this.performSearch() - const cloned = cloneDeep(response) - const foundRows = response.rows - - // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toHaveLength(expectedRows.length) - // eslint-disable-next-line jest/no-standalone-expect - expect([...foundRows]).toEqual( - expectedRows.map((expectedRow: any) => - expect.objectContaining(this.popRow(expectedRow, foundRows)) - ) - ) - return cloned - } - - // Asserts that the query returns rows matching exactly the set of rows - // passed in. The order of the rows is not important, but extra rows will - // cause the assertion to fail. - async toContainExactly(expectedRows: any[]) { - const response = await this.performSearch() - const cloned = cloneDeep(response) - const foundRows = response.rows - - // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toHaveLength(expectedRows.length) - // eslint-disable-next-line jest/no-standalone-expect - expect([...foundRows]).toEqual( - expect.arrayContaining( - expectedRows.map((expectedRow: any) => - expect.objectContaining(this.popRow(expectedRow, foundRows)) - ) - ) - ) - return cloned - } - - // Asserts that the query returns some property values - this cannot be used - // to check row values, however this shouldn't be important for checking properties - // typing for this has to be any, Jest doesn't expose types for matchers like expect.any(...) - async toMatch(properties: Record) { - const response = await this.performSearch() - const cloned = cloneDeep(response) - const keys = Object.keys(properties) as Array> - for (let key of keys) { - // eslint-disable-next-line jest/no-standalone-expect - expect(response[key]).toBeDefined() - if (properties[key]) { - // eslint-disable-next-line jest/no-standalone-expect - expect(response[key]).toEqual(properties[key]) - } - } - return cloned - } - - // Asserts that the query doesn't return a property, e.g. pagination parameters. - async toNotHaveProperty(properties: (keyof SearchResponse)[]) { - const response = await this.performSearch() - const cloned = cloneDeep(response) - for (let property of properties) { - // eslint-disable-next-line jest/no-standalone-expect - expect(response[property]).toBeUndefined() - } - return cloned - } - - // Asserts that the query returns rows matching the set of rows passed in. - // The order of the rows is not important. Extra rows will not cause the - // assertion to fail. - async toContain(expectedRows: any[]) { - const response = await this.performSearch() - const cloned = cloneDeep(response) - const foundRows = response.rows - - // eslint-disable-next-line jest/no-standalone-expect - expect([...foundRows]).toEqual( - expect.arrayContaining( - expectedRows.map((expectedRow: any) => - expect.objectContaining(this.popRow(expectedRow, foundRows)) - ) - ) - ) - return cloned - } - - async toFindNothing() { - await this.toContainExactly([]) - } - - async toHaveLength(length: number) { - const { rows: foundRows } = await this.performSearch() - - // eslint-disable-next-line jest/no-standalone-expect - expect(foundRows).toHaveLength(length) - } - } - - function expectSearch(query: SearchRowRequest) { - return new SearchAssertion(query) - } - - function expectQuery(query: SearchFilters) { - return expectSearch({ query }) - } - - describe("boolean", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - isTrue: { name: "isTrue", type: FieldType.BOOLEAN }, - }) - await createRows([{ isTrue: true }, { isTrue: false }]) - }) - - describe("equal", () => { - it("successfully finds true row", async () => { - await expectQuery({ equal: { isTrue: true } }).toMatchExactly([ - { isTrue: true }, - ]) - }) - - it("successfully finds false row", async () => { - await expectQuery({ equal: { isTrue: false } }).toMatchExactly([ - { isTrue: false }, - ]) - }) - }) - - describe("notEqual", () => { - it("successfully finds false row", async () => { - await expectQuery({ notEqual: { isTrue: true } }).toContainExactly([ - { isTrue: false }, - ]) - }) - - it("successfully finds true row", async () => { - await expectQuery({ notEqual: { isTrue: false } }).toContainExactly([ - { isTrue: true }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds true row", async () => { - await expectQuery({ oneOf: { isTrue: [true] } }).toContainExactly([ - { isTrue: true }, - ]) - }) - - it("successfully finds false row", async () => { - await expectQuery({ oneOf: { isTrue: [false] } }).toContainExactly([ - { isTrue: false }, - ]) - }) - }) - - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "isTrue", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ isTrue: false }, { isTrue: true }]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "isTrue", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ isTrue: true }, { isTrue: false }]) - }) - }) - }) - - !isInMemory && - describe("bindings", () => { - let globalUsers: any = [] - - const serverTime = new Date() - - // In MariaDB and MySQL we only store dates to second precision, so we need - // to remove milliseconds from the server time to ensure searches work as - // expected. - serverTime.setMilliseconds(0) - - const future = new Date(serverTime.getTime() + 1000 * 60 * 60 * 24 * 30) - - const rows = (currentUser: User) => { - return [ - { name: "foo", appointment: "1982-01-05T00:00:00.000Z" }, - { name: "bar", appointment: "1995-05-06T00:00:00.000Z" }, - { name: currentUser.firstName, appointment: future.toISOString() }, - { name: "serverDate", appointment: serverTime.toISOString() }, - { - name: "single user, session user", - single_user: currentUser, - }, - { - name: "single user", - single_user: globalUsers[0], - }, - { - name: "deprecated single user, session user", - deprecated_single_user: [currentUser], - }, - { - name: "deprecated single user", - deprecated_single_user: [globalUsers[0]], - }, - { - name: "multi user", - multi_user: globalUsers, - }, - { - name: "multi user with session user", - multi_user: [...globalUsers, currentUser], - }, - { - name: "deprecated multi user", - deprecated_multi_user: globalUsers, - }, - { - name: "deprecated multi user with session user", - deprecated_multi_user: [...globalUsers, currentUser], - }, - ] - } - - beforeAll(async () => { - // Set up some global users - globalUsers = await Promise.all( - Array(2) - .fill(0) - .map(async () => { - const globalUser = await config.globalUser() - const userMedataId = globalUser._id - ? dbCore.generateUserMetadataID(globalUser._id) - : null - return { - _id: globalUser._id, - _meta: userMedataId, - } - }) - ) - - tableOrViewId = await createTableOrView({ - name: { name: "name", type: FieldType.STRING }, - appointment: { name: "appointment", type: FieldType.DATETIME }, - single_user: { - name: "single_user", - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - }, - deprecated_single_user: { - name: "deprecated_single_user", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - }, - multi_user: { - name: "multi_user", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - constraints: { - type: "array", - }, - }, - deprecated_multi_user: { - name: "deprecated_multi_user", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USERS, - constraints: { - type: "array", - }, - }, - }) - await createRows(rows(config.getUser())) - }) - - // !! Current User is auto generated per run - it("should return all rows matching the session user firstname", async () => { - await expectQuery({ - equal: { name: "{{ [user].firstName }}" }, - }).toContainExactly([ - { - name: config.getUser().firstName, - appointment: future.toISOString(), - }, - ]) - }) - - !isLucene && - it("should return all rows matching the session user firstname when logical operator used", async () => { - await expectQuery({ - $and: { - conditions: [{ equal: { name: "{{ [user].firstName }}" } }], - }, - }).toContainExactly([ - { - name: config.getUser().firstName, - appointment: future.toISOString(), - }, - ]) - }) - - it("should parse the date binding and return all rows after the resolved value", async () => { - await tk.withFreeze(serverTime, async () => { - await expectQuery({ - range: { - appointment: { - low: "{{ [now] }}", - high: "9999-00-00T00:00:00.000Z", - }, - }, - }).toContainExactly([ - { - name: config.getUser().firstName, - appointment: future.toISOString(), - }, - { name: "serverDate", appointment: serverTime.toISOString() }, - ]) - }) - }) - - it("should parse the date binding and return all rows before the resolved value", async () => { - await expectQuery({ - range: { - appointment: { - low: "0000-00-00T00:00:00.000Z", - high: "{{ [now] }}", - }, - }, - }).toContainExactly([ - { name: "foo", appointment: "1982-01-05T00:00:00.000Z" }, - { name: "bar", appointment: "1995-05-06T00:00:00.000Z" }, - { name: "serverDate", appointment: serverTime.toISOString() }, - ]) - }) - - it("should parse the encoded js snippet. Return rows with appointments up to 1 week in the past", async () => { - const jsBinding = "return snippets.WeeksAgo();" - const encodedBinding = encodeJSBinding(jsBinding) - - await expectQuery({ - range: { - appointment: { - low: "0000-00-00T00:00:00.000Z", - high: encodedBinding, - }, - }, - }).toContainExactly([ - { name: "foo", appointment: "1982-01-05T00:00:00.000Z" }, - { name: "bar", appointment: "1995-05-06T00:00:00.000Z" }, - ]) - }) - - it("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => { - const jsBinding = `const currentTime = new Date(${Date.now()})\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();` - const encodedBinding = encodeJSBinding(jsBinding) - - await expectQuery({ - range: { - appointment: { - low: "0000-00-00T00:00:00.000Z", - high: encodedBinding, - }, - }, - }).toContainExactly([ - { name: "foo", appointment: "1982-01-05T00:00:00.000Z" }, - { name: "bar", appointment: "1995-05-06T00:00:00.000Z" }, - ]) - }) - - it("should match a single user row by the session user id", async () => { - await expectQuery({ - equal: { single_user: "{{ [user]._id }}" }, - }).toContainExactly([ - { - name: "single user, session user", - single_user: { _id: config.getUser()._id }, - }, - ]) - }) - - it("should match a deprecated single user row by the session user id", async () => { - await expectQuery({ - equal: { deprecated_single_user: "{{ [user]._id }}" }, - }).toContainExactly([ - { - name: "deprecated single user, session user", - deprecated_single_user: [{ _id: config.getUser()._id }], - }, - ]) - }) - - it("should match the session user id in a multi user field", async () => { - const allUsers = [...globalUsers, config.getUser()].map( - (user: any) => { - return { _id: user._id } - } - ) - - await expectQuery({ - contains: { multi_user: ["{{ [user]._id }}"] }, - }).toContainExactly([ - { - name: "multi user with session user", - multi_user: allUsers, - }, - ]) - }) - - it("should match the session user id in a deprecated multi user field", async () => { - const allUsers = [...globalUsers, config.getUser()].map( - (user: any) => { - return { _id: user._id } - } - ) - - await expectQuery({ - contains: { deprecated_multi_user: ["{{ [user]._id }}"] }, - }).toContainExactly([ - { - name: "deprecated multi user with session user", - deprecated_multi_user: allUsers, - }, - ]) - }) - - it("should not match the session user id in a multi user field", async () => { - await expectQuery({ - notContains: { multi_user: ["{{ [user]._id }}"] }, - notEmpty: { multi_user: true }, - }).toContainExactly([ - { - name: "multi user", - multi_user: globalUsers.map((user: any) => { - return { _id: user._id } - }), - }, - ]) - }) - - it("should not match the session user id in a deprecated multi user field", async () => { - await expectQuery({ - notContains: { deprecated_multi_user: ["{{ [user]._id }}"] }, - notEmpty: { deprecated_multi_user: true }, - }).toContainExactly([ - { - name: "deprecated multi user", - deprecated_multi_user: globalUsers.map((user: any) => { - return { _id: user._id } - }), - }, - ]) - }) - - it("should match the session user id and a user table row id using helpers, user binding and a static user id.", async () => { - await expectQuery({ - oneOf: { - single_user: [ - "{{ default [user]._id '_empty_' }}", - globalUsers[0]._id, - ], - }, - }).toContainExactly([ - { - name: "single user, session user", - single_user: { _id: config.getUser()._id }, - }, - { - name: "single user", - single_user: { _id: globalUsers[0]._id }, - }, - ]) - }) - - it("should match the session user id and a user table row id using helpers, user binding and a static user id. (deprecated single user)", async () => { - await expectQuery({ - oneOf: { - deprecated_single_user: [ - "{{ default [user]._id '_empty_' }}", - globalUsers[0]._id, - ], - }, - }).toContainExactly([ - { - name: "deprecated single user, session user", - deprecated_single_user: [{ _id: config.getUser()._id }], - }, - { - name: "deprecated single user", - deprecated_single_user: [{ _id: globalUsers[0]._id }], - }, - ]) - }) - - it("should resolve 'default' helper to '_empty_' when binding resolves to nothing", async () => { - await expectQuery({ - oneOf: { - single_user: [ - "{{ default [user]._idx '_empty_' }}", - globalUsers[0]._id, - ], - }, - }).toContainExactly([ - { - name: "single user", - single_user: { _id: globalUsers[0]._id }, - }, - ]) - }) - - it("should resolve 'default' helper to '_empty_' when binding resolves to nothing (deprecated single user)", async () => { - await expectQuery({ - oneOf: { - deprecated_single_user: [ - "{{ default [user]._idx '_empty_' }}", - globalUsers[0]._id, - ], - }, - }).toContainExactly([ - { - name: "deprecated single user", - deprecated_single_user: [{ _id: globalUsers[0]._id }], - }, - ]) - }) - }) - - const stringTypes = [FieldType.STRING, FieldType.LONGFORM] as const - describe.each(stringTypes)("%s", type => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - name: { name: "name", type }, - }) - await createRows([{ name: "foo" }, { name: "bar" }]) - }) - - describe("misc", () => { - it("should return all if no query is passed", async () => { - await expectSearch({} as RowSearchParams).toContainExactly([ - { name: "foo" }, - { name: "bar" }, - ]) - }) - - it("should return all if empty query is passed", async () => { - await expectQuery({}).toContainExactly([ - { name: "foo" }, - { name: "bar" }, - ]) - }) - - it("should return all if onEmptyFilter is RETURN_ALL", async () => { - await expectQuery({ - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - - // onEmptyFilter cannot be sent to view searches - !isView && - it("should return nothing if onEmptyFilter is RETURN_NONE", async () => { - await expectQuery({ - onEmptyFilter: EmptyFilterOption.RETURN_NONE, - }).toFindNothing() - }) - - it("should respect limit", async () => { - await expectSearch({ - limit: 1, - paginate: true, - query: {}, - }).toHaveLength(1) - }) - }) - - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ equal: { name: "foo" } }).toContainExactly([ - { name: "foo" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { name: "none" } }).toFindNothing() - }) - - it("works as an or condition", async () => { - await expectQuery({ - allOr: true, - equal: { name: "foo" }, - oneOf: { name: ["bar"] }, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - - it("can have multiple values for same column", async () => { - await expectQuery({ - allOr: true, - equal: { "1:name": "foo", "2:name": "bar" }, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - }) - - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { name: "foo" } }).toContainExactly([ - { name: "bar" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ notEqual: { name: "bar" } }).toContainExactly([ - { name: "foo" }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ oneOf: { name: ["foo"] } }).toContainExactly([ - { name: "foo" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ oneOf: { name: ["none"] } }).toFindNothing() - }) - - it("can have multiple values for same column", async () => { - await expectQuery({ - oneOf: { - name: ["foo", "bar"], - }, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - - it("splits comma separated strings", async () => { - await expectQuery({ - oneOf: { - // @ts-ignore - name: "foo,bar", - }, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - - it("trims whitespace", async () => { - await expectQuery({ - oneOf: { - // @ts-ignore - name: "foo, bar", - }, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - - it("empty arrays returns all when onEmptyFilter is set to return 'all'", async () => { - await expectQuery({ - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - oneOf: { name: [] }, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - - // onEmptyFilter cannot be sent to view searches - !isView && - it("empty arrays returns all when onEmptyFilter is set to return 'none'", async () => { - await expectQuery({ - onEmptyFilter: EmptyFilterOption.RETURN_NONE, - oneOf: { name: [] }, - }).toContainExactly([]) - }) - }) - - describe("fuzzy", () => { - it("successfully finds a row", async () => { - await expectQuery({ fuzzy: { name: "oo" } }).toContainExactly([ - { name: "foo" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ fuzzy: { name: "none" } }).toFindNothing() - }) - }) - - describe("string", () => { - it("successfully finds a row", async () => { - await expectQuery({ string: { name: "fo" } }).toContainExactly([ - { name: "foo" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ string: { name: "none" } }).toFindNothing() - }) - - it("is case-insensitive", async () => { - await expectQuery({ string: { name: "FO" } }).toContainExactly([ - { name: "foo" }, - ]) - }) - }) - - describe("range", () => { - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { name: { low: "a", high: "z" } }, - }).toContainExactly([{ name: "bar" }, { name: "foo" }]) - }) - - it("successfully finds a row with a high bound", async () => { - await expectQuery({ - range: { name: { low: "a", high: "c" } }, - }).toContainExactly([{ name: "bar" }]) - }) - - it("successfully finds a row with a low bound", async () => { - await expectQuery({ - range: { name: { low: "f", high: "z" } }, - }).toContainExactly([{ name: "foo" }]) - }) - - it("successfully finds no rows", async () => { - await expectQuery({ - range: { name: { low: "g", high: "h" } }, - }).toFindNothing() - }) - - !isLucene && - it("ignores low if it's an empty object", async () => { - await expectQuery({ - // @ts-ignore - range: { name: { low: {}, high: "z" } }, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - - !isLucene && - it("ignores high if it's an empty object", async () => { - await expectQuery({ - // @ts-ignore - range: { name: { low: "a", high: {} } }, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - }) - - describe("empty", () => { - it("finds no empty rows", async () => { - await expectQuery({ empty: { name: null } }).toFindNothing() - }) - - it("should not be affected by when filter empty behaviour", async () => { - await expectQuery({ - empty: { name: null }, - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - }).toFindNothing() - }) - }) - - describe("notEmpty", () => { - it("finds all non-empty rows", async () => { - await expectQuery({ notEmpty: { name: null } }).toContainExactly([ - { name: "foo" }, - { name: "bar" }, - ]) - }) - - it("should not be affected by when filter empty behaviour", async () => { - await expectQuery({ - notEmpty: { name: null }, - onEmptyFilter: EmptyFilterOption.RETURN_NONE, - }).toContainExactly([{ name: "foo" }, { name: "bar" }]) - }) - }) - - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "name", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ name: "bar" }, { name: "foo" }]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "name", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ name: "foo" }, { name: "bar" }]) - }) - - describe("sortType STRING", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "name", - sortType: SortType.STRING, - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ name: "bar" }, { name: "foo" }]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "name", - sortType: SortType.STRING, - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ name: "foo" }, { name: "bar" }]) - }) - }) - - !isInternal && - !isInMemory && - // This test was added because we automatically add in a sort by the - // primary key, and we used to do this unconditionally which caused - // problems because it was possible for the primary key to appear twice - // in the resulting SQL ORDER BY clause, resulting in an SQL error. - // We now check first to make sure that the primary key isn't already - // in the sort before adding it. - describe("sort on primary key", () => { - beforeAll(async () => { - const tableName = structures.uuid().substring(0, 10) - await client!.schema.createTable(tableName, t => { - t.string("name").primary() - }) - const resp = await config.api.datasource.fetchSchema({ - datasourceId: datasource!._id!, - }) - - tableOrViewId = resp.datasource.entities![tableName]._id! - - await createRows([{ name: "foo" }, { name: "bar" }]) - }) - - it("should be able to sort by a primary key column ascending", async () => - expectSearch({ - query: {}, - sort: "name", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ name: "bar" }, { name: "foo" }])) - - it("should be able to sort by a primary key column descending", async () => - expectSearch({ - query: {}, - sort: "name", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) - }) - }) - }) - - describe("numbers", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - age: { name: "age", type: FieldType.NUMBER }, - }) - await createRows([{ age: 1 }, { age: 10 }]) - }) - - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ equal: { age: 1 } }).toContainExactly([ - { age: 1 }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { age: 2 } }).toFindNothing() - }) - }) - - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { age: 1 } }).toContainExactly([ - { age: 10 }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ notEqual: { age: 10 } }).toContainExactly([ - { age: 1 }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ oneOf: { age: [1] } }).toContainExactly([ - { age: 1 }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ oneOf: { age: [2] } }).toFindNothing() - }) - - // I couldn't find a way to make this work in Lucene and given that - // we're getting rid of Lucene soon I wasn't inclined to spend time on - // it. - !isLucene && - it("can convert from a string", async () => { - await expectQuery({ - oneOf: { - // @ts-ignore - age: "1", - }, - }).toContainExactly([{ age: 1 }]) - }) - - // I couldn't find a way to make this work in Lucene and given that - // we're getting rid of Lucene soon I wasn't inclined to spend time on - // it. - !isLucene && - it("can find multiple values for same column", async () => { - await expectQuery({ - oneOf: { - // @ts-ignore - age: "1,10", - }, - }).toContainExactly([{ age: 1 }, { age: 10 }]) - }) - }) - - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { age: { low: 1, high: 5 } }, - }).toContainExactly([{ age: 1 }]) - }) - - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { age: { low: 1, high: 10 } }, - }).toContainExactly([{ age: 1 }, { age: 10 }]) - }) - - it("successfully finds a row with a high bound", async () => { - await expectQuery({ - range: { age: { low: 5, high: 10 } }, - }).toContainExactly([{ age: 10 }]) - }) - - it("successfully finds no rows", async () => { - await expectQuery({ - range: { age: { low: 5, high: 9 } }, - }).toFindNothing() - }) - - it("greater than equal to", async () => { - await expectQuery({ - range: { - age: { low: 10, high: Number.MAX_SAFE_INTEGER }, - }, - }).toContainExactly([{ age: 10 }]) - }) - - it("greater than", async () => { - await expectQuery({ - range: { - age: { low: 5, high: Number.MAX_SAFE_INTEGER }, - }, - }).toContainExactly([{ age: 10 }]) - }) - - it("less than equal to", async () => { - await expectQuery({ - range: { - age: { high: 1, low: Number.MIN_SAFE_INTEGER }, - }, - }).toContainExactly([{ age: 1 }]) - }) - - it("less than", async () => { - await expectQuery({ - range: { - age: { high: 5, low: Number.MIN_SAFE_INTEGER }, - }, - }).toContainExactly([{ age: 1 }]) - }) - }) - - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "age", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ age: 1 }, { age: 10 }]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "age", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ age: 10 }, { age: 1 }]) - }) - }) - - describe("sortType NUMBER", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "age", - sortType: SortType.NUMBER, - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ age: 1 }, { age: 10 }]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "age", - sortType: SortType.NUMBER, - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ age: 10 }, { age: 1 }]) - }) - }) - }) - - describe("dates", () => { - const JAN_1ST = "2020-01-01T00:00:00.000Z" - const JAN_2ND = "2020-01-02T00:00:00.000Z" - const JAN_5TH = "2020-01-05T00:00:00.000Z" - const JAN_9TH = "2020-01-09T00:00:00.000Z" - const JAN_10TH = "2020-01-10T00:00:00.000Z" - - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - dob: { name: "dob", type: FieldType.DATETIME }, - }) - - await createRows([{ dob: JAN_1ST }, { dob: JAN_10TH }]) - }) - - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ equal: { dob: JAN_1ST } }).toContainExactly([ - { dob: JAN_1ST }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing() - }) - }) - - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { dob: JAN_1ST } }).toContainExactly([ - { dob: JAN_10TH }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ notEqual: { dob: JAN_10TH } }).toContainExactly([ - { dob: JAN_1ST }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContainExactly([ - { dob: JAN_1ST }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ oneOf: { dob: [JAN_2ND] } }).toFindNothing() - }) - }) - - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { dob: { low: JAN_1ST, high: JAN_5TH } }, - }).toContainExactly([{ dob: JAN_1ST }]) - }) - - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { dob: { low: JAN_1ST, high: JAN_10TH } }, - }).toContainExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]) - }) - - it("successfully finds a row with a high bound", async () => { - await expectQuery({ - range: { dob: { low: JAN_5TH, high: JAN_10TH } }, - }).toContainExactly([{ dob: JAN_10TH }]) - }) - - it("successfully finds no rows", async () => { - await expectQuery({ - range: { dob: { low: JAN_5TH, high: JAN_9TH } }, - }).toFindNothing() - }) - - it("greater than equal to", async () => { - await expectQuery({ - range: { - dob: { low: JAN_10TH, high: MAX_VALID_DATE.toISOString() }, - }, - }).toContainExactly([{ dob: JAN_10TH }]) - }) - - it("greater than", async () => { - await expectQuery({ - range: { - dob: { low: JAN_5TH, high: MAX_VALID_DATE.toISOString() }, - }, - }).toContainExactly([{ dob: JAN_10TH }]) - }) - - it("less than equal to", async () => { - await expectQuery({ - range: { - dob: { high: JAN_1ST, low: MIN_VALID_DATE.toISOString() }, - }, - }).toContainExactly([{ dob: JAN_1ST }]) - }) - - it("less than", async () => { - await expectQuery({ - range: { - dob: { high: JAN_5TH, low: MIN_VALID_DATE.toISOString() }, - }, - }).toContainExactly([{ dob: JAN_1ST }]) - }) - }) - - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "dob", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "dob", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }]) - }) - - describe("sortType STRING", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "dob", - sortType: SortType.STRING, - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "dob", - sortType: SortType.STRING, - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }]) - }) - }) - }) - }) - - !isInternal && - describe("datetime - time only", () => { - const T_1000 = "10:00:00" - const T_1045 = "10:45:00" - const T_1200 = "12:00:00" - const T_1530 = "15:30:00" - const T_0000 = "00:00:00" - - const UNEXISTING_TIME = "10:01:00" - - const NULL_TIME__ID = `null_time__id` - - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - timeid: { name: "timeid", type: FieldType.STRING }, - time: { name: "time", type: FieldType.DATETIME, timeOnly: true }, - }) - - await createRows([ - { timeid: NULL_TIME__ID, time: null }, - { time: T_1000 }, - { time: T_1045 }, - { time: T_1200 }, - { time: T_1530 }, - { time: T_0000 }, - ]) - }) - - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ equal: { time: T_1000 } }).toContainExactly([ - { time: "10:00:00" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - equal: { time: UNEXISTING_TIME }, - }).toFindNothing() - }) - }) - - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { time: T_1000 } }).toContainExactly([ - { timeid: NULL_TIME__ID }, - { time: "10:45:00" }, - { time: "12:00:00" }, - { time: "15:30:00" }, - { time: "00:00:00" }, - ]) - }) - - it("return all when requesting non-existing", async () => { - await expectQuery({ - notEqual: { time: UNEXISTING_TIME }, - }).toContainExactly([ - { timeid: NULL_TIME__ID }, - { time: "10:00:00" }, - { time: "10:45:00" }, - { time: "12:00:00" }, - { time: "15:30:00" }, - { time: "00:00:00" }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ oneOf: { time: [T_1000] } }).toContainExactly([ - { time: "10:00:00" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - oneOf: { time: [UNEXISTING_TIME] }, - }).toFindNothing() - }) - }) - - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { time: { low: T_1045, high: T_1045 } }, - }).toContainExactly([{ time: "10:45:00" }]) - }) - - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { time: { low: T_1045, high: T_1530 } }, - }).toContainExactly([ - { time: "10:45:00" }, - { time: "12:00:00" }, - { time: "15:30:00" }, - ]) - }) - - it("successfully finds no rows", async () => { - await expectQuery({ - range: { time: { low: UNEXISTING_TIME, high: UNEXISTING_TIME } }, - }).toFindNothing() - }) - }) - - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "time", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([ - { timeid: NULL_TIME__ID }, - { time: "00:00:00" }, - { time: "10:00:00" }, - { time: "10:45:00" }, - { time: "12:00:00" }, - { time: "15:30:00" }, - ]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "time", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([ - { time: "15:30:00" }, - { time: "12:00:00" }, - { time: "10:45:00" }, - { time: "10:00:00" }, - { time: "00:00:00" }, - { timeid: NULL_TIME__ID }, - ]) - }) - - describe("sortType STRING", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "time", - sortType: SortType.STRING, - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([ - { timeid: NULL_TIME__ID }, - { time: "00:00:00" }, - { time: "10:00:00" }, - { time: "10:45:00" }, - { time: "12:00:00" }, - { time: "15:30:00" }, - ]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "time", - sortType: SortType.STRING, - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([ - { time: "15:30:00" }, - { time: "12:00:00" }, - { time: "10:45:00" }, - { time: "10:00:00" }, - { time: "00:00:00" }, - { timeid: NULL_TIME__ID }, - ]) - }) - }) - }) - }) - - isSqs && - describe("AI Column", () => { - const UNEXISTING_AI_COLUMN = "Real LLM Response" - - beforeAll(async () => { - mocks.licenses.useBudibaseAI() - mocks.licenses.useAICustomConfigs() - - tableOrViewId = await createTableOrView({ - product: { name: "product", type: FieldType.STRING }, - ai: { - name: "AI", - type: FieldType.AI, - operation: AIOperationEnum.PROMPT, - prompt: "Translate '{{ product }}' into German", - }, - }) - - await createRows([{ product: "Big Mac" }, { product: "McCrispy" }]) - }) - - describe("equal", () => { - it("successfully finds rows based on AI column", async () => { - await expectQuery({ - equal: { ai: "Mock LLM Response" }, - }).toContainExactly([ - { product: "Big Mac" }, - { product: "McCrispy" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - equal: { ai: UNEXISTING_AI_COLUMN }, - }).toFindNothing() - }) - }) - - describe("notEqual", () => { - it("Returns nothing when searching notEqual on the mock AI response", async () => { - await expectQuery({ - notEqual: { ai: "Mock LLM Response" }, - }).toContainExactly([]) - }) - - it("return all when requesting non-existing response", async () => { - await expectQuery({ - notEqual: { ai: "Real LLM Response" }, - }).toContainExactly([ - { product: "Big Mac" }, - { product: "McCrispy" }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ - oneOf: { ai: ["Mock LLM Response", "Other LLM Response"] }, - }).toContainExactly([ - { product: "Big Mac" }, - { product: "McCrispy" }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - oneOf: { ai: ["Whopper"] }, - }).toFindNothing() - }) - }) - }) - - describe("arrays", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - numbers: { - name: "numbers", - type: FieldType.ARRAY, +const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] }) + +if (descriptions.length) { + describe.each(descriptions)( + "search ($dbName)", + ({ config, dsProvider, isInternal, isOracle, isSql }) => { + let datasource: Datasource | undefined + let client: Knex | undefined + let tableOrViewId: string + let rows: Row[] + + async function basicRelationshipTables(type: RelationshipType) { + const relatedTable = await createTable({ + name: { name: "name", type: FieldType.STRING }, + }) + const tableId = await createTable({ + name: { name: "name", type: FieldType.STRING }, + //@ts-ignore - API accepts this structure, will build out rest of definition + productCat: { + type: FieldType.LINK, + relationshipType: type, + name: "productCat", + fieldName: "product", + tableId: relatedTable, constraints: { - type: JsonFieldSubType.ARRAY, - inclusion: ["one", "two", "three"], + type: "array", }, }, }) - await createRows([{ numbers: ["one", "two"] }, { numbers: ["three"] }]) - }) - - describe("contains", () => { - it("successfully finds a row", async () => { - await expectQuery({ - contains: { numbers: ["one"] }, - }).toContainExactly([{ numbers: ["one", "two"] }]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ contains: { numbers: ["none"] } }).toFindNothing() - }) - - it("fails to find row containing all", async () => { - await expectQuery({ - contains: { numbers: ["one", "two", "three"] }, - }).toFindNothing() - }) - - it("finds all with empty list", async () => { - await expectQuery({ contains: { numbers: [] } }).toContainExactly([ - { numbers: ["one", "two"] }, - { numbers: ["three"] }, - ]) - }) - }) - - describe("notContains", () => { - it("successfully finds a row", async () => { - await expectQuery({ - notContains: { numbers: ["one"] }, - }).toContainExactly([{ numbers: ["three"] }]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - notContains: { numbers: ["one", "two", "three"] }, - }).toContainExactly([ - { numbers: ["one", "two"] }, - { numbers: ["three"] }, - ]) - }) - - // Not sure if this is correct behaviour but changing it would be a - // breaking change. - it("finds all with empty list", async () => { - await expectQuery({ notContains: { numbers: [] } }).toContainExactly([ - { numbers: ["one", "two"] }, - { numbers: ["three"] }, - ]) - }) - }) - - describe("containsAny", () => { - it("successfully finds rows", async () => { - await expectQuery({ - containsAny: { numbers: ["one", "two", "three"] }, - }).toContainExactly([ - { numbers: ["one", "two"] }, - { numbers: ["three"] }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - containsAny: { numbers: ["none"] }, - }).toFindNothing() - }) - - it("finds all with empty list", async () => { - await expectQuery({ containsAny: { numbers: [] } }).toContainExactly([ - { numbers: ["one", "two"] }, - { numbers: ["three"] }, - ]) - }) - }) - }) - - describe("bigints", () => { - const SMALL = "1" - const MEDIUM = "10000000" - - // Our bigints are int64s in most datasources. - let BIG = "9223372036854775807" + return { + relatedTable: await config.api.table.get(relatedTable), + tableId, + } + } beforeAll(async () => { - tableOrViewId = await createTableOrView({ - num: { name: "num", type: FieldType.BIGINT }, - }) - await createRows([{ num: SMALL }, { num: MEDIUM }, { num: BIG }]) - }) + const ds = await dsProvider() + datasource = ds.datasource + client = ds.client - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ equal: { num: SMALL } }).toContainExactly([ - { num: SMALL }, - ]) - }) - - it("successfully finds a big value", async () => { - await expectQuery({ equal: { num: BIG } }).toContainExactly([ - { num: BIG }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { num: "2" } }).toFindNothing() - }) - }) - - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { num: SMALL } }).toContainExactly([ - { num: MEDIUM }, - { num: BIG }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ notEqual: { num: 10 } }).toContainExactly([ - { num: SMALL }, - { num: MEDIUM }, - { num: BIG }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ oneOf: { num: [SMALL] } }).toContainExactly([ - { num: SMALL }, - ]) - }) - - it("successfully finds all rows", async () => { - await expectQuery({ - oneOf: { num: [SMALL, MEDIUM, BIG] }, - }).toContainExactly([{ num: SMALL }, { num: MEDIUM }, { num: BIG }]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ oneOf: { num: [2] } }).toFindNothing() - }) - }) - - // Range searches against bigints don't seem to work at all in Lucene, and I - // couldn't figure out why. Given that we're replacing Lucene with SQS, - // we've decided not to spend time on it. - !isLucene && - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { num: { low: SMALL, high: "5" } }, - }).toContainExactly([{ num: SMALL }]) - }) - - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { num: { low: SMALL, high: MEDIUM } }, - }).toContainExactly([{ num: SMALL }, { num: MEDIUM }]) - }) - - it("successfully finds a row with a high bound", async () => { - await expectQuery({ - range: { num: { low: MEDIUM, high: BIG } }, - }).toContainExactly([{ num: MEDIUM }, { num: BIG }]) - }) - - it("successfully finds no rows", async () => { - await expectQuery({ - range: { num: { low: "5", high: "5" } }, - }).toFindNothing() - }) - - it("can search using just a low value", async () => { - await expectQuery({ - range: { num: { low: MEDIUM } }, - }).toContainExactly([{ num: MEDIUM }, { num: BIG }]) - }) - - it("can search using just a high value", async () => { - await expectQuery({ - range: { num: { high: MEDIUM } }, - }).toContainExactly([{ num: SMALL }, { num: MEDIUM }]) - }) - }) - }) - - isInternal && - describe("auto", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - auto: { - name: "auto", - type: FieldType.AUTO, - autocolumn: true, - subtype: AutoFieldSubType.AUTO_ID, + config.app = await config.api.application.update(config.getAppId(), { + snippets: [ + { + name: "WeeksAgo", + code: ` + return function (weeks) { + const currentTime = new Date(${Date.now()}); + currentTime.setDate(currentTime.getDate()-(7 * (weeks || 1))); + return currentTime.toISOString(); + } + `, }, - }) - await createRows(new Array(10).fill({})) + ], }) + }) - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ equal: { auto: 1 } }).toContainExactly([ - { auto: 1 }, - ]) - }) + async function createTable(schema?: TableSchema) { + const table = await config.api.table.save( + tableForDatasource(datasource, { schema }) + ) + return table._id! + } - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { auto: 0 } }).toFindNothing() - }) + async function createView(tableId: string, schema?: ViewV2Schema) { + const view = await config.api.viewV2.create({ + tableId: tableId, + name: generator.guid(), + schema, }) + return view.id + } - describe("not equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { auto: 1 } }).toContainExactly([ - { auto: 2 }, - { auto: 3 }, - { auto: 4 }, - { auto: 5 }, - { auto: 6 }, - { auto: 7 }, - { auto: 8 }, - { auto: 9 }, - { auto: 10 }, - ]) - }) + async function createRows(arr: Record[]) { + // Shuffling to avoid false positives given a fixed order + for (const row of _.shuffle(arr)) { + await config.api.row.save(tableOrViewId, row) + } + rows = await config.api.row.fetch(tableOrViewId) + } - it("fails to find nonexistent row", async () => { - await expectQuery({ notEqual: { auto: 0 } }).toContainExactly([ - { auto: 1 }, - { auto: 2 }, - { auto: 3 }, - { auto: 4 }, - { auto: 5 }, - { auto: 6 }, - { auto: 7 }, - { auto: 8 }, - { auto: 9 }, - { auto: 10 }, - ]) - }) - }) + async function getTable(tableOrViewId: string): Promise
{ + if (docIds.isViewId(tableOrViewId)) { + const view = await config.api.viewV2.get(tableOrViewId) + return await config.api.table.get(view.tableId) + } else { + return await config.api.table.get(tableOrViewId) + } + } - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ oneOf: { auto: [1] } }).toContainExactly([ - { auto: 1 }, - ]) - }) + async function assertTableExists(nameOrTable: string | Table) { + const name = + typeof nameOrTable === "string" ? nameOrTable : nameOrTable.name + expect(await client!.schema.hasTable(name)).toBeTrue() + } - it("fails to find nonexistent row", async () => { - await expectQuery({ oneOf: { auto: [0] } }).toFindNothing() - }) - }) + async function assertTableNumRows( + nameOrTable: string | Table, + numRows: number + ) { + const name = + typeof nameOrTable === "string" ? nameOrTable : nameOrTable.name + const row = await client!.from(name).count() + const count = parseInt(Object.values(row[0])[0] as string) + expect(count).toEqual(numRows) + } - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { auto: { low: 1, high: 1 } }, - }).toContainExactly([{ auto: 1 }]) - }) + describe.each([true, false])("in-memory: %s", isInMemory => { + // We only run the in-memory tests during the SQS (isInternal) run + if (isInMemory && !isInternal) { + return + } - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { auto: { low: 1, high: 2 } }, - }).toContainExactly([{ auto: 1 }, { auto: 2 }]) - }) + type CreateFn = (schema?: TableSchema) => Promise + let tableOrView: [string, CreateFn][] = [["table", createTable]] - it("successfully finds a row with a high bound", async () => { - await expectQuery({ - range: { auto: { low: 2, high: 2 } }, - }).toContainExactly([{ auto: 2 }]) - }) - - it("successfully finds no rows", async () => { - await expectQuery({ - range: { auto: { low: 0, high: 0 } }, - }).toFindNothing() - }) - - isSqs && - it("can search using just a low value", async () => { - await expectQuery({ - range: { auto: { low: 9 } }, - }).toContainExactly([{ auto: 9 }, { auto: 10 }]) - }) - - isSqs && - it("can search using just a high value", async () => { - await expectQuery({ - range: { auto: { high: 2 } }, - }).toContainExactly([{ auto: 1 }, { auto: 2 }]) - }) - }) - - isSqs && - describe("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "auto", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([ - { auto: 1 }, - { auto: 2 }, - { auto: 3 }, - { auto: 4 }, - { auto: 5 }, - { auto: 6 }, - { auto: 7 }, - { auto: 8 }, - { auto: 9 }, - { auto: 10 }, - ]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "auto", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([ - { auto: 10 }, - { auto: 9 }, - { auto: 8 }, - { auto: 7 }, - { auto: 6 }, - { auto: 5 }, - { auto: 4 }, - { auto: 3 }, - { auto: 2 }, - { auto: 1 }, - ]) - }) - - // This is important for pagination. The order of results must always - // be stable or pagination will break. We don't want the user to need - // to specify an order for pagination to work. - it("is stable without a sort specified", async () => { - let { rows: fullRowList } = await config.api.row.search( - tableOrViewId, - { - tableId: tableOrViewId, - query: {}, - } + if (!isInMemory) { + tableOrView.push([ + "view", + async (schema?: TableSchema) => { + const tableId = await createTable(schema) + const viewId = await createView( + tableId, + Object.keys(schema || {}).reduce( + (viewSchema, fieldName) => { + const field = schema![fieldName] + viewSchema[fieldName] = { + visible: field.visible ?? true, + readonly: false, + } + return viewSchema + }, + {} + ) ) + return viewId + }, + ]) + } - // repeat the search many times to check the first row is always the same - let bookmark: string | number | undefined, - hasNextPage: boolean | undefined = true, - rowCount: number = 0 - do { - const response = await config.api.row.search(tableOrViewId, { - tableId: tableOrViewId, - limit: 1, - paginate: true, - query: {}, - bookmark, + describe.each(tableOrView)( + "from %s", + (sourceType, createTableOrView) => { + const isView = sourceType === "view" + + class SearchAssertion { + constructor(private readonly query: SearchRowRequest) {} + + private async performSearch(): Promise> { + if (isInMemory) { + return dataFilters.search(_.cloneDeep(rows), { + ...this.query, + }) + } else { + return config.api.row.search(tableOrViewId, this.query) + } + } + + // We originally used _.isMatch to compare rows, but found that when + // comparing arrays it would return true if the source array was a subset of + // the target array. This would sometimes create false matches. This + // function is a more strict version of _.isMatch that only returns true if + // the source array is an exact match of the target. + // + // _.isMatch("100", "1") also returns true which is not what we want. + private isMatch>( + expected: T, + found: T + ) { + if (!expected) { + throw new Error("Expected is undefined") + } + if (!found) { + return false + } + + for (const key of Object.keys(expected)) { + if (Array.isArray(expected[key])) { + if (!Array.isArray(found[key])) { + return false + } + if (expected[key].length !== found[key].length) { + return false + } + if (!_.isMatch(found[key], expected[key])) { + return false + } + } else if (typeof expected[key] === "object") { + if (!this.isMatch(expected[key], found[key])) { + return false + } + } else { + if (expected[key] !== found[key]) { + return false + } + } + } + return true + } + + // This function exists to ensure that the same row is not matched twice. + // When a row gets matched, we make sure to remove it from the list of rows + // we're matching against. + private popRow( + expectedRow: T, + foundRows: T[] + ): NonNullable { + const row = foundRows.find(row => + this.isMatch(expectedRow, row) + ) + if (!row) { + const fields = Object.keys(expectedRow) + // To make the error message more readable, we only include the fields + // that are present in the expected row. + const searchedObjects = foundRows.map(row => + _.pick(row, fields) + ) + throw new Error( + `Failed to find row:\n\n${JSON.stringify( + expectedRow, + null, + 2 + )}\n\nin\n\n${JSON.stringify(searchedObjects, null, 2)}` + ) + } + + foundRows.splice(foundRows.indexOf(row), 1) + return row + } + + // Asserts that the query returns rows matching exactly the set of rows + // passed in. The order of the rows matters. Rows returned in an order + // different to the one passed in will cause the assertion to fail. Extra + // rows returned by the query will also cause the assertion to fail. + async toMatchExactly(expectedRows: any[]) { + const response = await this.performSearch() + const cloned = cloneDeep(response) + const foundRows = response.rows + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toHaveLength(expectedRows.length) + // eslint-disable-next-line jest/no-standalone-expect + expect([...foundRows]).toEqual( + expectedRows.map((expectedRow: any) => + expect.objectContaining(this.popRow(expectedRow, foundRows)) + ) + ) + return cloned + } + + // Asserts that the query returns rows matching exactly the set of rows + // passed in. The order of the rows is not important, but extra rows will + // cause the assertion to fail. + async toContainExactly(expectedRows: any[]) { + const response = await this.performSearch() + const cloned = cloneDeep(response) + const foundRows = response.rows + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toHaveLength(expectedRows.length) + // eslint-disable-next-line jest/no-standalone-expect + expect([...foundRows]).toEqual( + expect.arrayContaining( + expectedRows.map((expectedRow: any) => + expect.objectContaining( + this.popRow(expectedRow, foundRows) + ) + ) + ) + ) + return cloned + } + + // Asserts that the query returns some property values - this cannot be used + // to check row values, however this shouldn't be important for checking properties + // typing for this has to be any, Jest doesn't expose types for matchers like expect.any(...) + async toMatch(properties: Record) { + const response = await this.performSearch() + const cloned = cloneDeep(response) + const keys = Object.keys(properties) as Array< + keyof SearchResponse + > + for (let key of keys) { + // eslint-disable-next-line jest/no-standalone-expect + expect(response[key]).toBeDefined() + if (properties[key]) { + // eslint-disable-next-line jest/no-standalone-expect + expect(response[key]).toEqual(properties[key]) + } + } + return cloned + } + + // Asserts that the query doesn't return a property, e.g. pagination parameters. + async toNotHaveProperty( + properties: (keyof SearchResponse)[] + ) { + const response = await this.performSearch() + const cloned = cloneDeep(response) + for (let property of properties) { + // eslint-disable-next-line jest/no-standalone-expect + expect(response[property]).toBeUndefined() + } + return cloned + } + + // Asserts that the query returns rows matching the set of rows passed in. + // The order of the rows is not important. Extra rows will not cause the + // assertion to fail. + async toContain(expectedRows: any[]) { + const response = await this.performSearch() + const cloned = cloneDeep(response) + const foundRows = response.rows + + // eslint-disable-next-line jest/no-standalone-expect + expect([...foundRows]).toEqual( + expect.arrayContaining( + expectedRows.map((expectedRow: any) => + expect.objectContaining( + this.popRow(expectedRow, foundRows) + ) + ) + ) + ) + return cloned + } + + async toFindNothing() { + await this.toContainExactly([]) + } + + async toHaveLength(length: number) { + const { rows: foundRows } = await this.performSearch() + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toHaveLength(length) + } + } + + function expectSearch(query: SearchRowRequest) { + return new SearchAssertion(query) + } + + function expectQuery(query: SearchFilters) { + return expectSearch({ query }) + } + + describe("boolean", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + isTrue: { name: "isTrue", type: FieldType.BOOLEAN }, }) - bookmark = response.bookmark - hasNextPage = response.hasNextPage - expect(response.rows.length).toEqual(1) - const foundRow = response.rows[0] - expect(foundRow).toEqual(fullRowList[rowCount++]) - } while (hasNextPage) - }) - }) - - describe("pagination", () => { - it("should paginate through all rows", async () => { - // @ts-ignore - let bookmark: string | number = undefined - let rows: Row[] = [] - - // eslint-disable-next-line no-constant-condition - while (true) { - const response = await config.api.row.search(tableOrViewId, { - tableId: tableOrViewId, - limit: 3, - query: {}, - bookmark, - paginate: true, + await createRows([{ isTrue: true }, { isTrue: false }]) }) - rows.push(...response.rows) + describe("equal", () => { + it("successfully finds true row", async () => { + await expectQuery({ equal: { isTrue: true } }).toMatchExactly( + [{ isTrue: true }] + ) + }) - if (!response.bookmark || !response.hasNextPage) { - break - } - bookmark = response.bookmark - } + it("successfully finds false row", async () => { + await expectQuery({ + equal: { isTrue: false }, + }).toMatchExactly([{ isTrue: false }]) + }) + }) - const autoValues = rows.map(row => row.auto).sort((a, b) => a - b) - expect(autoValues).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - }) - }) - }) + describe("notEqual", () => { + it("successfully finds false row", async () => { + await expectQuery({ + notEqual: { isTrue: true }, + }).toContainExactly([{ isTrue: false }]) + }) - describe("field name 1:name", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - "1:name": { name: "1:name", type: FieldType.STRING }, - }) - await createRows([{ "1:name": "bar" }, { "1:name": "foo" }]) - }) + it("successfully finds true row", async () => { + await expectQuery({ + notEqual: { isTrue: false }, + }).toContainExactly([{ isTrue: true }]) + }) + }) - it("successfully finds a row", async () => { - await expectQuery({ equal: { "1:1:name": "bar" } }).toContainExactly([ - { "1:name": "bar" }, - ]) - }) + describe("oneOf", () => { + it("successfully finds true row", async () => { + await expectQuery({ + oneOf: { isTrue: [true] }, + }).toContainExactly([{ isTrue: true }]) + }) - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { "1:1:name": "none" } }).toFindNothing() - }) - }) + it("successfully finds false row", async () => { + await expectQuery({ + oneOf: { isTrue: [false] }, + }).toContainExactly([{ isTrue: false }]) + }) + }) - isSql && - describe("related formulas", () => { - beforeAll(async () => { - const arrayTable = await createTable({ - name: { name: "name", type: FieldType.STRING }, - array: { - name: "array", - type: FieldType.ARRAY, - constraints: { - type: JsonFieldSubType.ARRAY, - inclusion: ["option 1", "option 2"], - }, - }, - }) - tableOrViewId = await createTableOrView({ - relationship: { - type: FieldType.LINK, - relationshipType: RelationshipType.MANY_TO_ONE, - name: "relationship", - fieldName: "relate", - tableId: arrayTable, - constraints: { - type: "array", - }, - }, - formula: { - type: FieldType.FORMULA, - name: "formula", - formula: encodeJSBinding( - `let array = [];$("relationship").forEach(rel => array = array.concat(rel.array));return array.sort().join(",")` - ), - }, - }) - const arrayRows = await Promise.all([ - config.api.row.save(arrayTable, { - name: "foo", - array: ["option 1"], - }), - config.api.row.save(arrayTable, { - name: "bar", - array: ["option 2"], - }), - ]) - await Promise.all([ - config.api.row.save(tableOrViewId, { - relationship: [arrayRows[0]._id, arrayRows[1]._id], - }), - ]) - }) + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "isTrue", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ isTrue: false }, { isTrue: true }]) + }) - it("formula is correct with relationship arrays", async () => { - await expectQuery({}).toContain([{ formula: "option 1,option 2" }]) - }) - }) - - describe("user", () => { - let user1: User - let user2: User - - beforeAll(async () => { - user1 = await config.createUser({ _id: `us_${utils.newid()}` }) - user2 = await config.createUser({ _id: `us_${utils.newid()}` }) - - tableOrViewId = await createTableOrView({ - user: { - name: "user", - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - }, - }) - - await createRows([{ user: user1 }, { user: user2 }, { user: null }]) - }) - - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ equal: { user: user1._id } }).toContainExactly([ - { user: { _id: user1._id } }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ equal: { user: "us_none" } }).toFindNothing() - }) - }) - - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ notEqual: { user: user1._id } }).toContainExactly( - [{ user: { _id: user2._id } }, {}] - ) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ notEqual: { user: "us_none" } }).toContainExactly( - [{ user: { _id: user1._id } }, { user: { _id: user2._id } }, {}] - ) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ oneOf: { user: [user1._id] } }).toContainExactly([ - { user: { _id: user1._id } }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ oneOf: { user: ["us_none"] } }).toFindNothing() - }) - }) - - describe("empty", () => { - it("finds empty rows", async () => { - await expectQuery({ empty: { user: null } }).toContainExactly([{}]) - }) - }) - - describe("notEmpty", () => { - it("finds non-empty rows", async () => { - await expectQuery({ notEmpty: { user: null } }).toContainExactly([ - { user: { _id: user1._id } }, - { user: { _id: user2._id } }, - ]) - }) - }) - }) - - describe("multi user", () => { - let user1: User - let user2: User - - beforeAll(async () => { - user1 = await config.createUser({ _id: `us_${utils.newid()}` }) - user2 = await config.createUser({ _id: `us_${utils.newid()}` }) - - tableOrViewId = await createTableOrView({ - users: { - name: "users", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - constraints: { type: "array" }, - }, - number: { - name: "number", - type: FieldType.NUMBER, - }, - }) - - await createRows([ - { number: 1, users: [user1] }, - { number: 2, users: [user2] }, - { number: 3, users: [user1, user2] }, - { number: 4, users: [] }, - ]) - }) - - describe("contains", () => { - it("successfully finds a row", async () => { - await expectQuery({ - contains: { users: [user1._id] }, - }).toContainExactly([ - { users: [{ _id: user1._id }] }, - { users: [{ _id: user1._id }, { _id: user2._id }] }, - ]) - }) - - it("successfully finds a row searching with a string", async () => { - await expectQuery({ - // @ts-expect-error this test specifically goes against the type to - // test that we coerce the string to an array. - contains: { "1:users": user1._id }, - }).toContainExactly([ - { users: [{ _id: user1._id }] }, - { users: [{ _id: user1._id }, { _id: user2._id }] }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - contains: { users: ["us_none"] }, - }).toFindNothing() - }) - }) - - describe("notContains", () => { - it("successfully finds a row", async () => { - await expectQuery({ - notContains: { users: [user1._id] }, - }).toContainExactly([{ users: [{ _id: user2._id }] }, {}]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - notContains: { users: ["us_none"] }, - }).toContainExactly([ - { users: [{ _id: user1._id }] }, - { users: [{ _id: user2._id }] }, - { users: [{ _id: user1._id }, { _id: user2._id }] }, - {}, - ]) - }) - }) - - describe("containsAny", () => { - it("successfully finds rows", async () => { - await expectQuery({ - containsAny: { users: [user1._id, user2._id] }, - }).toContainExactly([ - { users: [{ _id: user1._id }] }, - { users: [{ _id: user2._id }] }, - { users: [{ _id: user1._id }, { _id: user2._id }] }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - containsAny: { users: ["us_none"] }, - }).toFindNothing() - }) - }) - - describe("multi-column equals", () => { - it("successfully finds a row", async () => { - await expectQuery({ - equal: { number: 1 }, - contains: { users: [user1._id] }, - }).toContainExactly([{ users: [{ _id: user1._id }], number: 1 }]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - equal: { number: 2 }, - contains: { users: [user1._id] }, - }).toFindNothing() - }) - }) - }) - - // This will never work for Lucene. - !isLucene && - // It also can't work for in-memory searching because the related table name - // isn't available. - !isInMemory && - describe.each([ - RelationshipType.ONE_TO_MANY, - RelationshipType.MANY_TO_ONE, - RelationshipType.MANY_TO_MANY, - ])("relations (%s)", relationshipType => { - let productCategoryTable: Table, productCatRows: Row[] - - beforeAll(async () => { - const { relatedTable, tableId } = await basicRelationshipTables( - relationshipType - ) - tableOrViewId = tableId - productCategoryTable = relatedTable - - productCatRows = await Promise.all([ - config.api.row.save(productCategoryTable._id!, { name: "foo" }), - config.api.row.save(productCategoryTable._id!, { name: "bar" }), - ]) - - await Promise.all([ - config.api.row.save(tableOrViewId, { - name: "foo", - productCat: [productCatRows[0]._id], - }), - config.api.row.save(tableOrViewId, { - name: "bar", - productCat: [productCatRows[1]._id], - }), - config.api.row.save(tableOrViewId, { - name: "baz", - productCat: [], - }), - ]) - }) - - it("should be able to filter by relationship using column name", async () => { - await expectQuery({ - equal: { ["productCat.name"]: "foo" }, - }).toContainExactly([ - { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, - ]) - }) - - it("should be able to filter by relationship using table name", async () => { - await expectQuery({ - equal: { [`${productCategoryTable.name}.name`]: "foo" }, - }).toContainExactly([ - { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, - ]) - }) - - it("shouldn't return any relationship for last row", async () => { - await expectQuery({ - equal: { ["name"]: "baz" }, - }).toContainExactly([{ name: "baz", productCat: undefined }]) - }) - - describe("logical filters", () => { - const logicalOperators = [LogicalOperator.AND, LogicalOperator.OR] - - describe("$and", () => { - it("should allow single conditions", async () => { - await expectQuery({ - $and: { - conditions: [ - { - equal: { ["productCat.name"]: "foo" }, - }, - ], - }, - }).toContainExactly([ - { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, - ]) + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "isTrue", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ isTrue: true }, { isTrue: false }]) + }) + }) }) - it("should allow exclusive conditions", async () => { - await expectQuery({ - $and: { - conditions: [ - { - equal: { ["productCat.name"]: "foo" }, - notEqual: { ["productCat.name"]: "foo" }, - }, - ], - }, - }).toContainExactly([]) - }) + !isInMemory && + describe("bindings", () => { + let globalUsers: any = [] - it.each([logicalOperators])( - "should allow nested ands with single conditions (with %s as root)", - async rootOperator => { - await expectQuery({ - [rootOperator]: { - conditions: [ + const serverTime = new Date() + + // In MariaDB and MySQL we only store dates to second precision, so we need + // to remove milliseconds from the server time to ensure searches work as + // expected. + serverTime.setMilliseconds(0) + + const future = new Date( + serverTime.getTime() + 1000 * 60 * 60 * 24 * 30 + ) + + const rows = (currentUser: User) => { + return [ + { name: "foo", appointment: "1982-01-05T00:00:00.000Z" }, + { name: "bar", appointment: "1995-05-06T00:00:00.000Z" }, + { + name: currentUser.firstName, + appointment: future.toISOString(), + }, + { + name: "serverDate", + appointment: serverTime.toISOString(), + }, + { + name: "single user, session user", + single_user: currentUser, + }, + { + name: "single user", + single_user: globalUsers[0], + }, + { + name: "deprecated single user, session user", + deprecated_single_user: [currentUser], + }, + { + name: "deprecated single user", + deprecated_single_user: [globalUsers[0]], + }, + { + name: "multi user", + multi_user: globalUsers, + }, + { + name: "multi user with session user", + multi_user: [...globalUsers, currentUser], + }, + { + name: "deprecated multi user", + deprecated_multi_user: globalUsers, + }, + { + name: "deprecated multi user with session user", + deprecated_multi_user: [...globalUsers, currentUser], + }, + ] + } + + beforeAll(async () => { + // Set up some global users + globalUsers = await Promise.all( + Array(2) + .fill(0) + .map(async () => { + const globalUser = await config.globalUser() + const userMedataId = globalUser._id + ? dbCore.generateUserMetadataID(globalUser._id) + : null + return { + _id: globalUser._id, + _meta: userMedataId, + } + }) + ) + + tableOrViewId = await createTableOrView({ + name: { name: "name", type: FieldType.STRING }, + appointment: { + name: "appointment", + type: FieldType.DATETIME, + }, + single_user: { + name: "single_user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + }, + deprecated_single_user: { + name: "deprecated_single_user", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + }, + multi_user: { + name: "multi_user", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + constraints: { + type: "array", + }, + }, + deprecated_multi_user: { + name: "deprecated_multi_user", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USERS, + constraints: { + type: "array", + }, + }, + }) + await createRows(rows(config.getUser())) + }) + + // !! Current User is auto generated per run + it("should return all rows matching the session user firstname", async () => { + await expectQuery({ + equal: { name: "{{ [user].firstName }}" }, + }).toContainExactly([ + { + name: config.getUser().firstName, + appointment: future.toISOString(), + }, + ]) + }) + + it("should return all rows matching the session user firstname when logical operator used", async () => { + await expectQuery({ + $and: { + conditions: [ + { equal: { name: "{{ [user].firstName }}" } }, + ], + }, + }).toContainExactly([ + { + name: config.getUser().firstName, + appointment: future.toISOString(), + }, + ]) + }) + + it("should parse the date binding and return all rows after the resolved value", async () => { + await tk.withFreeze(serverTime, async () => { + await expectQuery({ + range: { + appointment: { + low: "{{ [now] }}", + high: "9999-00-00T00:00:00.000Z", + }, + }, + }).toContainExactly([ { + name: config.getUser().firstName, + appointment: future.toISOString(), + }, + { + name: "serverDate", + appointment: serverTime.toISOString(), + }, + ]) + }) + }) + + it("should parse the date binding and return all rows before the resolved value", async () => { + await expectQuery({ + range: { + appointment: { + low: "0000-00-00T00:00:00.000Z", + high: "{{ [now] }}", + }, + }, + }).toContainExactly([ + { name: "foo", appointment: "1982-01-05T00:00:00.000Z" }, + { name: "bar", appointment: "1995-05-06T00:00:00.000Z" }, + { + name: "serverDate", + appointment: serverTime.toISOString(), + }, + ]) + }) + + it("should parse the encoded js snippet. Return rows with appointments up to 1 week in the past", async () => { + const jsBinding = "return snippets.WeeksAgo();" + const encodedBinding = encodeJSBinding(jsBinding) + + await expectQuery({ + range: { + appointment: { + low: "0000-00-00T00:00:00.000Z", + high: encodedBinding, + }, + }, + }).toContainExactly([ + { name: "foo", appointment: "1982-01-05T00:00:00.000Z" }, + { name: "bar", appointment: "1995-05-06T00:00:00.000Z" }, + ]) + }) + + it("should parse the encoded js binding. Return rows with appointments 2 weeks in the past", async () => { + const jsBinding = `const currentTime = new Date(${Date.now()})\ncurrentTime.setDate(currentTime.getDate()-14);\nreturn currentTime.toISOString();` + const encodedBinding = encodeJSBinding(jsBinding) + + await expectQuery({ + range: { + appointment: { + low: "0000-00-00T00:00:00.000Z", + high: encodedBinding, + }, + }, + }).toContainExactly([ + { name: "foo", appointment: "1982-01-05T00:00:00.000Z" }, + { name: "bar", appointment: "1995-05-06T00:00:00.000Z" }, + ]) + }) + + it("should match a single user row by the session user id", async () => { + await expectQuery({ + equal: { single_user: "{{ [user]._id }}" }, + }).toContainExactly([ + { + name: "single user, session user", + single_user: { _id: config.getUser()._id }, + }, + ]) + }) + + it("should match a deprecated single user row by the session user id", async () => { + await expectQuery({ + equal: { deprecated_single_user: "{{ [user]._id }}" }, + }).toContainExactly([ + { + name: "deprecated single user, session user", + deprecated_single_user: [{ _id: config.getUser()._id }], + }, + ]) + }) + + it("should match the session user id in a multi user field", async () => { + const allUsers = [...globalUsers, config.getUser()].map( + (user: any) => { + return { _id: user._id } + } + ) + + await expectQuery({ + contains: { multi_user: ["{{ [user]._id }}"] }, + }).toContainExactly([ + { + name: "multi user with session user", + multi_user: allUsers, + }, + ]) + }) + + it("should match the session user id in a deprecated multi user field", async () => { + const allUsers = [...globalUsers, config.getUser()].map( + (user: any) => { + return { _id: user._id } + } + ) + + await expectQuery({ + contains: { deprecated_multi_user: ["{{ [user]._id }}"] }, + }).toContainExactly([ + { + name: "deprecated multi user with session user", + deprecated_multi_user: allUsers, + }, + ]) + }) + + it("should not match the session user id in a multi user field", async () => { + await expectQuery({ + notContains: { multi_user: ["{{ [user]._id }}"] }, + notEmpty: { multi_user: true }, + }).toContainExactly([ + { + name: "multi user", + multi_user: globalUsers.map((user: any) => { + return { _id: user._id } + }), + }, + ]) + }) + + it("should not match the session user id in a deprecated multi user field", async () => { + await expectQuery({ + notContains: { + deprecated_multi_user: ["{{ [user]._id }}"], + }, + notEmpty: { deprecated_multi_user: true }, + }).toContainExactly([ + { + name: "deprecated multi user", + deprecated_multi_user: globalUsers.map((user: any) => { + return { _id: user._id } + }), + }, + ]) + }) + + it("should match the session user id and a user table row id using helpers, user binding and a static user id.", async () => { + await expectQuery({ + oneOf: { + single_user: [ + "{{ default [user]._id '_empty_' }}", + globalUsers[0]._id, + ], + }, + }).toContainExactly([ + { + name: "single user, session user", + single_user: { _id: config.getUser()._id }, + }, + { + name: "single user", + single_user: { _id: globalUsers[0]._id }, + }, + ]) + }) + + it("should match the session user id and a user table row id using helpers, user binding and a static user id. (deprecated single user)", async () => { + await expectQuery({ + oneOf: { + deprecated_single_user: [ + "{{ default [user]._id '_empty_' }}", + globalUsers[0]._id, + ], + }, + }).toContainExactly([ + { + name: "deprecated single user, session user", + deprecated_single_user: [{ _id: config.getUser()._id }], + }, + { + name: "deprecated single user", + deprecated_single_user: [{ _id: globalUsers[0]._id }], + }, + ]) + }) + + it("should resolve 'default' helper to '_empty_' when binding resolves to nothing", async () => { + await expectQuery({ + oneOf: { + single_user: [ + "{{ default [user]._idx '_empty_' }}", + globalUsers[0]._id, + ], + }, + }).toContainExactly([ + { + name: "single user", + single_user: { _id: globalUsers[0]._id }, + }, + ]) + }) + + it("should resolve 'default' helper to '_empty_' when binding resolves to nothing (deprecated single user)", async () => { + await expectQuery({ + oneOf: { + deprecated_single_user: [ + "{{ default [user]._idx '_empty_' }}", + globalUsers[0]._id, + ], + }, + }).toContainExactly([ + { + name: "deprecated single user", + deprecated_single_user: [{ _id: globalUsers[0]._id }], + }, + ]) + }) + }) + + const stringTypes = [FieldType.STRING, FieldType.LONGFORM] as const + describe.each(stringTypes)("%s", type => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + name: { name: "name", type }, + }) + await createRows([{ name: "foo" }, { name: "bar" }]) + }) + + describe("misc", () => { + it("should return all if no query is passed", async () => { + await expectSearch({} as RowSearchParams).toContainExactly([ + { name: "foo" }, + { name: "bar" }, + ]) + }) + + it("should return all if empty query is passed", async () => { + await expectQuery({}).toContainExactly([ + { name: "foo" }, + { name: "bar" }, + ]) + }) + + it("should return all if onEmptyFilter is RETURN_ALL", async () => { + await expectQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + // onEmptyFilter cannot be sent to view searches + !isView && + it("should return nothing if onEmptyFilter is RETURN_NONE", async () => { + await expectQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + }).toFindNothing() + }) + + it("should respect limit", async () => { + await expectSearch({ + limit: 1, + paginate: true, + query: {}, + }).toHaveLength(1) + }) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { name: "foo" }, + }).toContainExactly([{ name: "foo" }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ equal: { name: "none" } }).toFindNothing() + }) + + it("works as an or condition", async () => { + await expectQuery({ + allOr: true, + equal: { name: "foo" }, + oneOf: { name: ["bar"] }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + it("can have multiple values for same column", async () => { + await expectQuery({ + allOr: true, + equal: { "1:name": "foo", "2:name": "bar" }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { name: "foo" }, + }).toContainExactly([{ name: "bar" }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notEqual: { name: "bar" }, + }).toContainExactly([{ name: "foo" }]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { name: ["foo"] }, + }).toContainExactly([{ name: "foo" }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { name: ["none"] }, + }).toFindNothing() + }) + + it("can have multiple values for same column", async () => { + await expectQuery({ + oneOf: { + name: ["foo", "bar"], + }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + it("splits comma separated strings", async () => { + await expectQuery({ + oneOf: { + // @ts-ignore + name: "foo,bar", + }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + it("trims whitespace", async () => { + await expectQuery({ + oneOf: { + // @ts-ignore + name: "foo, bar", + }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + it("empty arrays returns all when onEmptyFilter is set to return 'all'", async () => { + await expectQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + oneOf: { name: [] }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + // onEmptyFilter cannot be sent to view searches + !isView && + it("empty arrays returns all when onEmptyFilter is set to return 'none'", async () => { + await expectQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + oneOf: { name: [] }, + }).toContainExactly([]) + }) + }) + + describe("fuzzy", () => { + it("successfully finds a row", async () => { + await expectQuery({ fuzzy: { name: "oo" } }).toContainExactly( + [{ name: "foo" }] + ) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ fuzzy: { name: "none" } }).toFindNothing() + }) + }) + + describe("string", () => { + it("successfully finds a row", async () => { + await expectQuery({ + string: { name: "fo" }, + }).toContainExactly([{ name: "foo" }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + string: { name: "none" }, + }).toFindNothing() + }) + + it("is case-insensitive", async () => { + await expectQuery({ + string: { name: "FO" }, + }).toContainExactly([{ name: "foo" }]) + }) + }) + + describe("range", () => { + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { name: { low: "a", high: "z" } }, + }).toContainExactly([{ name: "bar" }, { name: "foo" }]) + }) + + it("successfully finds a row with a high bound", async () => { + await expectQuery({ + range: { name: { low: "a", high: "c" } }, + }).toContainExactly([{ name: "bar" }]) + }) + + it("successfully finds a row with a low bound", async () => { + await expectQuery({ + range: { name: { low: "f", high: "z" } }, + }).toContainExactly([{ name: "foo" }]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { name: { low: "g", high: "h" } }, + }).toFindNothing() + }) + + it("ignores low if it's an empty object", async () => { + await expectQuery({ + // @ts-ignore + range: { name: { low: {}, high: "z" } }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + it("ignores high if it's an empty object", async () => { + await expectQuery({ + // @ts-ignore + range: { name: { low: "a", high: {} } }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + }) + + describe("empty", () => { + it("finds no empty rows", async () => { + await expectQuery({ empty: { name: null } }).toFindNothing() + }) + + it("should not be affected by when filter empty behaviour", async () => { + await expectQuery({ + empty: { name: null }, + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + }).toFindNothing() + }) + }) + + describe("notEmpty", () => { + it("finds all non-empty rows", async () => { + await expectQuery({ + notEmpty: { name: null }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + it("should not be affected by when filter empty behaviour", async () => { + await expectQuery({ + notEmpty: { name: null }, + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + }) + + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "name", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ name: "bar" }, { name: "foo" }]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "name", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ name: "foo" }, { name: "bar" }]) + }) + + describe("sortType STRING", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "name", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ name: "bar" }, { name: "foo" }]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "name", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ name: "foo" }, { name: "bar" }]) + }) + }) + + !isInternal && + !isInMemory && + // This test was added because we automatically add in a sort by the + // primary key, and we used to do this unconditionally which caused + // problems because it was possible for the primary key to appear twice + // in the resulting SQL ORDER BY clause, resulting in an SQL error. + // We now check first to make sure that the primary key isn't already + // in the sort before adding it. + describe("sort on primary key", () => { + beforeAll(async () => { + const tableName = structures.uuid().substring(0, 10) + await client!.schema.createTable(tableName, t => { + t.string("name").primary() + }) + const resp = await config.api.datasource.fetchSchema({ + datasourceId: datasource!._id!, + }) + + tableOrViewId = resp.datasource.entities![tableName]._id! + + await createRows([{ name: "foo" }, { name: "bar" }]) + }) + + it("should be able to sort by a primary key column ascending", async () => + expectSearch({ + query: {}, + sort: "name", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ name: "bar" }, { name: "foo" }])) + + it("should be able to sort by a primary key column descending", async () => + expectSearch({ + query: {}, + sort: "name", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) + }) + }) + }) + + describe("numbers", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + age: { name: "age", type: FieldType.NUMBER }, + }) + await createRows([{ age: 1 }, { age: 10 }]) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ equal: { age: 1 } }).toContainExactly([ + { age: 1 }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ equal: { age: 2 } }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ notEqual: { age: 1 } }).toContainExactly([ + { age: 10 }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ notEqual: { age: 10 } }).toContainExactly( + [{ age: 1 }] + ) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ oneOf: { age: [1] } }).toContainExactly([ + { age: 1 }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ oneOf: { age: [2] } }).toFindNothing() + }) + + it("can convert from a string", async () => { + await expectQuery({ + oneOf: { + // @ts-ignore + age: "1", + }, + }).toContainExactly([{ age: 1 }]) + }) + + it("can find multiple values for same column", async () => { + await expectQuery({ + oneOf: { + // @ts-ignore + age: "1,10", + }, + }).toContainExactly([{ age: 1 }, { age: 10 }]) + }) + }) + + describe("range", () => { + it("successfully finds a row", async () => { + await expectQuery({ + range: { age: { low: 1, high: 5 } }, + }).toContainExactly([{ age: 1 }]) + }) + + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { age: { low: 1, high: 10 } }, + }).toContainExactly([{ age: 1 }, { age: 10 }]) + }) + + it("successfully finds a row with a high bound", async () => { + await expectQuery({ + range: { age: { low: 5, high: 10 } }, + }).toContainExactly([{ age: 10 }]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { age: { low: 5, high: 9 } }, + }).toFindNothing() + }) + + it("greater than equal to", async () => { + await expectQuery({ + range: { + age: { low: 10, high: Number.MAX_SAFE_INTEGER }, + }, + }).toContainExactly([{ age: 10 }]) + }) + + it("greater than", async () => { + await expectQuery({ + range: { + age: { low: 5, high: Number.MAX_SAFE_INTEGER }, + }, + }).toContainExactly([{ age: 10 }]) + }) + + it("less than equal to", async () => { + await expectQuery({ + range: { + age: { high: 1, low: Number.MIN_SAFE_INTEGER }, + }, + }).toContainExactly([{ age: 1 }]) + }) + + it("less than", async () => { + await expectQuery({ + range: { + age: { high: 5, low: Number.MIN_SAFE_INTEGER }, + }, + }).toContainExactly([{ age: 1 }]) + }) + }) + + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "age", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ age: 1 }, { age: 10 }]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "age", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ age: 10 }, { age: 1 }]) + }) + }) + + describe("sortType NUMBER", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "age", + sortType: SortType.NUMBER, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ age: 1 }, { age: 10 }]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "age", + sortType: SortType.NUMBER, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ age: 10 }, { age: 1 }]) + }) + }) + }) + + describe("dates", () => { + const JAN_1ST = "2020-01-01T00:00:00.000Z" + const JAN_2ND = "2020-01-02T00:00:00.000Z" + const JAN_5TH = "2020-01-05T00:00:00.000Z" + const JAN_9TH = "2020-01-09T00:00:00.000Z" + const JAN_10TH = "2020-01-10T00:00:00.000Z" + + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + dob: { name: "dob", type: FieldType.DATETIME }, + }) + + await createRows([{ dob: JAN_1ST }, { dob: JAN_10TH }]) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { dob: JAN_1ST }, + }).toContainExactly([{ dob: JAN_1ST }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { dob: JAN_1ST }, + }).toContainExactly([{ dob: JAN_10TH }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notEqual: { dob: JAN_10TH }, + }).toContainExactly([{ dob: JAN_1ST }]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { dob: [JAN_1ST] }, + }).toContainExactly([{ dob: JAN_1ST }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { dob: [JAN_2ND] }, + }).toFindNothing() + }) + }) + + describe("range", () => { + it("successfully finds a row", async () => { + await expectQuery({ + range: { dob: { low: JAN_1ST, high: JAN_5TH } }, + }).toContainExactly([{ dob: JAN_1ST }]) + }) + + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { dob: { low: JAN_1ST, high: JAN_10TH } }, + }).toContainExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]) + }) + + it("successfully finds a row with a high bound", async () => { + await expectQuery({ + range: { dob: { low: JAN_5TH, high: JAN_10TH } }, + }).toContainExactly([{ dob: JAN_10TH }]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { dob: { low: JAN_5TH, high: JAN_9TH } }, + }).toFindNothing() + }) + + it("greater than equal to", async () => { + await expectQuery({ + range: { + dob: { + low: JAN_10TH, + high: MAX_VALID_DATE.toISOString(), + }, + }, + }).toContainExactly([{ dob: JAN_10TH }]) + }) + + it("greater than", async () => { + await expectQuery({ + range: { + dob: { low: JAN_5TH, high: MAX_VALID_DATE.toISOString() }, + }, + }).toContainExactly([{ dob: JAN_10TH }]) + }) + + it("less than equal to", async () => { + await expectQuery({ + range: { + dob: { high: JAN_1ST, low: MIN_VALID_DATE.toISOString() }, + }, + }).toContainExactly([{ dob: JAN_1ST }]) + }) + + it("less than", async () => { + await expectQuery({ + range: { + dob: { high: JAN_5TH, low: MIN_VALID_DATE.toISOString() }, + }, + }).toContainExactly([{ dob: JAN_1ST }]) + }) + }) + + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "dob", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "dob", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }]) + }) + + describe("sortType STRING", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "dob", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "dob", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }]) + }) + }) + }) + }) + + !isInternal && + describe("datetime - time only", () => { + const T_1000 = "10:00:00" + const T_1045 = "10:45:00" + const T_1200 = "12:00:00" + const T_1530 = "15:30:00" + const T_0000 = "00:00:00" + + const UNEXISTING_TIME = "10:01:00" + + const NULL_TIME__ID = `null_time__id` + + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + timeid: { name: "timeid", type: FieldType.STRING }, + time: { + name: "time", + type: FieldType.DATETIME, + timeOnly: true, + }, + }) + + await createRows([ + { timeid: NULL_TIME__ID, time: null }, + { time: T_1000 }, + { time: T_1045 }, + { time: T_1200 }, + { time: T_1530 }, + { time: T_0000 }, + ]) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { time: T_1000 }, + }).toContainExactly([{ time: "10:00:00" }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { time: UNEXISTING_TIME }, + }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { time: T_1000 }, + }).toContainExactly([ + { timeid: NULL_TIME__ID }, + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + { time: "00:00:00" }, + ]) + }) + + it("return all when requesting non-existing", async () => { + await expectQuery({ + notEqual: { time: UNEXISTING_TIME }, + }).toContainExactly([ + { timeid: NULL_TIME__ID }, + { time: "10:00:00" }, + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + { time: "00:00:00" }, + ]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { time: [T_1000] }, + }).toContainExactly([{ time: "10:00:00" }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { time: [UNEXISTING_TIME] }, + }).toFindNothing() + }) + }) + + describe("range", () => { + it("successfully finds a row", async () => { + await expectQuery({ + range: { time: { low: T_1045, high: T_1045 } }, + }).toContainExactly([{ time: "10:45:00" }]) + }) + + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { time: { low: T_1045, high: T_1530 } }, + }).toContainExactly([ + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + ]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { + time: { low: UNEXISTING_TIME, high: UNEXISTING_TIME }, + }, + }).toFindNothing() + }) + }) + + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "time", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { timeid: NULL_TIME__ID }, + { time: "00:00:00" }, + { time: "10:00:00" }, + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + ]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "time", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { time: "15:30:00" }, + { time: "12:00:00" }, + { time: "10:45:00" }, + { time: "10:00:00" }, + { time: "00:00:00" }, + { timeid: NULL_TIME__ID }, + ]) + }) + + describe("sortType STRING", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "time", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { timeid: NULL_TIME__ID }, + { time: "00:00:00" }, + { time: "10:00:00" }, + { time: "10:45:00" }, + { time: "12:00:00" }, + { time: "15:30:00" }, + ]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "time", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { time: "15:30:00" }, + { time: "12:00:00" }, + { time: "10:45:00" }, + { time: "10:00:00" }, + { time: "00:00:00" }, + { timeid: NULL_TIME__ID }, + ]) + }) + }) + }) + }) + + isInternal && + !isInMemory && + describe("AI Column", () => { + const UNEXISTING_AI_COLUMN = "Real LLM Response" + + beforeAll(async () => { + mocks.licenses.useBudibaseAI() + mocks.licenses.useAICustomConfigs() + + tableOrViewId = await createTableOrView({ + product: { name: "product", type: FieldType.STRING }, + ai: { + name: "AI", + type: FieldType.AI, + operation: AIOperationEnum.PROMPT, + prompt: "Translate '{{ product }}' into German", + }, + }) + + await createRows([ + { product: "Big Mac" }, + { product: "McCrispy" }, + ]) + }) + + describe("equal", () => { + it("successfully finds rows based on AI column", async () => { + await expectQuery({ + equal: { ai: "Mock LLM Response" }, + }).toContainExactly([ + { product: "Big Mac" }, + { product: "McCrispy" }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { ai: UNEXISTING_AI_COLUMN }, + }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("Returns nothing when searching notEqual on the mock AI response", async () => { + await expectQuery({ + notEqual: { ai: "Mock LLM Response" }, + }).toContainExactly([]) + }) + + it("return all when requesting non-existing response", async () => { + await expectQuery({ + notEqual: { ai: "Real LLM Response" }, + }).toContainExactly([ + { product: "Big Mac" }, + { product: "McCrispy" }, + ]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { + ai: ["Mock LLM Response", "Other LLM Response"], + }, + }).toContainExactly([ + { product: "Big Mac" }, + { product: "McCrispy" }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { ai: ["Whopper"] }, + }).toFindNothing() + }) + }) + }) + + describe("arrays", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + numbers: { + name: "numbers", + type: FieldType.ARRAY, + constraints: { + type: JsonFieldSubType.ARRAY, + inclusion: ["one", "two", "three"], + }, + }, + }) + await createRows([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ]) + }) + + describe("contains", () => { + it("successfully finds a row", async () => { + await expectQuery({ + contains: { numbers: ["one"] }, + }).toContainExactly([{ numbers: ["one", "two"] }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + contains: { numbers: ["none"] }, + }).toFindNothing() + }) + + it("fails to find row containing all", async () => { + await expectQuery({ + contains: { numbers: ["one", "two", "three"] }, + }).toFindNothing() + }) + + it("finds all with empty list", async () => { + await expectQuery({ + contains: { numbers: [] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ]) + }) + }) + + describe("notContains", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notContains: { numbers: ["one"] }, + }).toContainExactly([{ numbers: ["three"] }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notContains: { numbers: ["one", "two", "three"] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ]) + }) + + // Not sure if this is correct behaviour but changing it would be a + // breaking change. + it("finds all with empty list", async () => { + await expectQuery({ + notContains: { numbers: [] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ]) + }) + }) + + describe("containsAny", () => { + it("successfully finds rows", async () => { + await expectQuery({ + containsAny: { numbers: ["one", "two", "three"] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + containsAny: { numbers: ["none"] }, + }).toFindNothing() + }) + + it("finds all with empty list", async () => { + await expectQuery({ + containsAny: { numbers: [] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ]) + }) + }) + }) + + describe("bigints", () => { + const SMALL = "1" + const MEDIUM = "10000000" + + // Our bigints are int64s in most datasources. + let BIG = "9223372036854775807" + + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + num: { name: "num", type: FieldType.BIGINT }, + }) + await createRows([ + { num: SMALL }, + { num: MEDIUM }, + { num: BIG }, + ]) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ equal: { num: SMALL } }).toContainExactly( + [{ num: SMALL }] + ) + }) + + it("successfully finds a big value", async () => { + await expectQuery({ equal: { num: BIG } }).toContainExactly([ + { num: BIG }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ equal: { num: "2" } }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { num: SMALL }, + }).toContainExactly([{ num: MEDIUM }, { num: BIG }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ notEqual: { num: 10 } }).toContainExactly( + [{ num: SMALL }, { num: MEDIUM }, { num: BIG }] + ) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { num: [SMALL] }, + }).toContainExactly([{ num: SMALL }]) + }) + + it("successfully finds all rows", async () => { + await expectQuery({ + oneOf: { num: [SMALL, MEDIUM, BIG] }, + }).toContainExactly([ + { num: SMALL }, + { num: MEDIUM }, + { num: BIG }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ oneOf: { num: [2] } }).toFindNothing() + }) + }) + + describe("range", () => { + it("successfully finds a row", async () => { + await expectQuery({ + range: { num: { low: SMALL, high: "5" } }, + }).toContainExactly([{ num: SMALL }]) + }) + + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { num: { low: SMALL, high: MEDIUM } }, + }).toContainExactly([{ num: SMALL }, { num: MEDIUM }]) + }) + + it("successfully finds a row with a high bound", async () => { + await expectQuery({ + range: { num: { low: MEDIUM, high: BIG } }, + }).toContainExactly([{ num: MEDIUM }, { num: BIG }]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { num: { low: "5", high: "5" } }, + }).toFindNothing() + }) + + it("can search using just a low value", async () => { + await expectQuery({ + range: { num: { low: MEDIUM } }, + }).toContainExactly([{ num: MEDIUM }, { num: BIG }]) + }) + + it("can search using just a high value", async () => { + await expectQuery({ + range: { num: { high: MEDIUM } }, + }).toContainExactly([{ num: SMALL }, { num: MEDIUM }]) + }) + }) + }) + + isInternal && + describe("auto", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + auto: { + name: "auto", + type: FieldType.AUTO, + autocolumn: true, + subtype: AutoFieldSubType.AUTO_ID, + }, + }) + await createRows(new Array(10).fill({})) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ equal: { auto: 1 } }).toContainExactly([ + { auto: 1 }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ equal: { auto: 0 } }).toFindNothing() + }) + }) + + describe("not equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { auto: 1 }, + }).toContainExactly([ + { auto: 2 }, + { auto: 3 }, + { auto: 4 }, + { auto: 5 }, + { auto: 6 }, + { auto: 7 }, + { auto: 8 }, + { auto: 9 }, + { auto: 10 }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notEqual: { auto: 0 }, + }).toContainExactly([ + { auto: 1 }, + { auto: 2 }, + { auto: 3 }, + { auto: 4 }, + { auto: 5 }, + { auto: 6 }, + { auto: 7 }, + { auto: 8 }, + { auto: 9 }, + { auto: 10 }, + ]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { auto: [1] }, + }).toContainExactly([{ auto: 1 }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ oneOf: { auto: [0] } }).toFindNothing() + }) + }) + + describe("range", () => { + it("successfully finds a row", async () => { + await expectQuery({ + range: { auto: { low: 1, high: 1 } }, + }).toContainExactly([{ auto: 1 }]) + }) + + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { auto: { low: 1, high: 2 } }, + }).toContainExactly([{ auto: 1 }, { auto: 2 }]) + }) + + it("successfully finds a row with a high bound", async () => { + await expectQuery({ + range: { auto: { low: 2, high: 2 } }, + }).toContainExactly([{ auto: 2 }]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { auto: { low: 0, high: 0 } }, + }).toFindNothing() + }) + + it("can search using just a low value", async () => { + await expectQuery({ + range: { auto: { low: 9 } }, + }).toContainExactly([{ auto: 9 }, { auto: 10 }]) + }) + + it("can search using just a high value", async () => { + await expectQuery({ + range: { auto: { high: 2 } }, + }).toContainExactly([{ auto: 1 }, { auto: 2 }]) + }) + }) + + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "auto", + sortOrder: SortOrder.ASCENDING, + sortType: SortType.NUMBER, + }).toMatchExactly([ + { auto: 1 }, + { auto: 2 }, + { auto: 3 }, + { auto: 4 }, + { auto: 5 }, + { auto: 6 }, + { auto: 7 }, + { auto: 8 }, + { auto: 9 }, + { auto: 10 }, + ]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "auto", + sortOrder: SortOrder.DESCENDING, + sortType: SortType.NUMBER, + }).toMatchExactly([ + { auto: 10 }, + { auto: 9 }, + { auto: 8 }, + { auto: 7 }, + { auto: 6 }, + { auto: 5 }, + { auto: 4 }, + { auto: 3 }, + { auto: 2 }, + { auto: 1 }, + ]) + }) + + // This is important for pagination. The order of results must always + // be stable or pagination will break. We don't want the user to need + // to specify an order for pagination to work. + it("is stable without a sort specified", async () => { + let { rows: fullRowList } = await config.api.row.search( + tableOrViewId, + { + tableId: tableOrViewId, + query: {}, + } + ) + + // repeat the search many times to check the first row is always the same + let bookmark: string | number | undefined, + hasNextPage: boolean | undefined = true, + rowCount: number = 0 + do { + const response = await config.api.row.search( + tableOrViewId, + { + tableId: tableOrViewId, + limit: 1, + paginate: true, + query: {}, + bookmark, + } + ) + bookmark = response.bookmark + hasNextPage = response.hasNextPage + expect(response.rows.length).toEqual(1) + const foundRow = response.rows[0] + expect(foundRow).toEqual(fullRowList[rowCount++]) + } while (hasNextPage) + }) + }) + + describe("pagination", () => { + it("should paginate through all rows", async () => { + // @ts-ignore + let bookmark: string | number = undefined + let rows: Row[] = [] + + // eslint-disable-next-line no-constant-condition + while (true) { + const response = await config.api.row.search( + tableOrViewId, + { + tableId: tableOrViewId, + limit: 3, + query: {}, + bookmark, + paginate: true, + } + ) + + rows.push(...response.rows) + + if (!response.bookmark || !response.hasNextPage) { + break + } + bookmark = response.bookmark + } + + const autoValues = rows + .map(row => row.auto) + .sort((a, b) => a - b) + expect(autoValues).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + }) + }) + }) + + describe("field name 1:name", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + "1:name": { name: "1:name", type: FieldType.STRING }, + }) + await createRows([{ "1:name": "bar" }, { "1:name": "foo" }]) + }) + + it("successfully finds a row", async () => { + await expectQuery({ + equal: { "1:1:name": "bar" }, + }).toContainExactly([{ "1:name": "bar" }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { "1:1:name": "none" }, + }).toFindNothing() + }) + }) + + isSql && + describe("related formulas", () => { + beforeAll(async () => { + const arrayTable = await createTable({ + name: { name: "name", type: FieldType.STRING }, + array: { + name: "array", + type: FieldType.ARRAY, + constraints: { + type: JsonFieldSubType.ARRAY, + inclusion: ["option 1", "option 2"], + }, + }, + }) + tableOrViewId = await createTableOrView({ + relationship: { + type: FieldType.LINK, + relationshipType: RelationshipType.MANY_TO_ONE, + name: "relationship", + fieldName: "relate", + tableId: arrayTable, + constraints: { + type: "array", + }, + }, + formula: { + type: FieldType.FORMULA, + name: "formula", + formula: encodeJSBinding( + `let array = [];$("relationship").forEach(rel => array = array.concat(rel.array));return array.sort().join(",")` + ), + }, + }) + const arrayRows = await Promise.all([ + config.api.row.save(arrayTable, { + name: "foo", + array: ["option 1"], + }), + config.api.row.save(arrayTable, { + name: "bar", + array: ["option 2"], + }), + ]) + await Promise.all([ + config.api.row.save(tableOrViewId, { + relationship: [arrayRows[0]._id, arrayRows[1]._id], + }), + ]) + }) + + it("formula is correct with relationship arrays", async () => { + await expectQuery({}).toContain([ + { formula: "option 1,option 2" }, + ]) + }) + }) + + describe("user", () => { + let user1: User + let user2: User + + beforeAll(async () => { + user1 = await config.createUser({ _id: `us_${utils.newid()}` }) + user2 = await config.createUser({ _id: `us_${utils.newid()}` }) + + tableOrViewId = await createTableOrView({ + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + }, + }) + + await createRows([ + { user: user1 }, + { user: user2 }, + { user: null }, + ]) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { user: user1._id }, + }).toContainExactly([{ user: { _id: user1._id } }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { user: "us_none" }, + }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { user: user1._id }, + }).toContainExactly([{ user: { _id: user2._id } }, {}]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notEqual: { user: "us_none" }, + }).toContainExactly([ + { user: { _id: user1._id } }, + { user: { _id: user2._id } }, + {}, + ]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { user: [user1._id] }, + }).toContainExactly([{ user: { _id: user1._id } }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { user: ["us_none"] }, + }).toFindNothing() + }) + }) + + describe("empty", () => { + it("finds empty rows", async () => { + await expectQuery({ empty: { user: null } }).toContainExactly( + [{}] + ) + }) + }) + + describe("notEmpty", () => { + it("finds non-empty rows", async () => { + await expectQuery({ + notEmpty: { user: null }, + }).toContainExactly([ + { user: { _id: user1._id } }, + { user: { _id: user2._id } }, + ]) + }) + }) + }) + + describe("multi user", () => { + let user1: User + let user2: User + + beforeAll(async () => { + user1 = await config.createUser({ _id: `us_${utils.newid()}` }) + user2 = await config.createUser({ _id: `us_${utils.newid()}` }) + + tableOrViewId = await createTableOrView({ + users: { + name: "users", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + constraints: { type: "array" }, + }, + number: { + name: "number", + type: FieldType.NUMBER, + }, + }) + + await createRows([ + { number: 1, users: [user1] }, + { number: 2, users: [user2] }, + { number: 3, users: [user1, user2] }, + { number: 4, users: [] }, + ]) + }) + + describe("contains", () => { + it("successfully finds a row", async () => { + await expectQuery({ + contains: { users: [user1._id] }, + }).toContainExactly([ + { users: [{ _id: user1._id }] }, + { users: [{ _id: user1._id }, { _id: user2._id }] }, + ]) + }) + + it("successfully finds a row searching with a string", async () => { + await expectQuery({ + // @ts-expect-error this test specifically goes against the type to + // test that we coerce the string to an array. + contains: { "1:users": user1._id }, + }).toContainExactly([ + { users: [{ _id: user1._id }] }, + { users: [{ _id: user1._id }, { _id: user2._id }] }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + contains: { users: ["us_none"] }, + }).toFindNothing() + }) + }) + + describe("notContains", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notContains: { users: [user1._id] }, + }).toContainExactly([{ users: [{ _id: user2._id }] }, {}]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notContains: { users: ["us_none"] }, + }).toContainExactly([ + { users: [{ _id: user1._id }] }, + { users: [{ _id: user2._id }] }, + { users: [{ _id: user1._id }, { _id: user2._id }] }, + {}, + ]) + }) + }) + + describe("containsAny", () => { + it("successfully finds rows", async () => { + await expectQuery({ + containsAny: { users: [user1._id, user2._id] }, + }).toContainExactly([ + { users: [{ _id: user1._id }] }, + { users: [{ _id: user2._id }] }, + { users: [{ _id: user1._id }, { _id: user2._id }] }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + containsAny: { users: ["us_none"] }, + }).toFindNothing() + }) + }) + + describe("multi-column equals", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { number: 1 }, + contains: { users: [user1._id] }, + }).toContainExactly([ + { users: [{ _id: user1._id }], number: 1 }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { number: 2 }, + contains: { users: [user1._id] }, + }).toFindNothing() + }) + }) + }) + + // It also can't work for in-memory searching because the related table name + // isn't available. + !isInMemory && + describe.each([ + RelationshipType.ONE_TO_MANY, + RelationshipType.MANY_TO_ONE, + RelationshipType.MANY_TO_MANY, + ])("relations (%s)", relationshipType => { + let productCategoryTable: Table, productCatRows: Row[] + + beforeAll(async () => { + const { relatedTable, tableId } = + await basicRelationshipTables(relationshipType) + tableOrViewId = tableId + productCategoryTable = relatedTable + + productCatRows = await Promise.all([ + config.api.row.save(productCategoryTable._id!, { + name: "foo", + }), + config.api.row.save(productCategoryTable._id!, { + name: "bar", + }), + ]) + + await Promise.all([ + config.api.row.save(tableOrViewId, { + name: "foo", + productCat: [productCatRows[0]._id], + }), + config.api.row.save(tableOrViewId, { + name: "bar", + productCat: [productCatRows[1]._id], + }), + config.api.row.save(tableOrViewId, { + name: "baz", + productCat: [], + }), + ]) + }) + + it("should be able to filter by relationship using column name", async () => { + await expectQuery({ + equal: { ["productCat.name"]: "foo" }, + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + ]) + }) + + it("should be able to filter by relationship using table name", async () => { + await expectQuery({ + equal: { [`${productCategoryTable.name}.name`]: "foo" }, + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + ]) + }) + + it("shouldn't return any relationship for last row", async () => { + await expectQuery({ + equal: { ["name"]: "baz" }, + }).toContainExactly([{ name: "baz", productCat: undefined }]) + }) + + describe("logical filters", () => { + const logicalOperators = [ + LogicalOperator.AND, + LogicalOperator.OR, + ] + + describe("$and", () => { + it("should allow single conditions", async () => { + await expectQuery({ $and: { conditions: [ { @@ -2499,25 +2546,16 @@ describe.each([ }, ], }, - }, - ], - }, - }).toContainExactly([ - { - name: "foo", - productCat: [{ _id: productCatRows[0]._id }], - }, - ]) - } - ) + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + ]) + }) - it.each([logicalOperators])( - "should allow nested ands with exclusive conditions (with %s as root)", - async rootOperator => { - await expectQuery({ - [rootOperator]: { - conditions: [ - { + it("should allow exclusive conditions", async () => { + await expectQuery({ $and: { conditions: [ { @@ -2526,1168 +2564,1276 @@ describe.each([ }, ], }, - }, - ], - }, - }).toContainExactly([]) - } - ) + }).toContainExactly([]) + }) - it.each([logicalOperators])( - "should allow nested ands with multiple conditions (with %s as root)", - async rootOperator => { - await expectQuery({ - [rootOperator]: { - conditions: [ - { - $and: { - conditions: [ - { - equal: { ["productCat.name"]: "foo" }, - }, - ], - }, - notEqual: { ["productCat.name"]: "foo" }, - }, - ], - }, - }).toContainExactly([]) - } - ) - }) - - describe("$ors", () => { - it("should allow single conditions", async () => { - await expectQuery({ - $or: { - conditions: [ - { - equal: { ["productCat.name"]: "foo" }, - }, - ], - }, - }).toContainExactly([ - { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, - ]) - }) - - it("should allow exclusive conditions", async () => { - await expectQuery({ - $or: { - conditions: [ - { - equal: { ["productCat.name"]: "foo" }, - notEqual: { ["productCat.name"]: "foo" }, - }, - ], - }, - }).toContainExactly([ - { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, - { name: "bar", productCat: [{ _id: productCatRows[1]._id }] }, - { name: "baz", productCat: undefined }, - ]) - }) - - it.each([logicalOperators])( - "should allow nested ors with single conditions (with %s as root)", - async rootOperator => { - await expectQuery({ - [rootOperator]: { - conditions: [ - { - $or: { - conditions: [ - { - equal: { ["productCat.name"]: "foo" }, - }, - ], - }, - }, - ], - }, - }).toContainExactly([ - { - name: "foo", - productCat: [{ _id: productCatRows[0]._id }], - }, - ]) - } - ) - - it.each([logicalOperators])( - "should allow nested ors with exclusive conditions (with %s as root)", - async rootOperator => { - await expectQuery({ - [rootOperator]: { - conditions: [ - { - $or: { - conditions: [ - { - equal: { ["productCat.name"]: "foo" }, - notEqual: { ["productCat.name"]: "foo" }, - }, - ], - }, - }, - ], - }, - }).toContainExactly([ - { - name: "foo", - productCat: [{ _id: productCatRows[0]._id }], - }, - { - name: "bar", - productCat: [{ _id: productCatRows[1]._id }], - }, - { name: "baz", productCat: undefined }, - ]) - } - ) - - it("should allow nested ors with multiple conditions", async () => { - await expectQuery({ - $or: { - conditions: [ - { - $or: { - conditions: [ - { - equal: { ["productCat.name"]: "foo" }, + it.each([logicalOperators])( + "should allow nested ands with single conditions (with %s as root)", + async rootOperator => { + await expectQuery({ + [rootOperator]: { + conditions: [ + { + $and: { + conditions: [ + { + equal: { ["productCat.name"]: "foo" }, + }, + ], + }, + }, + ], }, - ], - }, - notEqual: { ["productCat.name"]: "foo" }, + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + ]) + } + ) + + it.each([logicalOperators])( + "should allow nested ands with exclusive conditions (with %s as root)", + async rootOperator => { + await expectQuery({ + [rootOperator]: { + conditions: [ + { + $and: { + conditions: [ + { + equal: { ["productCat.name"]: "foo" }, + notEqual: { ["productCat.name"]: "foo" }, + }, + ], + }, + }, + ], + }, + }).toContainExactly([]) + } + ) + + it.each([logicalOperators])( + "should allow nested ands with multiple conditions (with %s as root)", + async rootOperator => { + await expectQuery({ + [rootOperator]: { + conditions: [ + { + $and: { + conditions: [ + { + equal: { ["productCat.name"]: "foo" }, + }, + ], + }, + notEqual: { ["productCat.name"]: "foo" }, + }, + ], + }, + }).toContainExactly([]) + } + ) + }) + + describe("$ors", () => { + it("should allow single conditions", async () => { + await expectQuery({ + $or: { + conditions: [ + { + equal: { ["productCat.name"]: "foo" }, + }, + ], + }, + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + ]) + }) + + it("should allow exclusive conditions", async () => { + await expectQuery({ + $or: { + conditions: [ + { + equal: { ["productCat.name"]: "foo" }, + notEqual: { ["productCat.name"]: "foo" }, + }, + ], + }, + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + { + name: "bar", + productCat: [{ _id: productCatRows[1]._id }], + }, + { name: "baz", productCat: undefined }, + ]) + }) + + it.each([logicalOperators])( + "should allow nested ors with single conditions (with %s as root)", + async rootOperator => { + await expectQuery({ + [rootOperator]: { + conditions: [ + { + $or: { + conditions: [ + { + equal: { ["productCat.name"]: "foo" }, + }, + ], + }, + }, + ], + }, + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + ]) + } + ) + + it.each([logicalOperators])( + "should allow nested ors with exclusive conditions (with %s as root)", + async rootOperator => { + await expectQuery({ + [rootOperator]: { + conditions: [ + { + $or: { + conditions: [ + { + equal: { ["productCat.name"]: "foo" }, + notEqual: { ["productCat.name"]: "foo" }, + }, + ], + }, + }, + ], + }, + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + { + name: "bar", + productCat: [{ _id: productCatRows[1]._id }], + }, + { name: "baz", productCat: undefined }, + ]) + } + ) + + it("should allow nested ors with multiple conditions", async () => { + await expectQuery({ + $or: { + conditions: [ + { + $or: { + conditions: [ + { + equal: { ["productCat.name"]: "foo" }, + }, + ], + }, + notEqual: { ["productCat.name"]: "foo" }, + }, + ], + }, + }).toContainExactly([ + { + name: "foo", + productCat: [{ _id: productCatRows[0]._id }], + }, + { + name: "bar", + productCat: [{ _id: productCatRows[1]._id }], + }, + { name: "baz", productCat: undefined }, + ]) + }) + }) + }) + }) + + isSql && + describe.each([ + RelationshipType.MANY_TO_ONE, + RelationshipType.MANY_TO_MANY, + ])("big relations (%s)", relationshipType => { + beforeAll(async () => { + const { relatedTable, tableId } = + await basicRelationshipTables(relationshipType) + tableOrViewId = tableId + const mainRow = await config.api.row.save(tableOrViewId, { + name: "foo", + }) + for (let i = 0; i < 11; i++) { + await config.api.row.save(relatedTable._id!, { + name: i, + product: [mainRow._id!], + }) + } + }) + + it("can only pull 10 related rows", async () => { + await withCoreEnv( + { SQL_MAX_RELATED_ROWS: "10" }, + async () => { + const response = await expectQuery({}).toContain([ + { name: "foo" }, + ]) + expect(response.rows[0].productCat).toBeArrayOfSize(10) + } + ) + }) + + it("can pull max rows when env not set (defaults to 500)", async () => { + const response = await expectQuery({}).toContain([ + { name: "foo" }, + ]) + expect(response.rows[0].productCat).toBeArrayOfSize(11) + }) + }) + + isSql && + describe("relations to same table", () => { + let relatedTable: string, relatedRows: Row[] + + beforeAll(async () => { + relatedTable = await createTable({ + name: { name: "name", type: FieldType.STRING }, + }) + tableOrViewId = await createTableOrView({ + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTable, + relationshipType: RelationshipType.MANY_TO_MANY, }, - ], - }, - }).toContainExactly([ - { name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, - { name: "bar", productCat: [{ _id: productCatRows[1]._id }] }, - { name: "baz", productCat: undefined }, - ]) + related2: { + type: FieldType.LINK, + name: "related2", + fieldName: "main2", + tableId: relatedTable, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }) + relatedRows = await Promise.all([ + config.api.row.save(relatedTable, { name: "foo" }), + config.api.row.save(relatedTable, { name: "bar" }), + config.api.row.save(relatedTable, { name: "baz" }), + config.api.row.save(relatedTable, { name: "boo" }), + ]) + await Promise.all([ + config.api.row.save(tableOrViewId, { + name: "test", + related1: [relatedRows[0]._id!], + related2: [relatedRows[1]._id!], + }), + config.api.row.save(tableOrViewId, { + name: "test2", + related1: [relatedRows[2]._id!], + related2: [relatedRows[3]._id!], + }), + config.api.row.save(tableOrViewId, { + name: "test3", + related1: [relatedRows[1]._id], + related2: [relatedRows[2]._id!], + }), + ]) + }) + + it("should be able to relate to same table", async () => { + await expectSearch({ + query: {}, + }).toContainExactly([ + { + name: "test", + related1: [{ _id: relatedRows[0]._id }], + related2: [{ _id: relatedRows[1]._id }], + }, + { + name: "test2", + related1: [{ _id: relatedRows[2]._id }], + related2: [{ _id: relatedRows[3]._id }], + }, + { + name: "test3", + related1: [{ _id: relatedRows[1]._id }], + related2: [{ _id: relatedRows[2]._id }], + }, + ]) + }) + + it("should be able to filter via the first relation field with equal", async () => { + await expectSearch({ + query: { + equal: { + ["related1.name"]: "baz", + }, + }, + }).toContainExactly([ + { + name: "test2", + related1: [{ _id: relatedRows[2]._id }], + }, + ]) + }) + + it("should be able to filter via the second relation field with not equal", async () => { + await expectSearch({ + query: { + notEqual: { + ["1:related2.name"]: "foo", + ["2:related2.name"]: "baz", + ["3:related2.name"]: "boo", + }, + }, + }).toContainExactly([ + { + name: "test", + }, + ]) + }) + + it("should be able to filter on both fields", async () => { + await expectSearch({ + query: { + notEqual: { + ["related1.name"]: "foo", + ["related2.name"]: "baz", + }, + }, + }).toContainExactly([ + { + name: "test2", + }, + ]) + }) + }) + + isInternal && + describe("no column error backwards compat", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + name: { + name: "name", + type: FieldType.STRING, + }, + }) + }) + + it("shouldn't error when column doesn't exist", async () => { + await expectSearch({ + query: { + string: { + "1:something": "a", + }, + }, + }).toMatch({ rows: [] }) + }) + }) + + describe("row counting", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + name: { + name: "name", + type: FieldType.STRING, + }, + }) + await createRows([{ name: "a" }, { name: "b" }]) + }) + + it("should be able to count rows when option set", async () => { + await expectSearch({ + countRows: true, + query: { + notEmpty: { + name: true, + }, + }, + }).toMatch({ totalRows: 2, rows: expect.any(Array) }) + }) + + it("shouldn't count rows when option is not set", async () => { + await expectSearch({ + countRows: false, + query: { + notEmpty: { + name: true, + }, + }, + }).toNotHaveProperty(["totalRows"]) + }) }) - }) - }) - }) - isSql && - describe.each([ - RelationshipType.MANY_TO_ONE, - RelationshipType.MANY_TO_MANY, - ])("big relations (%s)", relationshipType => { - beforeAll(async () => { - const { relatedTable, tableId } = await basicRelationshipTables( - relationshipType - ) - tableOrViewId = tableId - const mainRow = await config.api.row.save(tableOrViewId, { - name: "foo", - }) - for (let i = 0; i < 11; i++) { - await config.api.row.save(relatedTable._id!, { - name: i, - product: [mainRow._id!], + describe("Invalid column definitions", () => { + beforeAll(async () => { + // need to create an invalid table - means ignoring typescript + tableOrViewId = await createTableOrView({ + // @ts-ignore + invalid: { + type: FieldType.STRING, + }, + name: { + name: "name", + type: FieldType.STRING, + }, + }) + await createRows([ + { name: "foo", invalid: "id1" }, + { name: "bar", invalid: "id2" }, + ]) + }) + + it("can get rows with all table data", async () => { + await expectSearch({ + query: {}, + }).toContain([ + { name: "foo", invalid: "id1" }, + { name: "bar", invalid: "id2" }, + ]) + }) }) - } - }) - it("can only pull 10 related rows", async () => { - await withCoreEnv({ SQL_MAX_RELATED_ROWS: "10" }, async () => { - const response = await expectQuery({}).toContain([{ name: "foo" }]) - expect(response.rows[0].productCat).toBeArrayOfSize(10) - }) - }) + describe.each([ + "data_name_test", + "name_data_test", + "name_test_data_", + ])("special (%s) case", column => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + [column]: { + name: column, + type: FieldType.STRING, + }, + }) + await createRows([{ [column]: "a" }, { [column]: "b" }]) + }) - it("can pull max rows when env not set (defaults to 500)", async () => { - const response = await expectQuery({}).toContain([{ name: "foo" }]) - expect(response.rows[0].productCat).toBeArrayOfSize(11) - }) - }) - - isSql && - describe("relations to same table", () => { - let relatedTable: string, relatedRows: Row[] - - beforeAll(async () => { - relatedTable = await createTable({ - name: { name: "name", type: FieldType.STRING }, - }) - tableOrViewId = await createTableOrView({ - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTable, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - related2: { - type: FieldType.LINK, - name: "related2", - fieldName: "main2", - tableId: relatedTable, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }) - relatedRows = await Promise.all([ - config.api.row.save(relatedTable, { name: "foo" }), - config.api.row.save(relatedTable, { name: "bar" }), - config.api.row.save(relatedTable, { name: "baz" }), - config.api.row.save(relatedTable, { name: "boo" }), - ]) - await Promise.all([ - config.api.row.save(tableOrViewId, { - name: "test", - related1: [relatedRows[0]._id!], - related2: [relatedRows[1]._id!], - }), - config.api.row.save(tableOrViewId, { - name: "test2", - related1: [relatedRows[2]._id!], - related2: [relatedRows[3]._id!], - }), - config.api.row.save(tableOrViewId, { - name: "test3", - related1: [relatedRows[1]._id], - related2: [relatedRows[2]._id!], - }), - ]) - }) - - it("should be able to relate to same table", async () => { - await expectSearch({ - query: {}, - }).toContainExactly([ - { - name: "test", - related1: [{ _id: relatedRows[0]._id }], - related2: [{ _id: relatedRows[1]._id }], - }, - { - name: "test2", - related1: [{ _id: relatedRows[2]._id }], - related2: [{ _id: relatedRows[3]._id }], - }, - { - name: "test3", - related1: [{ _id: relatedRows[1]._id }], - related2: [{ _id: relatedRows[2]._id }], - }, - ]) - }) - - it("should be able to filter via the first relation field with equal", async () => { - await expectSearch({ - query: { - equal: { - ["related1.name"]: "baz", - }, - }, - }).toContainExactly([ - { - name: "test2", - related1: [{ _id: relatedRows[2]._id }], - }, - ]) - }) - - it("should be able to filter via the second relation field with not equal", async () => { - await expectSearch({ - query: { - notEqual: { - ["1:related2.name"]: "foo", - ["2:related2.name"]: "baz", - ["3:related2.name"]: "boo", - }, - }, - }).toContainExactly([ - { - name: "test", - }, - ]) - }) - - it("should be able to filter on both fields", async () => { - await expectSearch({ - query: { - notEqual: { - ["related1.name"]: "foo", - ["related2.name"]: "baz", - }, - }, - }).toContainExactly([ - { - name: "test2", - }, - ]) - }) - }) - - isInternal && - describe("no column error backwards compat", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - }) - - it("shouldn't error when column doesn't exist", async () => { - await expectSearch({ - query: { - string: { - "1:something": "a", - }, - }, - }).toMatch({ rows: [] }) - }) - }) - - // lucene can't count the total rows - !isLucene && - describe("row counting", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - await createRows([{ name: "a" }, { name: "b" }]) - }) - - it("should be able to count rows when option set", async () => { - await expectSearch({ - countRows: true, - query: { - notEmpty: { - name: true, - }, - }, - }).toMatch({ totalRows: 2, rows: expect.any(Array) }) - }) - - it("shouldn't count rows when option is not set", async () => { - await expectSearch({ - countRows: false, - query: { - notEmpty: { - name: true, - }, - }, - }).toNotHaveProperty(["totalRows"]) - }) - }) - - describe("Invalid column definitions", () => { - beforeAll(async () => { - // need to create an invalid table - means ignoring typescript - tableOrViewId = await createTableOrView({ - // @ts-ignore - invalid: { - type: FieldType.STRING, - }, - name: { - name: "name", - type: FieldType.STRING, - }, - }) - await createRows([ - { name: "foo", invalid: "id1" }, - { name: "bar", invalid: "id2" }, - ]) - }) - - it("can get rows with all table data", async () => { - await expectSearch({ - query: {}, - }).toContain([ - { name: "foo", invalid: "id1" }, - { name: "bar", invalid: "id2" }, - ]) - }) - }) - - describe.each(["data_name_test", "name_data_test", "name_test_data_"])( - "special (%s) case", - column => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - [column]: { - name: column, - type: FieldType.STRING, - }, - }) - await createRows([{ [column]: "a" }, { [column]: "b" }]) - }) - - it("should be able to query a column with data_ in it", async () => { - await expectSearch({ - query: { - equal: { - [`1:${column}`]: "a", - }, - }, - }).toContainExactly([{ [column]: "a" }]) - }) - } - ) - - isInternal && - describe("sample data", () => { - beforeAll(async () => { - await config.api.application.addSampleData(config.appId!) - tableOrViewId = DEFAULT_EMPLOYEE_TABLE_SCHEMA._id! - rows = await config.api.row.fetch(tableOrViewId) - }) - - it("should be able to search sample data", async () => { - await expectSearch({ - query: {}, - }).toContain([ - { - "First Name": "Mandy", - }, - ]) - }) - }) - - describe.each([ - { low: "2024-07-03T00:00:00.000Z", high: "9999-00-00T00:00:00.000Z" }, - { low: "2024-07-03T00:00:00.000Z", high: "9998-00-00T00:00:00.000Z" }, - { low: "0000-00-00T00:00:00.000Z", high: "2024-07-04T00:00:00.000Z" }, - { low: "0001-00-00T00:00:00.000Z", high: "2024-07-04T00:00:00.000Z" }, - ])("date special cases", ({ low, high }) => { - const earlyDate = "2024-07-03T10:00:00.000Z", - laterDate = "2024-07-03T11:00:00.000Z" - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - date: { - name: "date", - type: FieldType.DATETIME, - }, - }) - await createRows([{ date: earlyDate }, { date: laterDate }]) - }) - - it("should be able to handle a date search", async () => { - await expectSearch({ - query: { - range: { - "1:date": { low, high }, - }, - }, - }).toContainExactly([{ date: earlyDate }, { date: laterDate }]) - }) - }) - - describe.each([ - "名前", // Japanese for "name" - "Benutzer-ID", // German for "user ID", includes a hyphen - "numéro", // French for "number", includes an accent - "år", // Swedish for "year", includes a ring above - "naïve", // English word borrowed from French, includes an umlaut - "الاسم", // Arabic for "name" - "оплата", // Russian for "payment" - "पता", // Hindi for "address" - "用戶名", // Chinese for "username" - "çalışma_zamanı", // Turkish for "runtime", includes an underscore and a cedilla - "preço", // Portuguese for "price", includes a cedilla - "사용자명", // Korean for "username" - "usuario_ñoño", // Spanish, uses an underscore and includes "ñ" - "файл", // Bulgarian for "file" - "δεδομένα", // Greek for "data" - "geändert_am", // German for "modified on", includes an umlaut - "ব্যবহারকারীর_নাম", // Bengali for "user name", includes an underscore - "São_Paulo", // Portuguese, includes an underscore and a tilde - "età", // Italian for "age", includes an accent - "ชื่อผู้ใช้", // Thai for "username" - ])("non-ascii column name: %s", name => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - [name]: { - name, - type: FieldType.STRING, - }, - }) - await createRows([{ [name]: "a" }, { [name]: "b" }]) - }) - - it("should be able to query a column with non-ascii characters", async () => { - await expectSearch({ - query: { - equal: { - [`1:${name}`]: "a", - }, - }, - }).toContainExactly([{ [name]: "a" }]) - }) - }) - - // This is currently not supported in external datasources, it produces SQL - // errors at time of writing. We supported it (potentially by accident) in - // Lucene, though, so we need to make sure it's supported in SQS as well. We - // found real cases in production of column names ending in a space. - isInternal && - describe("space at end of column name", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - "name ": { - name: "name ", - type: FieldType.STRING, - }, - }) - await createRows([{ ["name "]: "foo" }, { ["name "]: "bar" }]) - }) - - it("should be able to query a column that ends with a space", async () => { - await expectSearch({ - query: { - string: { - "name ": "foo", - }, - }, - }).toContainExactly([{ ["name "]: "foo" }]) - }) - - it("should be able to query a column that ends with a space using numeric notation", async () => { - await expectSearch({ - query: { - string: { - "1:name ": "foo", - }, - }, - }).toContainExactly([{ ["name "]: "foo" }]) - }) - }) - - // This was never actually supported in Lucene but SQS does support it, so may - // as well have a test for it. - ;(isSqs || isInMemory) && - describe("space at start of column name", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - " name": { - name: " name", - type: FieldType.STRING, - }, - }) - await createRows([{ [" name"]: "foo" }, { [" name"]: "bar" }]) - }) - - it("should be able to query a column that starts with a space", async () => { - await expectSearch({ - query: { - string: { - " name": "foo", - }, - }, - }).toContainExactly([{ [" name"]: "foo" }]) - }) - - it("should be able to query a column that starts with a space using numeric notation", async () => { - await expectSearch({ - query: { - string: { - "1: name": "foo", - }, - }, - }).toContainExactly([{ [" name"]: "foo" }]) - }) - }) - - isSqs && - !isView && - describe("duplicate columns", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - await context.doInAppContext(config.getAppId(), async () => { - const db = context.getAppDB() - const tableDoc = await db.get
(tableOrViewId) - tableDoc.schema.Name = { - name: "Name", - type: FieldType.STRING, - } - try { - // remove the SQLite definitions so that they can be rebuilt as part of the search - const sqliteDoc = await db.get(SQLITE_DESIGN_DOC_ID) - await db.remove(sqliteDoc) - } catch (err) { - // no-op - } - }) - await createRows([{ name: "foo", Name: "bar" }]) - }) - - it("should handle invalid duplicate column names", async () => { - await expectSearch({ - query: {}, - }).toContainExactly([{ name: "foo" }]) - }) - }) - - !isInMemory && - describe("search by _id", () => { - let row: Row - - beforeAll(async () => { - const toRelateTable = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - tableOrViewId = await createTableOrView({ - name: { - name: "name", - type: FieldType.STRING, - }, - rel: { - name: "rel", - type: FieldType.LINK, - relationshipType: RelationshipType.MANY_TO_MANY, - tableId: toRelateTable, - fieldName: "rel", - }, - }) - const [row1, row2] = await Promise.all([ - config.api.row.save(toRelateTable, { name: "tag 1" }), - config.api.row.save(toRelateTable, { name: "tag 2" }), - ]) - row = await config.api.row.save(tableOrViewId, { - name: "product 1", - rel: [row1._id, row2._id], - }) - }) - - it("can filter by the row ID with limit 1", async () => { - await expectSearch({ - query: { - equal: { _id: row._id }, - }, - limit: 1, - }).toContainExactly([row]) - }) - }) - - !isInternal && - describe("search by composite key", () => { - beforeAll(async () => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - idColumn1: { - name: "idColumn1", - type: FieldType.NUMBER, - }, - idColumn2: { - name: "idColumn2", - type: FieldType.NUMBER, - }, - }, - primary: ["idColumn1", "idColumn2"], + it("should be able to query a column with data_ in it", async () => { + await expectSearch({ + query: { + equal: { + [`1:${column}`]: "a", + }, + }, + }).toContainExactly([{ [column]: "a" }]) + }) }) - ) - tableOrViewId = table._id! - await createRows([{ idColumn1: 1, idColumn2: 2 }]) - }) - it("can filter by the row ID with limit 1", async () => { - await expectSearch({ - query: { - equal: { _id: generateRowIdField([1, 2]) }, - }, - limit: 1, - }).toContain([ - { - idColumn1: 1, - idColumn2: 2, - }, - ]) - }) - }) + isInternal && + describe("sample data", () => { + beforeAll(async () => { + await config.api.application.addSampleData(config.appId!) + tableOrViewId = DEFAULT_EMPLOYEE_TABLE_SCHEMA._id! + rows = await config.api.row.fetch(tableOrViewId) + }) - isSql && - describe("primaryDisplay", () => { - beforeAll(async () => { - let toRelateTableId = await createTable({ - name: { - name: "name", - type: FieldType.STRING, - }, - }) - tableOrViewId = await createTableOrView({ - name: { - name: "name", - type: FieldType.STRING, - }, - link: { - name: "link", - type: FieldType.LINK, - relationshipType: RelationshipType.MANY_TO_ONE, - tableId: toRelateTableId, - fieldName: "link", - }, - }) + it("should be able to search sample data", async () => { + await expectSearch({ + query: {}, + }).toContain([ + { + "First Name": "Mandy", + }, + ]) + }) + }) - const toRelateTable = await config.api.table.get(toRelateTableId) - await config.api.table.save({ - ...toRelateTable, - primaryDisplay: "link", - }) - const relatedRows = await Promise.all([ - config.api.row.save(toRelateTable._id!, { name: "related" }), - ]) - await config.api.row.save(tableOrViewId, { - name: "test", - link: relatedRows.map(row => row._id), - }) - }) + describe.each([ + { + low: "2024-07-03T00:00:00.000Z", + high: "9999-00-00T00:00:00.000Z", + }, + { + low: "2024-07-03T00:00:00.000Z", + high: "9998-00-00T00:00:00.000Z", + }, + { + low: "0000-00-00T00:00:00.000Z", + high: "2024-07-04T00:00:00.000Z", + }, + { + low: "0001-00-00T00:00:00.000Z", + high: "2024-07-04T00:00:00.000Z", + }, + ])("date special cases", ({ low, high }) => { + const earlyDate = "2024-07-03T10:00:00.000Z", + laterDate = "2024-07-03T11:00:00.000Z" + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + date: { + name: "date", + type: FieldType.DATETIME, + }, + }) + await createRows([{ date: earlyDate }, { date: laterDate }]) + }) - it("should be able to query, primary display on related table shouldn't be used", async () => { - // this test makes sure that if a relationship has been specified as the primary display on a table - // it is ignored and another column is used instead - await expectQuery({}).toContain([ - { name: "test", link: [{ primaryDisplay: "related" }] }, - ]) - }) - }) + it("should be able to handle a date search", async () => { + await expectSearch({ + query: { + range: { + "1:date": { low, high }, + }, + }, + }).toContainExactly([{ date: earlyDate }, { date: laterDate }]) + }) + }) - !isLucene && - describe("$and", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - age: { name: "age", type: FieldType.NUMBER }, - name: { name: "name", type: FieldType.STRING }, - }) - await createRows([ - { age: 1, name: "Jane" }, - { age: 10, name: "Jack" }, - { age: 7, name: "Hanna" }, - { age: 8, name: "Jan" }, - ]) - }) + describe.each([ + "名前", // Japanese for "name" + "Benutzer-ID", // German for "user ID", includes a hyphen + "numéro", // French for "number", includes an accent + "år", // Swedish for "year", includes a ring above + "naïve", // English word borrowed from French, includes an umlaut + "الاسم", // Arabic for "name" + "оплата", // Russian for "payment" + "पता", // Hindi for "address" + "用戶名", // Chinese for "username" + "çalışma_zamanı", // Turkish for "runtime", includes an underscore and a cedilla + "preço", // Portuguese for "price", includes a cedilla + "사용자명", // Korean for "username" + "usuario_ñoño", // Spanish, uses an underscore and includes "ñ" + "файл", // Bulgarian for "file" + "δεδομένα", // Greek for "data" + "geändert_am", // German for "modified on", includes an umlaut + "ব্যবহারকারীর_নাম", // Bengali for "user name", includes an underscore + "São_Paulo", // Portuguese, includes an underscore and a tilde + "età", // Italian for "age", includes an accent + "ชื่อผู้ใช้", // Thai for "username" + ])("non-ascii column name: %s", name => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + [name]: { + name, + type: FieldType.STRING, + }, + }) + await createRows([{ [name]: "a" }, { [name]: "b" }]) + }) - it("successfully finds a row for one level condition", async () => { - await expectQuery({ - $and: { - conditions: [{ equal: { age: 10 } }, { equal: { name: "Jack" } }], - }, - }).toContainExactly([{ age: 10, name: "Jack" }]) - }) + it("should be able to query a column with non-ascii characters", async () => { + await expectSearch({ + query: { + equal: { + [`1:${name}`]: "a", + }, + }, + }).toContainExactly([{ [name]: "a" }]) + }) + }) - it("successfully finds a row for one level with multiple conditions", async () => { - await expectQuery({ - $and: { - conditions: [{ equal: { age: 10 } }, { equal: { name: "Jack" } }], - }, - }).toContainExactly([{ age: 10, name: "Jack" }]) - }) + // This is currently not supported in external datasources, it produces SQL + // errors at time of writing. We supported it (potentially by accident) in + // Lucene, though, so we need to make sure it's supported in SQS as well. We + // found real cases in production of column names ending in a space. + isInternal && + describe("space at end of column name", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + "name ": { + name: "name ", + type: FieldType.STRING, + }, + }) + await createRows([{ ["name "]: "foo" }, { ["name "]: "bar" }]) + }) - it("successfully finds multiple rows for one level with multiple conditions", async () => { - await expectQuery({ - $and: { - conditions: [ - { range: { age: { low: 1, high: 9 } } }, - { string: { name: "Ja" } }, - ], - }, - }).toContainExactly([ - { age: 1, name: "Jane" }, - { age: 8, name: "Jan" }, - ]) - }) + it("should be able to query a column that ends with a space", async () => { + await expectSearch({ + query: { + string: { + "name ": "foo", + }, + }, + }).toContainExactly([{ ["name "]: "foo" }]) + }) - it("successfully finds rows for nested filters", async () => { - await expectQuery({ - $and: { - conditions: [ - { + it("should be able to query a column that ends with a space using numeric notation", async () => { + await expectSearch({ + query: { + string: { + "1:name ": "foo", + }, + }, + }).toContainExactly([{ ["name "]: "foo" }]) + }) + }) + + isInternal && + describe("space at start of column name", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + " name": { + name: " name", + type: FieldType.STRING, + }, + }) + await createRows([{ [" name"]: "foo" }, { [" name"]: "bar" }]) + }) + + it("should be able to query a column that starts with a space", async () => { + await expectSearch({ + query: { + string: { + " name": "foo", + }, + }, + }).toContainExactly([{ [" name"]: "foo" }]) + }) + + it("should be able to query a column that starts with a space using numeric notation", async () => { + await expectSearch({ + query: { + string: { + "1: name": "foo", + }, + }, + }).toContainExactly([{ [" name"]: "foo" }]) + }) + }) + + isInternal && + !isView && + describe("duplicate columns", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + name: { + name: "name", + type: FieldType.STRING, + }, + }) + await context.doInAppContext(config.getAppId(), async () => { + const db = context.getAppDB() + const tableDoc = await db.get
(tableOrViewId) + tableDoc.schema.Name = { + name: "Name", + type: FieldType.STRING, + } + try { + // remove the SQLite definitions so that they can be rebuilt as part of the search + const sqliteDoc = await db.get(SQLITE_DESIGN_DOC_ID) + await db.remove(sqliteDoc) + } catch (err) { + // no-op + } + }) + await createRows([{ name: "foo", Name: "bar" }]) + }) + + it("should handle invalid duplicate column names", async () => { + await expectSearch({ + query: {}, + }).toContainExactly([{ name: "foo" }]) + }) + }) + + !isInMemory && + describe("search by _id", () => { + let row: Row + + beforeAll(async () => { + const toRelateTable = await createTable({ + name: { + name: "name", + type: FieldType.STRING, + }, + }) + tableOrViewId = await createTableOrView({ + name: { + name: "name", + type: FieldType.STRING, + }, + rel: { + name: "rel", + type: FieldType.LINK, + relationshipType: RelationshipType.MANY_TO_MANY, + tableId: toRelateTable, + fieldName: "rel", + }, + }) + const [row1, row2] = await Promise.all([ + config.api.row.save(toRelateTable, { name: "tag 1" }), + config.api.row.save(toRelateTable, { name: "tag 2" }), + ]) + row = await config.api.row.save(tableOrViewId, { + name: "product 1", + rel: [row1._id, row2._id], + }) + }) + + it("can filter by the row ID with limit 1", async () => { + await expectSearch({ + query: { + equal: { _id: row._id }, + }, + limit: 1, + }).toContainExactly([row]) + }) + }) + + !isInternal && + describe("search by composite key", () => { + beforeAll(async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + idColumn1: { + name: "idColumn1", + type: FieldType.NUMBER, + }, + idColumn2: { + name: "idColumn2", + type: FieldType.NUMBER, + }, + }, + primary: ["idColumn1", "idColumn2"], + }) + ) + tableOrViewId = table._id! + await createRows([{ idColumn1: 1, idColumn2: 2 }]) + }) + + it("can filter by the row ID with limit 1", async () => { + await expectSearch({ + query: { + equal: { _id: generateRowIdField([1, 2]) }, + }, + limit: 1, + }).toContain([ + { + idColumn1: 1, + idColumn2: 2, + }, + ]) + }) + }) + + isSql && + describe("primaryDisplay", () => { + beforeAll(async () => { + let toRelateTableId = await createTable({ + name: { + name: "name", + type: FieldType.STRING, + }, + }) + tableOrViewId = await createTableOrView({ + name: { + name: "name", + type: FieldType.STRING, + }, + link: { + name: "link", + type: FieldType.LINK, + relationshipType: RelationshipType.MANY_TO_ONE, + tableId: toRelateTableId, + fieldName: "link", + }, + }) + + const toRelateTable = await config.api.table.get( + toRelateTableId + ) + await config.api.table.save({ + ...toRelateTable, + primaryDisplay: "link", + }) + const relatedRows = await Promise.all([ + config.api.row.save(toRelateTable._id!, { + name: "related", + }), + ]) + await config.api.row.save(tableOrViewId, { + name: "test", + link: relatedRows.map(row => row._id), + }) + }) + + it("should be able to query, primary display on related table shouldn't be used", async () => { + // this test makes sure that if a relationship has been specified as the primary display on a table + // it is ignored and another column is used instead + await expectQuery({}).toContain([ + { name: "test", link: [{ primaryDisplay: "related" }] }, + ]) + }) + }) + + describe("$and", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + age: { name: "age", type: FieldType.NUMBER }, + name: { name: "name", type: FieldType.STRING }, + }) + await createRows([ + { age: 1, name: "Jane" }, + { age: 10, name: "Jack" }, + { age: 7, name: "Hanna" }, + { age: 8, name: "Jan" }, + ]) + }) + + it("successfully finds a row for one level condition", async () => { + await expectQuery({ $and: { conditions: [ - { - range: { age: { low: 1, high: 10 } }, - }, + { equal: { age: 10 } }, + { equal: { name: "Jack" } }, + ], + }, + }).toContainExactly([{ age: 10, name: "Jack" }]) + }) + + it("successfully finds a row for one level with multiple conditions", async () => { + await expectQuery({ + $and: { + conditions: [ + { equal: { age: 10 } }, + { equal: { name: "Jack" } }, + ], + }, + }).toContainExactly([{ age: 10, name: "Jack" }]) + }) + + it("successfully finds multiple rows for one level with multiple conditions", async () => { + await expectQuery({ + $and: { + conditions: [ + { range: { age: { low: 1, high: 9 } } }, { string: { name: "Ja" } }, ], }, - equal: { name: "Jane" }, - }, - ], - }, - }).toContainExactly([{ age: 1, name: "Jane" }]) - }) + }).toContainExactly([ + { age: 1, name: "Jane" }, + { age: 8, name: "Jan" }, + ]) + }) - it("returns nothing when filtering out all data", async () => { - await expectQuery({ - $and: { - conditions: [{ equal: { age: 7 } }, { equal: { name: "Jack" } }], - }, - }).toFindNothing() - }) - - !isInMemory && - it("validates conditions that are not objects", async () => { - await expect( - expectQuery({ - $and: { - conditions: [ - { equal: { age: 10 } }, - "invalidCondition" as any, - ], - }, - }).toFindNothing() - ).rejects.toThrow( - 'Invalid body - "query.$and.conditions[1]" must be of type object' - ) - }) - - !isInMemory && - it("validates $and without conditions", async () => { - await expect( - expectQuery({ - $and: { - conditions: [ - { equal: { age: 10 } }, - { - $and: { - conditions: undefined as any, - }, - }, - ], - }, - }).toFindNothing() - ).rejects.toThrow( - 'Invalid body - "query.$and.conditions[1].$and.conditions" is required' - ) - }) - - // onEmptyFilter cannot be sent to view searches - !isView && - it("returns no rows when onEmptyFilter set to none", async () => { - await expectSearch({ - query: { - onEmptyFilter: EmptyFilterOption.RETURN_NONE, - $and: { - conditions: [{ equal: { name: "" } }], - }, - }, - }).toFindNothing() - }) - - it("returns all rows when onEmptyFilter set to all", async () => { - await expectSearch({ - query: { - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - $and: { - conditions: [{ equal: { name: "" } }], - }, - }, - }).toHaveLength(4) - }) - }) - - !isLucene && - describe("$or", () => { - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - age: { name: "age", type: FieldType.NUMBER }, - name: { name: "name", type: FieldType.STRING }, - }) - await createRows([ - { age: 1, name: "Jane" }, - { age: 10, name: "Jack" }, - { age: 7, name: "Hanna" }, - { age: 8, name: "Jan" }, - ]) - }) - - it("successfully finds a row for one level condition", async () => { - await expectQuery({ - $or: { - conditions: [{ equal: { age: 7 } }, { equal: { name: "Jack" } }], - }, - }).toContainExactly([ - { age: 10, name: "Jack" }, - { age: 7, name: "Hanna" }, - ]) - }) - - it("successfully finds a row for one level with multiple conditions", async () => { - await expectQuery({ - $or: { - conditions: [{ equal: { age: 7 } }, { equal: { name: "Jack" } }], - }, - }).toContainExactly([ - { age: 10, name: "Jack" }, - { age: 7, name: "Hanna" }, - ]) - }) - - it("successfully finds multiple rows for one level with multiple conditions", async () => { - await expectQuery({ - $or: { - conditions: [ - { range: { age: { low: 1, high: 9 } } }, - { string: { name: "Jan" } }, - ], - }, - }).toContainExactly([ - { age: 1, name: "Jane" }, - { age: 7, name: "Hanna" }, - { age: 8, name: "Jan" }, - ]) - }) - - it("successfully finds rows for nested filters", async () => { - await expectQuery({ - $or: { - conditions: [ - { - $or: { - conditions: [ - { - range: { age: { low: 1, high: 7 } }, - }, - { string: { name: "Jan" } }, - ], - }, - equal: { name: "Jane" }, - }, - ], - }, - }).toContainExactly([ - { age: 1, name: "Jane" }, - { age: 7, name: "Hanna" }, - { age: 8, name: "Jan" }, - ]) - }) - - it("returns nothing when filtering out all data", async () => { - await expectQuery({ - $or: { - conditions: [{ equal: { age: 6 } }, { equal: { name: "John" } }], - }, - }).toFindNothing() - }) - - it("can nest $and under $or filters", async () => { - await expectQuery({ - $or: { - conditions: [ - { + it("successfully finds rows for nested filters", async () => { + await expectQuery({ $and: { conditions: [ { - range: { age: { low: 1, high: 8 } }, + $and: { + conditions: [ + { + range: { age: { low: 1, high: 10 } }, + }, + { string: { name: "Ja" } }, + ], + }, + equal: { name: "Jane" }, }, - { equal: { name: "Jan" } }, ], }, - equal: { name: "Jane" }, - }, - ], - }, - }).toContainExactly([ - { age: 1, name: "Jane" }, - { age: 8, name: "Jan" }, - ]) - }) + }).toContainExactly([{ age: 1, name: "Jane" }]) + }) - it("can nest $or under $and filters", async () => { - await expectQuery({ - $and: { - conditions: [ - { + it("returns nothing when filtering out all data", async () => { + await expectQuery({ + $and: { + conditions: [ + { equal: { age: 7 } }, + { equal: { name: "Jack" } }, + ], + }, + }).toFindNothing() + }) + + !isInMemory && + it("validates conditions that are not objects", async () => { + await expect( + expectQuery({ + $and: { + conditions: [ + { equal: { age: 10 } }, + "invalidCondition" as any, + ], + }, + }).toFindNothing() + ).rejects.toThrow( + 'Invalid body - "query.$and.conditions[1]" must be of type object' + ) + }) + + !isInMemory && + it("validates $and without conditions", async () => { + await expect( + expectQuery({ + $and: { + conditions: [ + { equal: { age: 10 } }, + { + $and: { + conditions: undefined as any, + }, + }, + ], + }, + }).toFindNothing() + ).rejects.toThrow( + 'Invalid body - "query.$and.conditions[1].$and.conditions" is required' + ) + }) + + // onEmptyFilter cannot be sent to view searches + !isView && + it("returns no rows when onEmptyFilter set to none", async () => { + await expectSearch({ + query: { + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + $and: { + conditions: [{ equal: { name: "" } }], + }, + }, + }).toFindNothing() + }) + + it("returns all rows when onEmptyFilter set to all", async () => { + await expectSearch({ + query: { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + $and: { + conditions: [{ equal: { name: "" } }], + }, + }, + }).toHaveLength(4) + }) + }) + + describe("$or", () => { + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + age: { name: "age", type: FieldType.NUMBER }, + name: { name: "name", type: FieldType.STRING }, + }) + await createRows([ + { age: 1, name: "Jane" }, + { age: 10, name: "Jack" }, + { age: 7, name: "Hanna" }, + { age: 8, name: "Jan" }, + ]) + }) + + it("successfully finds a row for one level condition", async () => { + await expectQuery({ + $or: { + conditions: [ + { equal: { age: 7 } }, + { equal: { name: "Jack" } }, + ], + }, + }).toContainExactly([ + { age: 10, name: "Jack" }, + { age: 7, name: "Hanna" }, + ]) + }) + + it("successfully finds a row for one level with multiple conditions", async () => { + await expectQuery({ + $or: { + conditions: [ + { equal: { age: 7 } }, + { equal: { name: "Jack" } }, + ], + }, + }).toContainExactly([ + { age: 10, name: "Jack" }, + { age: 7, name: "Hanna" }, + ]) + }) + + it("successfully finds multiple rows for one level with multiple conditions", async () => { + await expectQuery({ + $or: { + conditions: [ + { range: { age: { low: 1, high: 9 } } }, + { string: { name: "Jan" } }, + ], + }, + }).toContainExactly([ + { age: 1, name: "Jane" }, + { age: 7, name: "Hanna" }, + { age: 8, name: "Jan" }, + ]) + }) + + it("successfully finds rows for nested filters", async () => { + await expectQuery({ $or: { conditions: [ { - range: { age: { low: 1, high: 8 } }, + $or: { + conditions: [ + { + range: { age: { low: 1, high: 7 } }, + }, + { string: { name: "Jan" } }, + ], + }, + equal: { name: "Jane" }, }, - { equal: { name: "Jan" } }, ], }, - equal: { name: "Jane" }, - }, - ], - }, - }).toContainExactly([{ age: 1, name: "Jane" }]) - }) - - // onEmptyFilter cannot be sent to view searches - !isView && - it("returns no rows when onEmptyFilter set to none", async () => { - await expectSearch({ - query: { - onEmptyFilter: EmptyFilterOption.RETURN_NONE, - $or: { - conditions: [{ equal: { name: "" } }], - }, - }, - }).toFindNothing() - }) - - it("returns all rows when onEmptyFilter set to all", async () => { - await expectSearch({ - query: { - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - $or: { - conditions: [{ equal: { name: "" } }], - }, - }, - }).toHaveLength(4) - }) - }) - - isSql && - describe("max related columns", () => { - let relatedRows: Row[] - - beforeAll(async () => { - const relatedSchema: TableSchema = {} - const row: Row = {} - for (let i = 0; i < 100; i++) { - const name = `column${i}` - relatedSchema[name] = { name, type: FieldType.NUMBER } - row[name] = i - } - const relatedTable = await createTable(relatedSchema) - tableOrViewId = await createTableOrView({ - name: { name: "name", type: FieldType.STRING }, - related1: { - type: FieldType.LINK, - name: "related1", - fieldName: "main1", - tableId: relatedTable, - relationshipType: RelationshipType.MANY_TO_MANY, - }, - }) - relatedRows = await Promise.all([ - config.api.row.save(relatedTable, row), - ]) - await config.api.row.save(tableOrViewId, { - name: "foo", - related1: [relatedRows[0]._id], - }) - }) - - it("retrieve the row with relationships", async () => { - await expectQuery({}).toContainExactly([ - { - name: "foo", - related1: [{ _id: relatedRows[0]._id }], - }, - ]) - }) - }) - - isSql && - !isSqs && - describe("SQL injection", () => { - const badStrings = [ - "1; DROP TABLE %table_name%;", - "1; DELETE FROM %table_name%;", - "1; UPDATE %table_name% SET name = 'foo';", - "1; INSERT INTO %table_name% (name) VALUES ('foo');", - "' OR '1'='1' --", - "'; DROP TABLE %table_name%; --", - "' OR 1=1 --", - "' UNION SELECT null, null, null; --", - "' AND (SELECT COUNT(*) FROM %table_name%) > 0 --", - "\"; EXEC xp_cmdshell('dir'); --", - "\"' OR 'a'='a", - "OR 1=1;", - "'; SHUTDOWN --", - ] - - describe.each(badStrings)("bad string: %s", badStringTemplate => { - // The SQL that knex generates when you try to use a double quote in a - // field name is always invalid and never works, so we skip it for these - // tests. - const skipFieldNameCheck = isOracle && badStringTemplate.includes('"') - - !skipFieldNameCheck && - it("should not allow SQL injection as a field name", async () => { - const tableOrViewId = await createTableOrView() - const table = await getTable(tableOrViewId) - const badString = badStringTemplate.replace( - /%table_name%/g, - table.name - ) - - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - [badString]: { name: badString, type: FieldType.STRING }, - }, + }).toContainExactly([ + { age: 1, name: "Jane" }, + { age: 7, name: "Hanna" }, + { age: 8, name: "Jan" }, + ]) }) - if (docIds.isViewId(tableOrViewId)) { - const view = await config.api.viewV2.get(tableOrViewId) - await config.api.viewV2.update({ - ...view, - schema: { - [badString]: { visible: true }, + it("returns nothing when filtering out all data", async () => { + await expectQuery({ + $or: { + conditions: [ + { equal: { age: 6 } }, + { equal: { name: "John" } }, + ], }, + }).toFindNothing() + }) + + it("can nest $and under $or filters", async () => { + await expectQuery({ + $or: { + conditions: [ + { + $and: { + conditions: [ + { + range: { age: { low: 1, high: 8 } }, + }, + { equal: { name: "Jan" } }, + ], + }, + equal: { name: "Jane" }, + }, + ], + }, + }).toContainExactly([ + { age: 1, name: "Jane" }, + { age: 8, name: "Jan" }, + ]) + }) + + it("can nest $or under $and filters", async () => { + await expectQuery({ + $and: { + conditions: [ + { + $or: { + conditions: [ + { + range: { age: { low: 1, high: 8 } }, + }, + { equal: { name: "Jan" } }, + ], + }, + equal: { name: "Jane" }, + }, + ], + }, + }).toContainExactly([{ age: 1, name: "Jane" }]) + }) + + // onEmptyFilter cannot be sent to view searches + !isView && + it("returns no rows when onEmptyFilter set to none", async () => { + await expectSearch({ + query: { + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + $or: { + conditions: [{ equal: { name: "" } }], + }, + }, + }).toFindNothing() }) - } - await config.api.row.save(tableOrViewId, { [badString]: "foo" }) - - await assertTableExists(table) - await assertTableNumRows(table, 1) - - const { rows } = await config.api.row.search( - tableOrViewId, - { query: {} }, - { status: 200 } - ) - - expect(rows).toHaveLength(1) - - await assertTableExists(table) - await assertTableNumRows(table, 1) + it("returns all rows when onEmptyFilter set to all", async () => { + await expectSearch({ + query: { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + $or: { + conditions: [{ equal: { name: "" } }], + }, + }, + }).toHaveLength(4) + }) }) - it("should not allow SQL injection as a field value", async () => { - const tableOrViewId = await createTableOrView({ - foo: { - name: "foo", - type: FieldType.STRING, - }, - }) - const table = await getTable(tableOrViewId) - const badString = badStringTemplate.replace( - /%table_name%/g, - table.name - ) + isSql && + describe("max related columns", () => { + let relatedRows: Row[] - await config.api.row.save(tableOrViewId, { foo: "foo" }) + beforeAll(async () => { + const relatedSchema: TableSchema = {} + const row: Row = {} + for (let i = 0; i < 100; i++) { + const name = `column${i}` + relatedSchema[name] = { name, type: FieldType.NUMBER } + row[name] = i + } + const relatedTable = await createTable(relatedSchema) + tableOrViewId = await createTableOrView({ + name: { name: "name", type: FieldType.STRING }, + related1: { + type: FieldType.LINK, + name: "related1", + fieldName: "main1", + tableId: relatedTable, + relationshipType: RelationshipType.MANY_TO_MANY, + }, + }) + relatedRows = await Promise.all([ + config.api.row.save(relatedTable, row), + ]) + await config.api.row.save(tableOrViewId, { + name: "foo", + related1: [relatedRows[0]._id], + }) + }) - await assertTableExists(table) - await assertTableNumRows(table, 1) + it("retrieve the row with relationships", async () => { + await expectQuery({}).toContainExactly([ + { + name: "foo", + related1: [{ _id: relatedRows[0]._id }], + }, + ]) + }) + }) - const { rows } = await config.api.row.search( - tableOrViewId, - { query: { equal: { foo: badString } } }, - { status: 200 } - ) + !isInternal && + describe("SQL injection", () => { + const badStrings = [ + "1; DROP TABLE %table_name%;", + "1; DELETE FROM %table_name%;", + "1; UPDATE %table_name% SET name = 'foo';", + "1; INSERT INTO %table_name% (name) VALUES ('foo');", + "' OR '1'='1' --", + "'; DROP TABLE %table_name%; --", + "' OR 1=1 --", + "' UNION SELECT null, null, null; --", + "' AND (SELECT COUNT(*) FROM %table_name%) > 0 --", + "\"; EXEC xp_cmdshell('dir'); --", + "\"' OR 'a'='a", + "OR 1=1;", + "'; SHUTDOWN --", + ] - expect(rows).toBeEmpty() - await assertTableExists(table) - await assertTableNumRows(table, 1) - }) - }) + describe.each(badStrings)( + "bad string: %s", + badStringTemplate => { + // The SQL that knex generates when you try to use a double quote in a + // field name is always invalid and never works, so we skip it for these + // tests. + const skipFieldNameCheck = + isOracle && badStringTemplate.includes('"') + + !skipFieldNameCheck && + it("should not allow SQL injection as a field name", async () => { + const tableOrViewId = await createTableOrView() + const table = await getTable(tableOrViewId) + const badString = badStringTemplate.replace( + /%table_name%/g, + table.name + ) + + await config.api.table.save({ + ...table, + schema: { + ...table.schema, + [badString]: { + name: badString, + type: FieldType.STRING, + }, + }, + }) + + if (docIds.isViewId(tableOrViewId)) { + const view = await config.api.viewV2.get( + tableOrViewId + ) + await config.api.viewV2.update({ + ...view, + schema: { + [badString]: { visible: true }, + }, + }) + } + + await config.api.row.save(tableOrViewId, { + [badString]: "foo", + }) + + await assertTableExists(table) + await assertTableNumRows(table, 1) + + const { rows } = await config.api.row.search( + tableOrViewId, + { query: {} }, + { status: 200 } + ) + + expect(rows).toHaveLength(1) + + await assertTableExists(table) + await assertTableNumRows(table, 1) + }) + + it("should not allow SQL injection as a field value", async () => { + const tableOrViewId = await createTableOrView({ + foo: { + name: "foo", + type: FieldType.STRING, + }, + }) + const table = await getTable(tableOrViewId) + const badString = badStringTemplate.replace( + /%table_name%/g, + table.name + ) + + await config.api.row.save(tableOrViewId, { foo: "foo" }) + + await assertTableExists(table) + await assertTableNumRows(table, 1) + + const { rows } = await config.api.row.search( + tableOrViewId, + { query: { equal: { foo: badString } } }, + { status: 200 } + ) + + expect(rows).toBeEmpty() + await assertTableExists(table) + await assertTableNumRows(table, 1) + }) + } + ) + }) + } + ) }) - }) -}) + } + ) +} diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts index f7fe6a66d4..8556a598c6 100644 --- a/packages/server/src/api/routes/tests/table.spec.ts +++ b/packages/server/src/api/routes/tests/table.spec.ts @@ -28,1053 +28,879 @@ import * as setup from "./utilities" import * as uuid from "uuid" import { generator } from "@budibase/backend-core/tests" -import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" +import { + DatabaseName, + datasourceDescribe, +} from "../../../integrations/tests/utils" import { tableForDatasource } from "../../../tests/utilities/structures" import timekeeper from "timekeeper" const { basicTable } = setup.structures const ISO_REGEX_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ -describe.each([ - ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], - [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], -])("/tables (%s)", (name, dsProvider) => { - const isInternal: boolean = !dsProvider - let datasource: Datasource | undefined - let config = setup.getConfig() +const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] }) - afterAll(setup.afterAll) +if (descriptions.length) { + describe.each(descriptions)( + "/tables ($dbName)", + ({ config, dsProvider, isInternal, isOracle }) => { + let datasource: Datasource | undefined - beforeAll(async () => { - await config.init() - if (dsProvider) { - datasource = await config.api.datasource.create(await dsProvider) - } - }) - - describe("create", () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - let names = [ - "alphanum", - "with spaces", - "with-dashes", - "with_underscores", - "with `backticks`", - ] - - if (name !== DatabaseName.ORACLE) { - names.push(`with "double quotes"`) - names.push(`with 'single quotes'`) - } - - it.each(names)("creates a table with name: %s", async name => { - const table = await config.api.table.save( - tableForDatasource(datasource, { name }) - ) - expect(table.name).toEqual(name) - expect(events.table.created).toHaveBeenCalledTimes(1) - expect(events.table.created).toHaveBeenCalledWith(table) - - const res = await config.api.table.get(table._id!) - expect(res.name).toEqual(name) - }) - - it("creates a table via data import", async () => { - const table: SaveTableRequest = basicTable() - table.rows = [{ name: "test-name", description: "test-desc" }] - - const res = await config.api.table.save(table) - - expect(events.table.created).toHaveBeenCalledTimes(1) - expect(events.table.created).toHaveBeenCalledWith(res) - expect(events.table.imported).toHaveBeenCalledTimes(1) - expect(events.table.imported).toHaveBeenCalledWith(res) - expect(events.rows.imported).toHaveBeenCalledTimes(1) - expect(events.rows.imported).toHaveBeenCalledWith(res, 1) - }) - - it("should not allow a column to have a default value and be required", async () => { - await config.api.table.save( - tableForDatasource(datasource, { - schema: { - name: { - name: "name", - type: FieldType.STRING, - default: "default", - constraints: { - presence: true, - }, - }, - }, - }), - { - status: 400, - body: { - message: - 'Cannot make field "name" required, it has a default value.', - }, - } - ) - }) - - it("should apply authorization to endpoint", async () => { - await checkBuilderEndpoint({ - config, - method: "POST", - url: `/api/tables`, - body: basicTable(), + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource }) - }) - it("does not persist the row fields that are not on the table schema", async () => { - const table: SaveTableRequest = basicTable() - table.rows = [ - { - name: "test-name", - description: "test-desc", - nonValid: "test-non-valid", - }, - ] + describe("create", () => { + beforeEach(() => { + jest.clearAllMocks() + }) - const res = await config.api.table.save(table) - - const persistedRows = await config.api.row.search(res._id!) - - expect(persistedRows.rows).toEqual([ - expect.objectContaining({ - name: "test-name", - description: "test-desc", - }), - ]) - expect(persistedRows.rows[0].nonValid).toBeUndefined() - }) - - it.each( - isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS - )( - "cannot use protected column names (%s) while importing a table", - async columnName => { - const table: SaveTableRequest = basicTable() - table.rows = [ - { - name: "test-name", - description: "test-desc", - }, + let names = [ + "alphanum", + "with spaces", + "with-dashes", + "with_underscores", + "with `backticks`", ] - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - [columnName]: { - name: columnName, - type: FieldType.STRING, + if (!isOracle) { + names.push(`with "double quotes"`) + names.push(`with 'single quotes'`) + } + + it.each(names)("creates a table with name: %s", async name => { + const table = await config.api.table.save( + tableForDatasource(datasource, { name }) + ) + expect(table.name).toEqual(name) + expect(events.table.created).toHaveBeenCalledTimes(1) + expect(events.table.created).toHaveBeenCalledWith(table) + + const res = await config.api.table.get(table._id!) + expect(res.name).toEqual(name) + }) + + it("creates a table via data import", async () => { + const table: SaveTableRequest = basicTable() + table.rows = [{ name: "test-name", description: "test-desc" }] + + const res = await config.api.table.save(table) + + expect(events.table.created).toHaveBeenCalledTimes(1) + expect(events.table.created).toHaveBeenCalledWith(res) + expect(events.table.imported).toHaveBeenCalledTimes(1) + expect(events.table.imported).toHaveBeenCalledWith(res) + expect(events.rows.imported).toHaveBeenCalledTimes(1) + expect(events.rows.imported).toHaveBeenCalledWith(res, 1) + }) + + it("should not allow a column to have a default value and be required", async () => { + await config.api.table.save( + tableForDatasource(datasource, { + schema: { + name: { + name: "name", + type: FieldType.STRING, + default: "default", + constraints: { + presence: true, + }, + }, }, - }, - }, - { - status: 400, - body: { - message: `Column(s) "${columnName}" are duplicated - check for other columns with these name (case in-sensitive)`, + }), + { status: 400, + body: { + message: + 'Cannot make field "name" required, it has a default value.', + }, + } + ) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "POST", + url: `/api/tables`, + body: basicTable(), + }) + }) + + it("does not persist the row fields that are not on the table schema", async () => { + const table: SaveTableRequest = basicTable() + table.rows = [ + { + name: "test-name", + description: "test-desc", + nonValid: "test-non-valid", }, + ] + + const res = await config.api.table.save(table) + + const persistedRows = await config.api.row.search(res._id!) + + expect(persistedRows.rows).toEqual([ + expect.objectContaining({ + name: "test-name", + description: "test-desc", + }), + ]) + expect(persistedRows.rows[0].nonValid).toBeUndefined() + }) + + it.each( + isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS + )( + "cannot use protected column names (%s) while importing a table", + async columnName => { + const table: SaveTableRequest = basicTable() + table.rows = [ + { + name: "test-name", + description: "test-desc", + }, + ] + + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + [columnName]: { + name: columnName, + type: FieldType.STRING, + }, + }, + }, + { + status: 400, + body: { + message: `Column(s) "${columnName}" are duplicated - check for other columns with these name (case in-sensitive)`, + status: 400, + }, + } + ) } ) - } - ) - }) - - describe("permissions", () => { - it("get the base permissions for the table", async () => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - name: { - type: FieldType.STRING, - name: "name", - }, - }, - }) - ) - - // get the explicit permissions - const { permissions } = await config.api.permission.get(table._id!, { - status: 200, - }) - const explicitPermissions = { - role: "ADMIN", - permissionType: "EXPLICIT", - } - expect(permissions.write).toEqual(explicitPermissions) - expect(permissions.read).toEqual(explicitPermissions) - - // revoke the explicit permissions - for (let level of [PermissionLevel.WRITE, PermissionLevel.READ]) { - await config.api.permission.revoke( - { - roleId: permissions[level].role, - resourceId: table._id!, - level, - }, - { status: 200 } - ) - } - - // check base permissions - const { permissions: basePermissions } = await config.api.permission.get( - table._id!, - { - status: 200, - } - ) - const basePerms = { role: "BASIC", permissionType: "BASE" } - expect(basePermissions.write).toEqual(basePerms) - expect(basePermissions.read).toEqual(basePerms) - }) - }) - - describe("update", () => { - it("updates a table", async () => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - name: { - type: FieldType.STRING, - name: "name", - constraints: { - type: "string", - }, - }, - }, - }) - ) - - const updatedTable = await config.api.table.save({ - ...table, - name: generator.guid(), }) - expect(events.table.updated).toHaveBeenCalledTimes(1) - expect(events.table.updated).toHaveBeenCalledWith(updatedTable) - }) - - it("updates all the row fields for a table when a schema key is renamed", async () => { - const testTable = await config.api.table.save(basicTable(datasource)) - await config.createLegacyView({ - name: "TestView", - field: "Price", - calculation: ViewCalculation.STATISTICS, - tableId: testTable._id!, - schema: {}, - filters: [], - }) - - const testRow = await config.api.row.save(testTable._id!, { - name: "test", - }) - - const { name, ...otherColumns } = testTable.schema - const updatedTable = await config.api.table.save({ - ...testTable, - _rename: { - old: "name", - updated: "updatedName", - }, - schema: { - ...otherColumns, - updatedName: { - ...name, - name: "updatedName", - }, - }, - }) - - expect(updatedTable.name).toEqual(testTable.name) - - const res = await config.api.row.get(testTable._id!, testRow._id!) - expect(res.updatedName).toEqual("test") - expect(res.name).toBeUndefined() - }) - - isInternal && - it("updates only the passed fields", async () => { - await timekeeper.withFreeze(new Date(2021, 1, 1), async () => { + describe("permissions", () => { + it("get the base permissions for the table", async () => { const table = await config.api.table.save( tableForDatasource(datasource, { schema: { - autoId: { - name: "id", - type: FieldType.NUMBER, - subtype: AutoFieldSubType.AUTO_ID, - autocolumn: true, + name: { + type: FieldType.STRING, + name: "name", + }, + }, + }) + ) + + // get the explicit permissions + const { permissions } = await config.api.permission.get(table._id!, { + status: 200, + }) + const explicitPermissions = { + role: "ADMIN", + permissionType: "EXPLICIT", + } + expect(permissions.write).toEqual(explicitPermissions) + expect(permissions.read).toEqual(explicitPermissions) + + // revoke the explicit permissions + for (let level of [PermissionLevel.WRITE, PermissionLevel.READ]) { + await config.api.permission.revoke( + { + roleId: permissions[level].role, + resourceId: table._id!, + level, + }, + { status: 200 } + ) + } + + // check base permissions + const { permissions: basePermissions } = + await config.api.permission.get(table._id!, { + status: 200, + }) + const basePerms = { role: "BASIC", permissionType: "BASE" } + expect(basePermissions.write).toEqual(basePerms) + expect(basePermissions.read).toEqual(basePerms) + }) + }) + + describe("update", () => { + it("updates a table", async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + name: { + type: FieldType.STRING, + name: "name", constraints: { - type: "number", - presence: false, + type: "string", }, }, }, }) ) - const newName = generator.guid() - const updatedTable = await config.api.table.save({ ...table, - name: newName, + name: generator.guid(), }) - let expected: Table = { - ...table, - name: newName, - _id: expect.any(String), - } - if (isInternal) { - expected._rev = expect.stringMatching(/^2-.+/) - } - - expect(updatedTable).toEqual(expect.objectContaining(expected)) - - const persistedTable = await config.api.table.get(updatedTable._id!) - expected = { - ...table, - name: newName, - _id: updatedTable._id, - } - if (datasource?.isSQL) { - expected.sql = true - } - if (isInternal) { - expected._rev = expect.stringMatching(/^2-.+/) - } - expect(persistedTable).toEqual(expect.objectContaining(expected)) - }) - }) - - describe("user table", () => { - isInternal && - it("should add roleId and email field when adjusting user table schema", async () => { - const table = await config.api.table.save({ - ...basicTable(datasource), - _id: "ta_users", - }) - expect(table.schema.email).toBeDefined() - expect(table.schema.roleId).toBeDefined() - }) - }) - - describe("default field validation", () => { - it("should error if an existing column is set to required and has a default value", async () => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - name: { - name: "name", - type: FieldType.STRING, - default: "default", - }, - }, - }) - ) - - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - name: { - name: "name", - type: FieldType.STRING, - default: "default", - constraints: { - presence: true, - }, - }, - }, - }, - { - status: 400, - body: { - message: - 'Cannot make field "name" required, it has a default value.', - }, - } - ) - }) - - it("should error if an existing column is given a default value and is required", async () => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - name: { - name: "name", - type: FieldType.STRING, - constraints: { - presence: true, - }, - }, - }, - }) - ) - - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - name: { - name: "name", - type: FieldType.STRING, - default: "default", - constraints: { - presence: true, - }, - }, - }, - }, - { - status: 400, - body: { - message: - 'Cannot make field "name" required, it has a default value.', - }, - } - ) - }) - - it("should be able to set an existing column to have a default value if it's not required", async () => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - }, - }) - ) - - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - name: { - name: "name", - type: FieldType.STRING, - default: "default", - }, - }, - }, - { status: 200 } - ) - }) - - it("should be able to remove a default value if the column is not required", async () => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - schema: { - name: { - name: "name", - type: FieldType.STRING, - default: "default", - }, - }, - }) - ) - - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - name: { - name: "name", - type: FieldType.STRING, - }, - }, - }, - { status: 200 } - ) - }) - }) - - describe("external table validation", () => { - !isInternal && - it("should error if column is of type auto", async () => { - const table = basicTable(datasource) - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - auto: { - name: "auto", - autocolumn: true, - type: FieldType.AUTO, - subtype: AutoFieldSubType.AUTO_ID, - }, - }, - }, - { - status: 400, - body: { - message: `Column "auto" has type "${FieldType.AUTO}" - this is not supported.`, - }, - } - ) + expect(events.table.updated).toHaveBeenCalledTimes(1) + expect(events.table.updated).toHaveBeenCalledWith(updatedTable) }) - !isInternal && - it("should error if column has auto subtype", async () => { - const table = basicTable(datasource) - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - auto: { - name: "auto", - autocolumn: true, - type: FieldType.NUMBER, - subtype: AutoFieldSubType.AUTO_ID, - }, - }, - }, - { - status: 400, - body: { - message: `Column "auto" has subtype "${AutoFieldSubType.AUTO_ID}" - this is not supported.`, - }, - } - ) - }) - }) - - isInternal && - it("shouldn't allow duplicate column names", async () => { - const saveTableRequest: SaveTableRequest = { - ...basicTable(), - } - saveTableRequest.schema["Type"] = { - type: FieldType.STRING, - name: "Type", - } - // allow the "Type" column - internal columns aren't case sensitive - await config.api.table.save(saveTableRequest, { - status: 200, - }) - saveTableRequest.schema.foo = { type: FieldType.STRING, name: "foo" } - saveTableRequest.schema.FOO = { type: FieldType.STRING, name: "FOO" } - - await config.api.table.save(saveTableRequest, { - status: 400, - body: { - message: - 'Column(s) "foo" are duplicated - check for other columns with these name (case in-sensitive)', - }, - }) - }) - - it("should add a new column for an internal DB table", async () => { - const saveTableRequest: SaveTableRequest = { - ...basicTable(), - } - - const response = await config.api.table.save(saveTableRequest) - - const expectedResponse = { - ...saveTableRequest, - _rev: expect.stringMatching(/^\d-.+/), - _id: expect.stringMatching(/^ta_.+/), - createdAt: expect.stringMatching(ISO_REGEX_PATTERN), - updatedAt: expect.stringMatching(ISO_REGEX_PATTERN), - views: {}, - } - expect(response).toEqual(expectedResponse) - }) - }) - - describe("import", () => { - it("imports rows successfully", async () => { - const name = generator.guid() - const table = await config.api.table.save( - basicTable(datasource, { name }) - ) - const importRequest = { - schema: table.schema, - rows: [{ name: "test-name", description: "test-desc" }], - } - - jest.clearAllMocks() - - await config.api.table.import(table._id!, importRequest) - - expect(events.table.created).not.toHaveBeenCalled() - expect(events.rows.imported).toHaveBeenCalledTimes(1) - expect(events.rows.imported).toHaveBeenCalledWith( - expect.objectContaining({ - name, - _id: table._id, - }), - 1 - ) - }) - }) - - describe("fetch", () => { - let testTable: Table - - beforeEach(async () => { - testTable = await config.api.table.save( - basicTable(datasource, { name: generator.guid() }) - ) - }) - - it("returns all tables", async () => { - const res = await config.api.table.fetch() - const table = res.find(t => t._id === testTable._id) - expect(table).toBeDefined() - expect(table!.name).toEqual(testTable.name) - expect(table!.type).toEqual("table") - expect(table!.sourceType).toEqual(testTable.sourceType) - }) - - it("should apply authorization to endpoint", async () => { - await checkBuilderEndpoint({ - config, - method: "GET", - url: `/api/tables`, - }) - }) - - it("should enrich the view schemas", async () => { - const viewV2 = await config.api.viewV2.create({ - tableId: testTable._id!, - name: generator.guid(), - }) - const legacyView = await config.api.legacyView.save({ - tableId: testTable._id!, - name: generator.guid(), - filters: [], - schema: {}, - }) - - const res = await config.api.table.fetch() - - const table = res.find(t => t._id === testTable._id) - expect(table).toBeDefined() - expect(table!.views![viewV2.name]).toBeDefined() - - const expectedViewV2: ViewV2Enriched = { - ...viewV2, - schema: { - description: { - constraints: { - type: "string", - }, - name: "description", - type: FieldType.STRING, - visible: false, - }, - name: { - constraints: { - type: "string", - }, - name: "name", - type: FieldType.STRING, - visible: false, - }, - }, - } - - if (!isInternal) { - expectedViewV2.schema!.id = { - name: "id", - type: FieldType.NUMBER, - visible: false, - autocolumn: true, - } - } - - expect(table!.views![viewV2.name!]).toEqual(expectedViewV2) - - if (isInternal) { - expect(table!.views![legacyView.name!]).toBeDefined() - expect(table!.views![legacyView.name!]).toEqual({ - ...legacyView, - schema: { - description: { - constraints: { - type: "string", - }, - name: "description", - type: "string", - }, - name: { - constraints: { - type: "string", - }, - name: "name", - type: "string", - }, - }, - }) - } - }) - }) - - describe("get", () => { - it("returns a table", async () => { - const table = await config.api.table.save( - basicTable(datasource, { name: generator.guid() }) - ) - const res = await config.api.table.get(table._id!) - expect(res).toEqual(expect.objectContaining(table)) - }) - }) - - describe("indexing", () => { - it("should be able to create a table with indexes", async () => { - await context.doInAppContext(config.getAppId(), async () => { - const db = context.getAppDB() - const indexCount = (await db.getIndexes()).total_rows - const table = basicTable() - table.indexes = ["name"] - const res = await config.api.table.save(table) - expect(res._id).toBeDefined() - expect(res._rev).toBeDefined() - expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1) - // update index to see what happens - table.indexes = ["name", "description"] - await config.api.table.save({ - ...table, - _id: res._id, - _rev: res._rev, - }) - // shouldn't have created a new index - expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1) - }) - }) - }) - - describe("destroy", () => { - let testTable: Table - - beforeEach(async () => { - testTable = await config.createTable() - }) - - it("returns a success response when a table is deleted.", async () => { - await config.api.table.destroy(testTable._id!, testTable._rev!, { - body: { message: `Table ${testTable._id} deleted.` }, - }) - expect(events.table.deleted).toHaveBeenCalledTimes(1) - expect(events.table.deleted).toHaveBeenCalledWith( - expect.objectContaining({ - ...testTable, - tableId: testTable._id, - }) - ) - }) - - it("deletes linked references to the table after deletion", async () => { - const linkedTable = await config.createTable({ - name: "LinkedTable", - type: "table", - schema: { - name: { - type: FieldType.STRING, - name: "name", - constraints: { - type: "string", - }, - }, - TestTable: { - type: FieldType.LINK, - relationshipType: RelationshipType.ONE_TO_MANY, - name: "TestTable", - fieldName: "TestTable", + it("updates all the row fields for a table when a schema key is renamed", async () => { + const testTable = await config.api.table.save(basicTable(datasource)) + await config.createLegacyView({ + name: "TestView", + field: "Price", + calculation: ViewCalculation.STATISTICS, tableId: testTable._id!, - constraints: { - type: "array", - }, - }, - }, - }) - - await config.api.table.destroy(testTable._id!, testTable._rev!, { - body: { message: `Table ${testTable._id} deleted.` }, - }) - const dependentTable = await config.api.table.get(linkedTable._id!) - expect(dependentTable.schema.TestTable).not.toBeDefined() - }) - - it("should apply authorization to endpoint", async () => { - await checkBuilderEndpoint({ - config, - method: "DELETE", - url: `/api/tables/${testTable._id}/${testTable._rev}`, - }) - }) - }) - - describe("migrate", () => { - let users: User[] - beforeAll(async () => { - users = await Promise.all([ - config.createUser({ email: `${uuid.v4()}@example.com` }), - config.createUser({ email: `${uuid.v4()}@example.com` }), - config.createUser({ email: `${uuid.v4()}@example.com` }), - ]) - }) - - it("should successfully migrate a one-to-many user relationship to a user column", async () => { - const table = await config.api.table.save({ - name: "table", - type: "table", - sourceId: INTERNAL_TABLE_SOURCE_ID, - sourceType: TableSourceType.INTERNAL, - schema: { - "user relationship": { - type: FieldType.LINK, - fieldName: "test", - name: "user relationship", - constraints: { - type: "array", - presence: false, - }, - relationshipType: RelationshipType.ONE_TO_MANY, - tableId: InternalTable.USER_METADATA, - }, - }, - }) - - const rows = await Promise.all( - users.map(u => - config.api.row.save(table._id!, { "user relationship": [u] }) - ) - ) - - await config.api.table.migrate(table._id!, { - oldColumn: "user relationship", - newColumn: "user column", - }) - - const migratedTable = await config.api.table.get(table._id!) - expect(migratedTable.schema["user column"]).toEqual({ - name: "user column", - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - }) - expect(migratedTable.schema["user relationship"]).not.toBeDefined() - - const migratedRows = await config.api.row.fetch(table._id!) - - rows.sort((a, b) => a._id!.localeCompare(b._id!)) - migratedRows.sort((a, b) => a._id!.localeCompare(b._id!)) - - for (const [i, row] of rows.entries()) { - const migratedRow = migratedRows[i] - expect(migratedRow["user column"]).toBeDefined() - expect(migratedRow["user relationship"]).not.toBeDefined() - expect(row["user relationship"][0]._id).toEqual( - migratedRow["user column"]._id - ) - } - }) - - it("should succeed when the row is created from the other side of the relationship", async () => { - // We found a bug just after releasing this feature where if the row was created from the - // users table, not the table linking to it, the migration would succeed but lose the data. - // This happened because the order of the documents in the link was reversed. - const table = await config.api.table.save({ - name: "table", - type: "table", - sourceId: INTERNAL_TABLE_SOURCE_ID, - sourceType: TableSourceType.INTERNAL, - schema: { - "user relationship": { - type: FieldType.LINK, - fieldName: "test", - name: "user relationship", - constraints: { - type: "array", - presence: false, - }, - relationshipType: RelationshipType.MANY_TO_ONE, - tableId: InternalTable.USER_METADATA, - }, - }, - }) - - let testRow = await config.api.row.save(table._id!, {}) - - await Promise.all( - users.map(u => - config.api.row.patch(InternalTable.USER_METADATA, { - tableId: InternalTable.USER_METADATA, - _rev: u._rev!, - _id: u._id!, - test: [testRow], + schema: {}, + filters: [], }) - ) - ) - await config.api.table.migrate(table._id!, { - oldColumn: "user relationship", - newColumn: "user column", - }) + const testRow = await config.api.row.save(testTable._id!, { + name: "test", + }) - const migratedTable = await config.api.table.get(table._id!) - expect(migratedTable.schema["user column"]).toEqual({ - name: "user column", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - constraints: { - type: "array", - }, - }) - expect(migratedTable.schema["user relationship"]).not.toBeDefined() - - const migratedRow = await config.api.row.get(table._id!, testRow._id!) - - expect(migratedRow["user column"]).toBeDefined() - expect(migratedRow["user relationship"]).not.toBeDefined() - expect(migratedRow["user column"]).toHaveLength(3) - expect(migratedRow["user column"].map((u: Row) => u._id)).toEqual( - expect.arrayContaining(users.map(u => u._id)) - ) - }) - - it("should successfully migrate a many-to-many user relationship to a users column", async () => { - const table = await config.api.table.save({ - name: "table", - type: "table", - sourceId: INTERNAL_TABLE_SOURCE_ID, - sourceType: TableSourceType.INTERNAL, - schema: { - "user relationship": { - type: FieldType.LINK, - fieldName: "test", - name: "user relationship", - constraints: { - type: "array", - presence: false, + const { name, ...otherColumns } = testTable.schema + const updatedTable = await config.api.table.save({ + ...testTable, + _rename: { + old: "name", + updated: "updatedName", }, - relationshipType: RelationshipType.MANY_TO_MANY, - tableId: InternalTable.USER_METADATA, - }, - }, - }) - - const row1 = await config.api.row.save(table._id!, { - "user relationship": [users[0], users[1]], - }) - - const row2 = await config.api.row.save(table._id!, { - "user relationship": [users[1], users[2]], - }) - - await config.api.table.migrate(table._id!, { - oldColumn: "user relationship", - newColumn: "user column", - }) - - const migratedTable = await config.api.table.get(table._id!) - expect(migratedTable.schema["user column"]).toEqual({ - name: "user column", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - constraints: { - type: "array", - }, - }) - expect(migratedTable.schema["user relationship"]).not.toBeDefined() - - const row1Migrated = await config.api.row.get(table._id!, row1._id!) - expect(row1Migrated["user relationship"]).not.toBeDefined() - expect(row1Migrated["user column"].map((r: Row) => r._id)).toEqual( - expect.arrayContaining([users[0]._id, users[1]._id]) - ) - - const row2Migrated = await config.api.row.get(table._id!, row2._id!) - expect(row2Migrated["user relationship"]).not.toBeDefined() - expect(row2Migrated["user column"].map((r: Row) => r._id)).toEqual( - expect.arrayContaining([users[1]._id, users[2]._id]) - ) - }) - - it("should successfully migrate a many-to-one user relationship to a users column", async () => { - const table = await config.api.table.save({ - name: "table", - type: "table", - sourceId: INTERNAL_TABLE_SOURCE_ID, - sourceType: TableSourceType.INTERNAL, - schema: { - "user relationship": { - type: FieldType.LINK, - fieldName: "test", - name: "user relationship", - constraints: { - type: "array", - presence: false, + schema: { + ...otherColumns, + updatedName: { + ...name, + name: "updatedName", + }, }, - relationshipType: RelationshipType.MANY_TO_ONE, - tableId: InternalTable.USER_METADATA, - }, - }, + }) + + expect(updatedTable.name).toEqual(testTable.name) + + const res = await config.api.row.get(testTable._id!, testRow._id!) + expect(res.updatedName).toEqual("test") + expect(res.name).toBeUndefined() + }) + + isInternal && + it("updates only the passed fields", async () => { + await timekeeper.withFreeze(new Date(2021, 1, 1), async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + autoId: { + name: "id", + type: FieldType.NUMBER, + subtype: AutoFieldSubType.AUTO_ID, + autocolumn: true, + constraints: { + type: "number", + presence: false, + }, + }, + }, + }) + ) + + const newName = generator.guid() + + const updatedTable = await config.api.table.save({ + ...table, + name: newName, + }) + + let expected: Table = { + ...table, + name: newName, + _id: expect.any(String), + } + if (isInternal) { + expected._rev = expect.stringMatching(/^2-.+/) + } + + expect(updatedTable).toEqual(expect.objectContaining(expected)) + + const persistedTable = await config.api.table.get( + updatedTable._id! + ) + expected = { + ...table, + name: newName, + _id: updatedTable._id, + } + if (datasource?.isSQL) { + expected.sql = true + } + if (isInternal) { + expected._rev = expect.stringMatching(/^2-.+/) + } + expect(persistedTable).toEqual(expect.objectContaining(expected)) + }) + }) + + describe("user table", () => { + isInternal && + it("should add roleId and email field when adjusting user table schema", async () => { + const table = await config.api.table.save({ + ...basicTable(datasource), + _id: "ta_users", + }) + expect(table.schema.email).toBeDefined() + expect(table.schema.roleId).toBeDefined() + }) + }) + + describe("default field validation", () => { + it("should error if an existing column is set to required and has a default value", async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + name: { + name: "name", + type: FieldType.STRING, + default: "default", + }, + }, + }) + ) + + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + default: "default", + constraints: { + presence: true, + }, + }, + }, + }, + { + status: 400, + body: { + message: + 'Cannot make field "name" required, it has a default value.', + }, + } + ) + }) + + it("should error if an existing column is given a default value and is required", async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + name: { + name: "name", + type: FieldType.STRING, + constraints: { + presence: true, + }, + }, + }, + }) + ) + + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + default: "default", + constraints: { + presence: true, + }, + }, + }, + }, + { + status: 400, + body: { + message: + 'Cannot make field "name" required, it has a default value.', + }, + } + ) + }) + + it("should be able to set an existing column to have a default value if it's not required", async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + }, + }) + ) + + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + default: "default", + }, + }, + }, + { status: 200 } + ) + }) + + it("should be able to remove a default value if the column is not required", async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + name: { + name: "name", + type: FieldType.STRING, + default: "default", + }, + }, + }) + ) + + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + }, + }, + }, + { status: 200 } + ) + }) + }) + + describe("external table validation", () => { + !isInternal && + it("should error if column is of type auto", async () => { + const table = basicTable(datasource) + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + auto: { + name: "auto", + autocolumn: true, + type: FieldType.AUTO, + subtype: AutoFieldSubType.AUTO_ID, + }, + }, + }, + { + status: 400, + body: { + message: `Column "auto" has type "${FieldType.AUTO}" - this is not supported.`, + }, + } + ) + }) + + !isInternal && + it("should error if column has auto subtype", async () => { + const table = basicTable(datasource) + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + auto: { + name: "auto", + autocolumn: true, + type: FieldType.NUMBER, + subtype: AutoFieldSubType.AUTO_ID, + }, + }, + }, + { + status: 400, + body: { + message: `Column "auto" has subtype "${AutoFieldSubType.AUTO_ID}" - this is not supported.`, + }, + } + ) + }) + }) + + isInternal && + it("shouldn't allow duplicate column names", async () => { + const saveTableRequest: SaveTableRequest = { + ...basicTable(), + } + saveTableRequest.schema["Type"] = { + type: FieldType.STRING, + name: "Type", + } + // allow the "Type" column - internal columns aren't case sensitive + await config.api.table.save(saveTableRequest, { + status: 200, + }) + saveTableRequest.schema.foo = { + type: FieldType.STRING, + name: "foo", + } + saveTableRequest.schema.FOO = { + type: FieldType.STRING, + name: "FOO", + } + + await config.api.table.save(saveTableRequest, { + status: 400, + body: { + message: + 'Column(s) "foo" are duplicated - check for other columns with these name (case in-sensitive)', + }, + }) + }) + + it("should add a new column for an internal DB table", async () => { + const saveTableRequest: SaveTableRequest = { + ...basicTable(), + } + + const response = await config.api.table.save(saveTableRequest) + + const expectedResponse = { + ...saveTableRequest, + _rev: expect.stringMatching(/^\d-.+/), + _id: expect.stringMatching(/^ta_.+/), + createdAt: expect.stringMatching(ISO_REGEX_PATTERN), + updatedAt: expect.stringMatching(ISO_REGEX_PATTERN), + views: {}, + } + expect(response).toEqual(expectedResponse) + }) }) - const row1 = await config.api.row.save(table._id!, { - "user relationship": [users[0], users[1]], + describe("import", () => { + it("imports rows successfully", async () => { + const name = generator.guid() + const table = await config.api.table.save( + basicTable(datasource, { name }) + ) + const importRequest = { + schema: table.schema, + rows: [{ name: "test-name", description: "test-desc" }], + } + + jest.clearAllMocks() + + await config.api.table.import(table._id!, importRequest) + + expect(events.table.created).not.toHaveBeenCalled() + expect(events.rows.imported).toHaveBeenCalledTimes(1) + expect(events.rows.imported).toHaveBeenCalledWith( + expect.objectContaining({ + name, + _id: table._id, + }), + 1 + ) + }) }) - const row2 = await config.api.row.save(table._id!, { - "user relationship": [users[2]], + describe("fetch", () => { + let testTable: Table + + beforeEach(async () => { + testTable = await config.api.table.save( + basicTable(datasource, { name: generator.guid() }) + ) + }) + + it("returns all tables", async () => { + const res = await config.api.table.fetch() + const table = res.find(t => t._id === testTable._id) + expect(table).toBeDefined() + expect(table!.name).toEqual(testTable.name) + expect(table!.type).toEqual("table") + expect(table!.sourceType).toEqual(testTable.sourceType) + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "GET", + url: `/api/tables`, + }) + }) + + it("should enrich the view schemas", async () => { + const viewV2 = await config.api.viewV2.create({ + tableId: testTable._id!, + name: generator.guid(), + }) + const legacyView = await config.api.legacyView.save({ + tableId: testTable._id!, + name: generator.guid(), + filters: [], + schema: {}, + }) + + const res = await config.api.table.fetch() + + const table = res.find(t => t._id === testTable._id) + expect(table).toBeDefined() + expect(table!.views![viewV2.name]).toBeDefined() + + const expectedViewV2: ViewV2Enriched = { + ...viewV2, + schema: { + description: { + constraints: { + type: "string", + }, + name: "description", + type: FieldType.STRING, + visible: false, + }, + name: { + constraints: { + type: "string", + }, + name: "name", + type: FieldType.STRING, + visible: false, + }, + }, + } + + if (!isInternal) { + expectedViewV2.schema!.id = { + name: "id", + type: FieldType.NUMBER, + visible: false, + autocolumn: true, + } + } + + expect(table!.views![viewV2.name!]).toEqual(expectedViewV2) + + if (isInternal) { + expect(table!.views![legacyView.name!]).toBeDefined() + expect(table!.views![legacyView.name!]).toEqual({ + ...legacyView, + schema: { + description: { + constraints: { + type: "string", + }, + name: "description", + type: "string", + }, + name: { + constraints: { + type: "string", + }, + name: "name", + type: "string", + }, + }, + }) + } + }) }) - await config.api.table.migrate(table._id!, { - oldColumn: "user relationship", - newColumn: "user column", + describe("get", () => { + it("returns a table", async () => { + const table = await config.api.table.save( + basicTable(datasource, { name: generator.guid() }) + ) + const res = await config.api.table.get(table._id!) + expect(res).toEqual(expect.objectContaining(table)) + }) }) - const migratedTable = await config.api.table.get(table._id!) - expect(migratedTable.schema["user column"]).toEqual({ - name: "user column", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - constraints: { - type: "array", - }, + describe("indexing", () => { + it("should be able to create a table with indexes", async () => { + await context.doInAppContext(config.getAppId(), async () => { + const db = context.getAppDB() + const indexCount = (await db.getIndexes()).total_rows + const table = basicTable() + table.indexes = ["name"] + const res = await config.api.table.save(table) + expect(res._id).toBeDefined() + expect(res._rev).toBeDefined() + expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1) + // update index to see what happens + table.indexes = ["name", "description"] + await config.api.table.save({ + ...table, + _id: res._id, + _rev: res._rev, + }) + // shouldn't have created a new index + expect((await db.getIndexes()).total_rows).toEqual(indexCount + 1) + }) + }) }) - expect(migratedTable.schema["user relationship"]).not.toBeDefined() - const row1Migrated = await config.api.row.get(table._id!, row1._id!) - expect(row1Migrated["user relationship"]).not.toBeDefined() - expect(row1Migrated["user column"].map((r: Row) => r._id)).toEqual( - expect.arrayContaining([users[0]._id, users[1]._id]) - ) + describe("destroy", () => { + let testTable: Table - const row2Migrated = await config.api.row.get(table._id!, row2._id!) - expect(row2Migrated["user relationship"]).not.toBeDefined() - expect(row2Migrated["user column"].map((r: Row) => r._id)).toEqual([ - users[2]._id, - ]) - }) + beforeEach(async () => { + testTable = await config.createTable() + }) - describe("unhappy paths", () => { - let table: Table - beforeAll(async () => { - table = await config.api.table.save( - tableForDatasource(datasource, { + it("returns a success response when a table is deleted.", async () => { + await config.api.table.destroy(testTable._id!, testTable._rev!, { + body: { message: `Table ${testTable._id} deleted.` }, + }) + expect(events.table.deleted).toHaveBeenCalledTimes(1) + expect(events.table.deleted).toHaveBeenCalledWith( + expect.objectContaining({ + ...testTable, + tableId: testTable._id, + }) + ) + }) + + it("deletes linked references to the table after deletion", async () => { + const linkedTable = await config.createTable({ + name: "LinkedTable", + type: "table", + schema: { + name: { + type: FieldType.STRING, + name: "name", + constraints: { + type: "string", + }, + }, + TestTable: { + type: FieldType.LINK, + relationshipType: RelationshipType.ONE_TO_MANY, + name: "TestTable", + fieldName: "TestTable", + tableId: testTable._id!, + constraints: { + type: "array", + }, + }, + }, + }) + + await config.api.table.destroy(testTable._id!, testTable._rev!, { + body: { message: `Table ${testTable._id} deleted.` }, + }) + const dependentTable = await config.api.table.get(linkedTable._id!) + expect(dependentTable.schema.TestTable).not.toBeDefined() + }) + + it("should apply authorization to endpoint", async () => { + await checkBuilderEndpoint({ + config, + method: "DELETE", + url: `/api/tables/${testTable._id}/${testTable._rev}`, + }) + }) + }) + + describe("migrate", () => { + let users: User[] + beforeAll(async () => { + users = await Promise.all([ + config.createUser({ email: `${uuid.v4()}@example.com` }), + config.createUser({ email: `${uuid.v4()}@example.com` }), + config.createUser({ email: `${uuid.v4()}@example.com` }), + ]) + }) + + it("should successfully migrate a one-to-many user relationship to a user column", async () => { + const table = await config.api.table.save({ + name: "table", + type: "table", + sourceId: INTERNAL_TABLE_SOURCE_ID, + sourceType: TableSourceType.INTERNAL, + schema: { + "user relationship": { + type: FieldType.LINK, + fieldName: "test", + name: "user relationship", + constraints: { + type: "array", + presence: false, + }, + relationshipType: RelationshipType.ONE_TO_MANY, + tableId: InternalTable.USER_METADATA, + }, + }, + }) + + const rows = await Promise.all( + users.map(u => + config.api.row.save(table._id!, { "user relationship": [u] }) + ) + ) + + await config.api.table.migrate(table._id!, { + oldColumn: "user relationship", + newColumn: "user column", + }) + + const migratedTable = await config.api.table.get(table._id!) + expect(migratedTable.schema["user column"]).toEqual({ + name: "user column", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + }) + expect(migratedTable.schema["user relationship"]).not.toBeDefined() + + const migratedRows = await config.api.row.fetch(table._id!) + + rows.sort((a, b) => a._id!.localeCompare(b._id!)) + migratedRows.sort((a, b) => a._id!.localeCompare(b._id!)) + + for (const [i, row] of rows.entries()) { + const migratedRow = migratedRows[i] + expect(migratedRow["user column"]).toBeDefined() + expect(migratedRow["user relationship"]).not.toBeDefined() + expect(row["user relationship"][0]._id).toEqual( + migratedRow["user column"]._id + ) + } + }) + + it("should succeed when the row is created from the other side of the relationship", async () => { + // We found a bug just after releasing this feature where if the row was created from the + // users table, not the table linking to it, the migration would succeed but lose the data. + // This happened because the order of the documents in the link was reversed. + const table = await config.api.table.save({ + name: "table", + type: "table", + sourceId: INTERNAL_TABLE_SOURCE_ID, + sourceType: TableSourceType.INTERNAL, schema: { "user relationship": { type: FieldType.LINK, @@ -1087,429 +913,621 @@ describe.each([ relationshipType: RelationshipType.MANY_TO_ONE, tableId: InternalTable.USER_METADATA, }, - num: { - type: FieldType.NUMBER, - name: "num", + }, + }) + + let testRow = await config.api.row.save(table._id!, {}) + + await Promise.all( + users.map(u => + config.api.row.patch(InternalTable.USER_METADATA, { + tableId: InternalTable.USER_METADATA, + _rev: u._rev!, + _id: u._id!, + test: [testRow], + }) + ) + ) + + await config.api.table.migrate(table._id!, { + oldColumn: "user relationship", + newColumn: "user column", + }) + + const migratedTable = await config.api.table.get(table._id!) + expect(migratedTable.schema["user column"]).toEqual({ + name: "user column", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + constraints: { + type: "array", + }, + }) + expect(migratedTable.schema["user relationship"]).not.toBeDefined() + + const migratedRow = await config.api.row.get(table._id!, testRow._id!) + + expect(migratedRow["user column"]).toBeDefined() + expect(migratedRow["user relationship"]).not.toBeDefined() + expect(migratedRow["user column"]).toHaveLength(3) + expect(migratedRow["user column"].map((u: Row) => u._id)).toEqual( + expect.arrayContaining(users.map(u => u._id)) + ) + }) + + it("should successfully migrate a many-to-many user relationship to a users column", async () => { + const table = await config.api.table.save({ + name: "table", + type: "table", + sourceId: INTERNAL_TABLE_SOURCE_ID, + sourceType: TableSourceType.INTERNAL, + schema: { + "user relationship": { + type: FieldType.LINK, + fieldName: "test", + name: "user relationship", constraints: { - type: "number", + type: "array", presence: false, }, + relationshipType: RelationshipType.MANY_TO_MANY, + tableId: InternalTable.USER_METADATA, }, }, }) - ) - }) - it("should fail if the new column name is blank", async () => { - await config.api.table.migrate( - table._id!, - { - oldColumn: "user relationship", - newColumn: "", - }, - { status: 400 } - ) - }) - - it("should fail if the new column name is a reserved name", async () => { - await config.api.table.migrate( - table._id!, - { - oldColumn: "user relationship", - newColumn: "_id", - }, - { status: 400 } - ) - }) - - it("should fail if the new column name is the same as an existing column", async () => { - await config.api.table.migrate( - table._id!, - { - oldColumn: "user relationship", - newColumn: "num", - }, - { status: 400 } - ) - }) - - it("should fail if the old column name isn't a column in the table", async () => { - await config.api.table.migrate( - table._id!, - { - oldColumn: "not a column", - newColumn: "new column", - }, - { status: 400 } - ) - }) - }) - }) - - describe.each([ - [RowExportFormat.CSV, (val: any) => JSON.stringify(val).replace(/"/g, "'")], - [RowExportFormat.JSON, (val: any) => val], - ])("import validation (%s)", (_, userParser) => { - const basicSchema: TableSchema = { - id: { - type: FieldType.NUMBER, - name: "id", - }, - name: { - type: FieldType.STRING, - name: "name", - }, - } - - const importCases: [ - string, - (rows: Row[], schema: TableSchema) => Promise - ][] = [ - [ - "validateNewTableImport", - async (rows: Row[], schema: TableSchema) => { - const result = await config.api.table.validateNewTableImport({ - rows, - schema, + const row1 = await config.api.row.save(table._id!, { + "user relationship": [users[0], users[1]], }) - return result - }, - ], - [ - "validateExistingTableImport", - async (rows: Row[], schema: TableSchema) => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - primary: ["id"], - schema, - }) + + const row2 = await config.api.row.save(table._id!, { + "user relationship": [users[1], users[2]], + }) + + await config.api.table.migrate(table._id!, { + oldColumn: "user relationship", + newColumn: "user column", + }) + + const migratedTable = await config.api.table.get(table._id!) + expect(migratedTable.schema["user column"]).toEqual({ + name: "user column", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + constraints: { + type: "array", + }, + }) + expect(migratedTable.schema["user relationship"]).not.toBeDefined() + + const row1Migrated = await config.api.row.get(table._id!, row1._id!) + expect(row1Migrated["user relationship"]).not.toBeDefined() + expect(row1Migrated["user column"].map((r: Row) => r._id)).toEqual( + expect.arrayContaining([users[0]._id, users[1]._id]) ) - const result = await config.api.table.validateExistingTableImport({ - tableId: table._id, - rows, - }) - return result - }, - ], - ] - describe.each(importCases)("%s", (_, testDelegate) => { - it("validates basic imports", async () => { - const result = await testDelegate( - [{ id: generator.natural(), name: generator.first() }], - basicSchema - ) - - expect(result).toEqual({ - allValid: true, - errors: {}, - invalidColumns: [], - schemaValidation: { - id: true, - name: true, - }, - }) - }) - - it.each( - isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS - )("don't allow protected names in schema (%s)", async columnName => { - const result = await config.api.table.validateNewTableImport({ - rows: [ - { - id: generator.natural(), - name: generator.first(), - [columnName]: generator.word(), - }, - ], - schema: { - ...basicSchema, - }, + const row2Migrated = await config.api.row.get(table._id!, row2._id!) + expect(row2Migrated["user relationship"]).not.toBeDefined() + expect(row2Migrated["user column"].map((r: Row) => r._id)).toEqual( + expect.arrayContaining([users[1]._id, users[2]._id]) + ) }) - expect(result).toEqual({ - allValid: false, - errors: { - [columnName]: `${columnName} is a protected column name`, - }, - invalidColumns: [], - schemaValidation: { - id: true, - name: true, - [columnName]: false, - }, - }) - }) - - it("does not allow imports without rows", async () => { - const result = await testDelegate([], basicSchema) - - expect(result).toEqual({ - allValid: false, - errors: {}, - invalidColumns: [], - schemaValidation: {}, - }) - }) - - it("validates imports with some empty rows", async () => { - const result = await testDelegate( - [{}, { id: generator.natural(), name: generator.first() }, {}], - basicSchema - ) - - expect(result).toEqual({ - allValid: true, - errors: {}, - invalidColumns: [], - schemaValidation: { - id: true, - name: true, - }, - }) - }) - - isInternal && - it.each( - isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS - )("don't allow protected names in the rows (%s)", async columnName => { - const result = await config.api.table.validateNewTableImport({ - rows: [ - { - id: generator.natural(), - name: generator.first(), - }, - ], + it("should successfully migrate a many-to-one user relationship to a users column", async () => { + const table = await config.api.table.save({ + name: "table", + type: "table", + sourceId: INTERNAL_TABLE_SOURCE_ID, + sourceType: TableSourceType.INTERNAL, schema: { - ...basicSchema, - [columnName]: { - name: columnName, - type: FieldType.STRING, + "user relationship": { + type: FieldType.LINK, + fieldName: "test", + name: "user relationship", + constraints: { + type: "array", + presence: false, + }, + relationshipType: RelationshipType.MANY_TO_ONE, + tableId: InternalTable.USER_METADATA, }, }, }) - expect(result).toEqual({ - allValid: false, - errors: { - [columnName]: `${columnName} is a protected column name`, - }, - invalidColumns: [], - schemaValidation: { - id: true, - name: true, - [columnName]: false, + const row1 = await config.api.row.save(table._id!, { + "user relationship": [users[0], users[1]], + }) + + const row2 = await config.api.row.save(table._id!, { + "user relationship": [users[2]], + }) + + await config.api.table.migrate(table._id!, { + oldColumn: "user relationship", + newColumn: "user column", + }) + + const migratedTable = await config.api.table.get(table._id!) + expect(migratedTable.schema["user column"]).toEqual({ + name: "user column", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + constraints: { + type: "array", }, }) + expect(migratedTable.schema["user relationship"]).not.toBeDefined() + + const row1Migrated = await config.api.row.get(table._id!, row1._id!) + expect(row1Migrated["user relationship"]).not.toBeDefined() + expect(row1Migrated["user column"].map((r: Row) => r._id)).toEqual( + expect.arrayContaining([users[0]._id, users[1]._id]) + ) + + const row2Migrated = await config.api.row.get(table._id!, row2._id!) + expect(row2Migrated["user relationship"]).not.toBeDefined() + expect(row2Migrated["user column"].map((r: Row) => r._id)).toEqual([ + users[2]._id, + ]) }) - it("validates required fields and valid rows", async () => { - const schema: TableSchema = { - ...basicSchema, + describe("unhappy paths", () => { + let table: Table + beforeAll(async () => { + table = await config.api.table.save( + tableForDatasource(datasource, { + schema: { + "user relationship": { + type: FieldType.LINK, + fieldName: "test", + name: "user relationship", + constraints: { + type: "array", + presence: false, + }, + relationshipType: RelationshipType.MANY_TO_ONE, + tableId: InternalTable.USER_METADATA, + }, + num: { + type: FieldType.NUMBER, + name: "num", + constraints: { + type: "number", + presence: false, + }, + }, + }, + }) + ) + }) + + it("should fail if the new column name is blank", async () => { + await config.api.table.migrate( + table._id!, + { + oldColumn: "user relationship", + newColumn: "", + }, + { status: 400 } + ) + }) + + it("should fail if the new column name is a reserved name", async () => { + await config.api.table.migrate( + table._id!, + { + oldColumn: "user relationship", + newColumn: "_id", + }, + { status: 400 } + ) + }) + + it("should fail if the new column name is the same as an existing column", async () => { + await config.api.table.migrate( + table._id!, + { + oldColumn: "user relationship", + newColumn: "num", + }, + { status: 400 } + ) + }) + + it("should fail if the old column name isn't a column in the table", async () => { + await config.api.table.migrate( + table._id!, + { + oldColumn: "not a column", + newColumn: "new column", + }, + { status: 400 } + ) + }) + }) + }) + + describe.each([ + [ + RowExportFormat.CSV, + (val: any) => JSON.stringify(val).replace(/"/g, "'"), + ], + [RowExportFormat.JSON, (val: any) => val], + ])("import validation (%s)", (_, userParser) => { + const basicSchema: TableSchema = { + id: { + type: FieldType.NUMBER, + name: "id", + }, name: { type: FieldType.STRING, name: "name", - constraints: { presence: true }, }, } - const result = await testDelegate( + const importCases: [ + string, + ( + rows: Row[], + schema: TableSchema + ) => Promise + ][] = [ [ - { id: generator.natural(), name: generator.first() }, - { id: generator.natural(), name: generator.first() }, + "validateNewTableImport", + async (rows: Row[], schema: TableSchema) => { + const result = await config.api.table.validateNewTableImport({ + rows, + schema, + }) + return result + }, ], - schema - ) - - expect(result).toEqual({ - allValid: true, - errors: {}, - invalidColumns: [], - schemaValidation: { - id: true, - name: true, - }, - }) - }) - - it("validates required fields and non-valid rows", async () => { - const schema: TableSchema = { - ...basicSchema, - name: { - type: FieldType.STRING, - name: "name", - constraints: { presence: true }, - }, - } - - const result = await testDelegate( [ - { id: generator.natural(), name: generator.first() }, - { id: generator.natural(), name: "" }, + "validateExistingTableImport", + async (rows: Row[], schema: TableSchema) => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + primary: ["id"], + schema, + }) + ) + const result = await config.api.table.validateExistingTableImport( + { + tableId: table._id, + rows, + } + ) + return result + }, ], - schema - ) + ] - expect(result).toEqual({ - allValid: false, - errors: {}, - invalidColumns: [], - schemaValidation: { - id: true, - name: false, - }, - }) - }) + describe.each(importCases)("%s", (_, testDelegate) => { + it("validates basic imports", async () => { + const result = await testDelegate( + [{ id: generator.natural(), name: generator.first() }], + basicSchema + ) - describe("bb references", () => { - const getUserValues = () => ({ - _id: docIds.generateGlobalUserID(), - primaryDisplay: generator.first(), - email: generator.email({}), - }) - - it("can validate user column imports", async () => { - const schema: TableSchema = { - ...basicSchema, - user: { - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - name: "user", - }, - } - - const result = await testDelegate( - [ - { - id: generator.natural(), - name: generator.first(), - user: userParser(getUserValues()), + expect(result).toEqual({ + allValid: true, + errors: {}, + invalidColumns: [], + schemaValidation: { + id: true, + name: true, }, - ], - schema - ) - - expect(result).toEqual({ - allValid: true, - errors: {}, - invalidColumns: [], - schemaValidation: { - id: true, - name: true, - user: true, - }, - }) - }) - - it("can validate user column imports with invalid data", async () => { - const schema: TableSchema = { - ...basicSchema, - user: { - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - name: "user", - }, - } - - const result = await testDelegate( - [ - { - id: generator.natural(), - name: generator.first(), - user: userParser(getUserValues()), - }, - { - id: generator.natural(), - name: generator.first(), - user: "no valid user data", - }, - ], - schema - ) - - expect(result).toEqual({ - allValid: false, - errors: {}, - invalidColumns: [], - schemaValidation: { - id: true, - name: true, - user: false, - }, - }) - }) - - it("can validate users column imports", async () => { - const schema: TableSchema = { - ...basicSchema, - user: { - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - name: "user", - externalType: "array", - }, - } - - const result = await testDelegate( - [ - { - id: generator.natural(), - name: generator.first(), - user: userParser([ - getUserValues(), - getUserValues(), - getUserValues(), - ]), - }, - ], - schema - ) - - expect(result).toEqual({ - allValid: true, - errors: {}, - invalidColumns: [], - schemaValidation: { - id: true, - name: true, - user: true, - }, - }) - }) - }) - }) - - describe("validateExistingTableImport", () => { - isInternal && - it("can reimport _id fields for internal tables", async () => { - const table = await config.api.table.save( - tableForDatasource(datasource, { - primary: ["id"], - schema: basicSchema, }) - ) - const result = await config.api.table.validateExistingTableImport({ - tableId: table._id, - rows: [ - { - _id: docIds.generateRowID(table._id!), - id: generator.natural(), - name: generator.first(), - }, - ], }) - expect(result).toEqual({ - allValid: true, - errors: {}, - invalidColumns: [], - schemaValidation: { - _id: true, - id: true, - name: true, - }, + it.each( + isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS + )("don't allow protected names in schema (%s)", async columnName => { + const result = await config.api.table.validateNewTableImport({ + rows: [ + { + id: generator.natural(), + name: generator.first(), + [columnName]: generator.word(), + }, + ], + schema: { + ...basicSchema, + }, + }) + + expect(result).toEqual({ + allValid: false, + errors: { + [columnName]: `${columnName} is a protected column name`, + }, + invalidColumns: [], + schemaValidation: { + id: true, + name: true, + [columnName]: false, + }, + }) + }) + + it("does not allow imports without rows", async () => { + const result = await testDelegate([], basicSchema) + + expect(result).toEqual({ + allValid: false, + errors: {}, + invalidColumns: [], + schemaValidation: {}, + }) + }) + + it("validates imports with some empty rows", async () => { + const result = await testDelegate( + [{}, { id: generator.natural(), name: generator.first() }, {}], + basicSchema + ) + + expect(result).toEqual({ + allValid: true, + errors: {}, + invalidColumns: [], + schemaValidation: { + id: true, + name: true, + }, + }) + }) + + isInternal && + it.each( + isInternal + ? PROTECTED_INTERNAL_COLUMNS + : PROTECTED_EXTERNAL_COLUMNS + )( + "don't allow protected names in the rows (%s)", + async columnName => { + const result = await config.api.table.validateNewTableImport({ + rows: [ + { + id: generator.natural(), + name: generator.first(), + }, + ], + schema: { + ...basicSchema, + [columnName]: { + name: columnName, + type: FieldType.STRING, + }, + }, + }) + + expect(result).toEqual({ + allValid: false, + errors: { + [columnName]: `${columnName} is a protected column name`, + }, + invalidColumns: [], + schemaValidation: { + id: true, + name: true, + [columnName]: false, + }, + }) + } + ) + + it("validates required fields and valid rows", async () => { + const schema: TableSchema = { + ...basicSchema, + name: { + type: FieldType.STRING, + name: "name", + constraints: { presence: true }, + }, + } + + const result = await testDelegate( + [ + { id: generator.natural(), name: generator.first() }, + { id: generator.natural(), name: generator.first() }, + ], + schema + ) + + expect(result).toEqual({ + allValid: true, + errors: {}, + invalidColumns: [], + schemaValidation: { + id: true, + name: true, + }, + }) + }) + + it("validates required fields and non-valid rows", async () => { + const schema: TableSchema = { + ...basicSchema, + name: { + type: FieldType.STRING, + name: "name", + constraints: { presence: true }, + }, + } + + const result = await testDelegate( + [ + { id: generator.natural(), name: generator.first() }, + { id: generator.natural(), name: "" }, + ], + schema + ) + + expect(result).toEqual({ + allValid: false, + errors: {}, + invalidColumns: [], + schemaValidation: { + id: true, + name: false, + }, + }) + }) + + describe("bb references", () => { + const getUserValues = () => ({ + _id: docIds.generateGlobalUserID(), + primaryDisplay: generator.first(), + email: generator.email({}), + }) + + it("can validate user column imports", async () => { + const schema: TableSchema = { + ...basicSchema, + user: { + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + name: "user", + }, + } + + const result = await testDelegate( + [ + { + id: generator.natural(), + name: generator.first(), + user: userParser(getUserValues()), + }, + ], + schema + ) + + expect(result).toEqual({ + allValid: true, + errors: {}, + invalidColumns: [], + schemaValidation: { + id: true, + name: true, + user: true, + }, + }) + }) + + it("can validate user column imports with invalid data", async () => { + const schema: TableSchema = { + ...basicSchema, + user: { + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + name: "user", + }, + } + + const result = await testDelegate( + [ + { + id: generator.natural(), + name: generator.first(), + user: userParser(getUserValues()), + }, + { + id: generator.natural(), + name: generator.first(), + user: "no valid user data", + }, + ], + schema + ) + + expect(result).toEqual({ + allValid: false, + errors: {}, + invalidColumns: [], + schemaValidation: { + id: true, + name: true, + user: false, + }, + }) + }) + + it("can validate users column imports", async () => { + const schema: TableSchema = { + ...basicSchema, + user: { + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + name: "user", + externalType: "array", + }, + } + + const result = await testDelegate( + [ + { + id: generator.natural(), + name: generator.first(), + user: userParser([ + getUserValues(), + getUserValues(), + getUserValues(), + ]), + }, + ], + schema + ) + + expect(result).toEqual({ + allValid: true, + errors: {}, + invalidColumns: [], + schemaValidation: { + id: true, + name: true, + user: true, + }, + }) + }) }) }) - }) - }) -}) + + describe("validateExistingTableImport", () => { + isInternal && + it("can reimport _id fields for internal tables", async () => { + const table = await config.api.table.save( + tableForDatasource(datasource, { + primary: ["id"], + schema: basicSchema, + }) + ) + const result = await config.api.table.validateExistingTableImport( + { + tableId: table._id, + rows: [ + { + _id: docIds.generateRowID(table._id!), + id: generator.natural(), + name: generator.first(), + }, + ], + } + ) + + expect(result).toEqual({ + allValid: true, + errors: {}, + invalidColumns: [], + schemaValidation: { + _id: true, + id: true, + name: true, + }, + }) + }) + }) + }) + } + ) +} diff --git a/packages/server/src/api/routes/tests/templates.spec.ts b/packages/server/src/api/routes/tests/templates.spec.ts index d5483c54b4..725938cb04 100644 --- a/packages/server/src/api/routes/tests/templates.spec.ts +++ b/packages/server/src/api/routes/tests/templates.spec.ts @@ -2,7 +2,6 @@ import * as setup from "./utilities" import path from "path" import nock from "nock" import { generator } from "@budibase/backend-core/tests" -import { features } from "@budibase/backend-core" interface App { background: string @@ -82,48 +81,36 @@ describe("/templates", () => { }) describe("create app from template", () => { - it.each(["sqs", "lucene"])( - `should be able to create an app from a template (%s)`, - async source => { - await features.testutils.withFeatureFlags( - "*", - { SQS: source === "sqs" }, - async () => { - const name = generator.guid().replaceAll("-", "") - const url = `/${name}` + it("should be able to create an app from a template", async () => { + const name = generator.guid().replaceAll("-", "") + const url = `/${name}` - const app = await config.api.application.create({ - name, - url, - useTemplate: "true", - templateName: "Agency Client Portal", - templateKey: "app/agency-client-portal", - }) - expect(app.name).toBe(name) - expect(app.url).toBe(url) + const app = await config.api.application.create({ + name, + url, + useTemplate: "true", + templateName: "Agency Client Portal", + templateKey: "app/agency-client-portal", + }) + expect(app.name).toBe(name) + expect(app.url).toBe(url) - await config.withApp(app, async () => { - const tables = await config.api.table.fetch() - expect(tables).toHaveLength(2) + await config.withApp(app, async () => { + const tables = await config.api.table.fetch() + expect(tables).toHaveLength(2) - tables.sort((a, b) => a.name.localeCompare(b.name)) - const [agencyProjects, users] = tables - expect(agencyProjects.name).toBe("Agency Projects") - expect(users.name).toBe("Users") + tables.sort((a, b) => a.name.localeCompare(b.name)) + const [agencyProjects, users] = tables + expect(agencyProjects.name).toBe("Agency Projects") + expect(users.name).toBe("Users") - const { rows } = await config.api.row.search( - agencyProjects._id!, - { - tableId: agencyProjects._id!, - query: {}, - } - ) + const { rows } = await config.api.row.search(agencyProjects._id!, { + tableId: agencyProjects._id!, + query: {}, + }) - expect(rows).toHaveLength(3) - }) - } - ) - } - ) + expect(rows).toHaveLength(3) + }) + }) }) }) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 415b22d407..23ae7c79d3 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -1,4 +1,3 @@ -import * as setup from "./utilities" import { CreateViewRequest, Datasource, @@ -37,1442 +36,812 @@ import { SearchFilters, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" -import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" +import { + DatabaseName, + datasourceDescribe, +} from "../../../integrations/tests/utils" import merge from "lodash/merge" import { quotas } from "@budibase/pro" -import { db, roles, features, context } from "@budibase/backend-core" +import { db, roles, context } from "@budibase/backend-core" -describe.each([ - ["lucene", undefined], - ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], - [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], -])("/v2/views (%s)", (name, dsProvider) => { - const config = setup.getConfig() - const isSqs = name === "sqs" - const isLucene = name === "lucene" - const isInternal = isSqs || isLucene +const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] }) - let table: Table - let rawDatasource: Datasource | undefined - let datasource: Datasource | undefined - let envCleanup: (() => void) | undefined +if (descriptions.length) { + describe.each(descriptions)( + "/v2/views ($dbName)", + ({ config, isInternal, dsProvider }) => { + let table: Table + let rawDatasource: Datasource | undefined + let datasource: Datasource | undefined - function saveTableRequest( - ...overrides: Partial>[] - ): SaveTableRequest { - const req: SaveTableRequest = { - name: generator.guid().replaceAll("-", "").substring(0, 16), - type: "table", - sourceType: datasource - ? TableSourceType.EXTERNAL - : TableSourceType.INTERNAL, - sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID, - primary: ["id"], - schema: { - id: { - type: FieldType.NUMBER, - name: "id", - autocolumn: true, - constraints: { - presence: true, - }, - }, - }, - } - return merge(req, ...overrides) - } - - function priceTable(): SaveTableRequest { - return saveTableRequest({ - schema: { - Price: { - type: FieldType.NUMBER, - name: "Price", - constraints: {}, - }, - Category: { - type: FieldType.STRING, - name: "Category", - constraints: { - type: "string", - }, - }, - }, - }) - } - - beforeAll(async () => { - await features.testutils.withFeatureFlags("*", { SQS: isSqs }, () => - config.init() - ) - - envCleanup = features.testutils.setFeatureFlags("*", { - SQS: isSqs, - }) - - if (dsProvider) { - rawDatasource = await dsProvider - datasource = await config.createDatasource({ - datasource: rawDatasource, - }) - } - table = await config.api.table.save(priceTable()) - }) - - afterAll(async () => { - setup.afterAll() - if (envCleanup) { - envCleanup() - } - }) - - beforeEach(() => { - jest.clearAllMocks() - mocks.licenses.useCloudFree() - }) - - describe("view crud", () => { - describe("create", () => { - it("persist the view when the view is successfully created", async () => { - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, + function saveTableRequest( + ...overrides: Partial>[] + ): SaveTableRequest { + const req: SaveTableRequest = { + name: generator.guid().replaceAll("-", "").substring(0, 16), + type: "table", + sourceType: datasource + ? TableSourceType.EXTERNAL + : TableSourceType.INTERNAL, + sourceId: datasource ? datasource._id! : INTERNAL_TABLE_SOURCE_ID, + primary: ["id"], schema: { - id: { visible: true }, - }, - } - const res = await config.api.viewV2.create(newView) - - expect(res).toEqual({ - ...newView, - id: expect.stringMatching(new RegExp(`${table._id!}_`)), - version: 2, - }) - }) - - it("can persist views with all fields", async () => { - const newView: Required> = { - name: generator.name(), - tableId: table._id!, - primaryDisplay: "id", - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.EQUAL, - field: "field", - value: "value", - }, - ], + id: { + type: FieldType.NUMBER, + name: "id", + autocolumn: true, + constraints: { + presence: true, }, - ], - }, - sort: { - field: "fieldToSort", - order: SortOrder.DESCENDING, - type: SortType.STRING, - }, - schema: { - id: { visible: true }, - Price: { - visible: true, }, }, } - const res = await config.api.viewV2.create(newView) + return merge(req, ...overrides) + } - const expected: ViewV2 = { - ...newView, + function priceTable(): SaveTableRequest { + return saveTableRequest({ schema: { - id: { visible: true }, Price: { - visible: true, + type: FieldType.NUMBER, + name: "Price", + constraints: {}, + }, + Category: { + type: FieldType.STRING, + name: "Category", + constraints: { + type: "string", + }, }, }, - query: { - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - $and: { - conditions: [ - { - $and: { - conditions: [ - { - equal: { - field: "value", + }) + } + + beforeAll(async () => { + await config.init() + + const ds = await dsProvider() + rawDatasource = ds.rawDatasource + datasource = ds.datasource + table = await config.api.table.save(priceTable()) + }) + + beforeEach(() => { + jest.clearAllMocks() + mocks.licenses.useCloudFree() + }) + + describe("view crud", () => { + describe("create", () => { + it("persist the view when the view is successfully created", async () => { + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + schema: { + id: { visible: true }, + }, + } + const res = await config.api.viewV2.create(newView) + + expect(res).toEqual({ + ...newView, + id: expect.stringMatching(new RegExp(`${table._id!}_`)), + version: 2, + }) + }) + + it("can persist views with all fields", async () => { + const newView: Required> = + { + name: generator.name(), + tableId: table._id!, + primaryDisplay: "id", + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", }, + ], + }, + ], + }, + sort: { + field: "fieldToSort", + order: SortOrder.DESCENDING, + type: SortType.STRING, + }, + schema: { + id: { visible: true }, + Price: { + visible: true, + }, + }, + } + const res = await config.api.viewV2.create(newView) + + const expected: ViewV2 = { + ...newView, + schema: { + id: { visible: true }, + Price: { + visible: true, + }, + }, + query: { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + $and: { + conditions: [ + { + $and: { + conditions: [ + { + equal: { + field: "value", + }, + }, + ], + }, + }, + ], + }, + }, + id: expect.any(String), + version: 2, + } + + expect(res).toEqual(expected) + }) + + it("can create a view with just a query field, no queryUI, for backwards compatibility", async () => { + const newView: Required< + Omit + > = { + name: generator.name(), + tableId: table._id!, + primaryDisplay: "id", + query: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", + }, + ], + sort: { + field: "fieldToSort", + order: SortOrder.DESCENDING, + type: SortType.STRING, + }, + schema: { + id: { visible: true }, + Price: { + visible: true, + }, + }, + } + const res = await config.api.viewV2.create(newView) + + const expected: ViewV2 = { + ...newView, + schema: { + id: { visible: true }, + Price: { + visible: true, + }, + }, + queryUI: { + logicalOperator: UILogicalOperator.ALL, + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", }, ], }, - }, - ], - }, - }, - id: expect.any(String), - version: 2, - } - - expect(res).toEqual(expected) - }) - - it("can create a view with just a query field, no queryUI, for backwards compatibility", async () => { - const newView: Required> = { - name: generator.name(), - tableId: table._id!, - primaryDisplay: "id", - query: [ - { - operator: BasicOperator.EQUAL, - field: "field", - value: "value", - }, - ], - sort: { - field: "fieldToSort", - order: SortOrder.DESCENDING, - type: SortType.STRING, - }, - schema: { - id: { visible: true }, - Price: { - visible: true, - }, - }, - } - const res = await config.api.viewV2.create(newView) - - const expected: ViewV2 = { - ...newView, - schema: { - id: { visible: true }, - Price: { - visible: true, - }, - }, - queryUI: { - logicalOperator: UILogicalOperator.ALL, - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - groups: [ - { - logicalOperator: UILogicalOperator.ALL, - filters: [ - { - operator: BasicOperator.EQUAL, - field: "field", - value: "value", - }, ], }, - ], - }, - id: expect.any(String), - version: 2, - } + id: expect.any(String), + version: 2, + } - expect(res).toEqual(expected) - }) + expect(res).toEqual(expected) + }) - it("persist only UI schema overrides", async () => { - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - schema: { - id: { - name: "id", - type: FieldType.NUMBER, - visible: true, - }, - Price: { - name: "Price", - type: FieldType.NUMBER, - visible: true, - order: 1, - width: 100, - }, - Category: { - name: "Category", - type: FieldType.STRING, - visible: false, - icon: "ic", - }, - } as ViewV2Schema, - } - - const createdView = await config.api.viewV2.create(newView) - - expect(createdView).toEqual({ - ...newView, - schema: { - id: { visible: true }, - Price: { - visible: true, - order: 1, - width: 100, - }, - Category: { - visible: false, - icon: "ic", - }, - }, - id: createdView.id, - version: 2, - }) - }) - - it("will not throw an exception if the schema is 'deleting' non UI fields", async () => { - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - schema: { - id: { - name: "id", - type: FieldType.NUMBER, - autocolumn: true, - visible: true, - }, - Price: { - name: "Price", - type: FieldType.NUMBER, - visible: true, - }, - Category: { - name: "Category", - type: FieldType.STRING, - }, - } as ViewV2Schema, - } - - await config.api.viewV2.create(newView, { - status: 201, - }) - }) - - it("does not persist non-visible fields", async () => { - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - primaryDisplay: "id", - schema: { - id: { visible: true }, - Price: { visible: true }, - Category: { visible: false }, - }, - } - const res = await config.api.viewV2.create(newView) - - expect(res).toEqual({ - ...newView, - schema: { - id: { visible: true }, - Price: { visible: true }, - Category: { visible: false }, - }, - id: expect.any(String), - version: 2, - }) - }) - - it("throws bad request when the schema fields are not valid", async () => { - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - schema: { - id: { visible: true }, - nonExisting: { - visible: true, - }, - }, - } - await config.api.viewV2.create(newView, { - status: 400, - body: { - message: 'Field "nonExisting" is not valid for the requested table', - }, - }) - }) - - describe("readonly fields", () => { - it("readonly fields are persisted", async () => { - const table = await config.api.table.save( - saveTableRequest({ + it("persist only UI schema overrides", async () => { + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - description: { - name: "description", - type: FieldType.STRING, - }, - }, - }) - ) - - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - schema: { - id: { visible: true }, - name: { - visible: true, - readonly: true, - }, - description: { - visible: true, - readonly: true, - }, - }, - } - - const res = await config.api.viewV2.create(newView) - expect(res.schema).toEqual({ - id: { visible: true }, - name: { - visible: true, - readonly: true, - }, - description: { - visible: true, - readonly: true, - }, - }) - }) - - it("required fields cannot be marked as readonly", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - name: "name", - type: FieldType.STRING, - constraints: { presence: true }, - }, - description: { - name: "description", - type: FieldType.STRING, - }, - }, - }) - ) - - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - schema: { - id: { visible: true }, - name: { - visible: true, - readonly: true, - }, - }, - } - - await config.api.viewV2.create(newView, { - status: 400, - body: { - message: - 'You can\'t make "name" readonly because it is a required field.', - status: 400, - }, - }) - }) - - it("readonly fields must be visible", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - description: { - name: "description", - type: FieldType.STRING, - }, - }, - }) - ) - - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - schema: { - id: { visible: true }, - name: { - visible: false, - readonly: true, - }, - }, - } - - await config.api.viewV2.create(newView, { - status: 400, - body: { - message: - 'Field "name" must be visible if you want to make it readonly', - status: 400, - }, - }) - }) - - it("readonly fields can be used on free license", async () => { - mocks.licenses.useCloudFree() - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - description: { - name: "description", - type: FieldType.STRING, - }, - }, - }) - ) - - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - schema: { - id: { visible: true }, - name: { - visible: true, - readonly: true, - }, - }, - } - - await config.api.viewV2.create(newView, { - status: 201, - }) - }) - }) - - it("display fields must be visible", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - description: { - name: "description", - type: FieldType.STRING, - }, - }, - }) - ) - - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - primaryDisplay: "name", - schema: { - id: { visible: true }, - name: { - visible: false, - }, - }, - } - - await config.api.viewV2.create(newView, { - status: 400, - body: { - message: 'You can\'t hide "name" because it is the display column.', - status: 400, - }, - }) - }) - - it("display fields can be readonly", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - description: { - name: "description", - type: FieldType.STRING, - }, - }, - }) - ) - - const newView: CreateViewRequest = { - name: generator.name(), - tableId: table._id!, - primaryDisplay: "name", - schema: { - id: { visible: true }, - name: { - visible: true, - readonly: true, - }, - }, - } - - await config.api.viewV2.create(newView, { - status: 201, - }) - }) - - it("can create a view with calculation fields", async () => { - let view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - sum: { - visible: true, - calculationType: CalculationType.SUM, - field: "Price", - }, - }, - }) - - expect(Object.keys(view.schema!)).toHaveLength(1) - - let sum = view.schema!.sum as NumericCalculationFieldMetadata - expect(sum).toBeDefined() - expect(sum.calculationType).toEqual(CalculationType.SUM) - expect(sum.field).toEqual("Price") - - view = await config.api.viewV2.get(view.id) - sum = view.schema!.sum as NumericCalculationFieldMetadata - expect(sum).toBeDefined() - expect(sum.calculationType).toEqual(CalculationType.SUM) - expect(sum.field).toEqual("Price") - }) - - it("cannot create a view with calculation fields unless it has the right type", async () => { - await config.api.viewV2.create( - { - tableId: table._id!, - name: generator.guid(), - schema: { - sum: { - visible: true, - calculationType: CalculationType.SUM, - field: "Price", - }, - }, - }, - { - status: 400, - body: { - message: - "Calculation fields are not allowed in non-calculation views", - }, - } - ) - }) - - it("cannot create a calculation view with more than 5 aggregations", async () => { - await config.api.viewV2.create( - { - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - sum: { - visible: true, - calculationType: CalculationType.SUM, - field: "Price", - }, - count: { - visible: true, - calculationType: CalculationType.COUNT, - field: "Price", - }, - countDistinct: { - visible: true, - calculationType: CalculationType.COUNT, - distinct: true, - field: "Price", - }, - min: { - visible: true, - calculationType: CalculationType.MIN, - field: "Price", - }, - max: { - visible: true, - calculationType: CalculationType.MAX, - field: "Price", - }, - avg: { - visible: true, - calculationType: CalculationType.AVG, - field: "Price", - }, - }, - }, - { - status: 400, - body: { - message: "Calculation views can only have a maximum of 5 fields", - }, - } - ) - }) - - it("cannot create a calculation view with duplicate calculations", async () => { - await config.api.viewV2.create( - { - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - sum: { - visible: true, - calculationType: CalculationType.SUM, - field: "Price", - }, - sum2: { - visible: true, - calculationType: CalculationType.SUM, - field: "Price", - }, - }, - }, - { - status: 400, - body: { - message: - 'Duplicate calculation on field "Price", calculation type "sum"', - }, - } - ) - }) - - it("finds duplicate counts", async () => { - await config.api.viewV2.create( - { - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - count: { - visible: true, - calculationType: CalculationType.COUNT, - field: "Price", - }, - count2: { - visible: true, - calculationType: CalculationType.COUNT, - field: "Price", - }, - }, - }, - { - status: 400, - body: { - message: - 'Duplicate calculation on field "Price", calculation type "count"', - }, - } - ) - }) - - it("finds duplicate count distincts", async () => { - await config.api.viewV2.create( - { - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - count: { - visible: true, - calculationType: CalculationType.COUNT, - distinct: true, - field: "Price", - }, - count2: { - visible: true, - calculationType: CalculationType.COUNT, - distinct: true, - field: "Price", - }, - }, - }, - { - status: 400, - body: { - message: - 'Duplicate calculation on field "Price", calculation type "count distinct"', - }, - } - ) - }) - - it("does not confuse counts and count distincts in the duplicate check", async () => { - await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - count: { - visible: true, - calculationType: CalculationType.COUNT, - field: "Price", - }, - count2: { - visible: true, - calculationType: CalculationType.COUNT, - distinct: true, - field: "Price", - }, - }, - }) - }) - - it("does not confuse counts on different fields in the duplicate check", async () => { - await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - count: { - visible: true, - calculationType: CalculationType.COUNT, - field: "Price", - }, - count2: { - visible: true, - calculationType: CalculationType.COUNT, - field: "Category", - }, - }, - }) - }) - - !isLucene && - it("does not get confused when a calculation field shadows a basic one", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - age: { - name: "age", + id: { + name: "id", type: FieldType.NUMBER, + visible: true, + }, + Price: { + name: "Price", + type: FieldType.NUMBER, + visible: true, + order: 1, + width: 100, + }, + Category: { + name: "Category", + type: FieldType.STRING, + visible: false, + icon: "ic", + }, + } as ViewV2Schema, + } + + const createdView = await config.api.viewV2.create(newView) + + expect(createdView).toEqual({ + ...newView, + schema: { + id: { visible: true }, + Price: { + visible: true, + order: 1, + width: 100, + }, + Category: { + visible: false, + icon: "ic", + }, + }, + id: createdView.id, + version: 2, + }) + }) + + it("will not throw an exception if the schema is 'deleting' non UI fields", async () => { + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + schema: { + id: { + name: "id", + type: FieldType.NUMBER, + autocolumn: true, + visible: true, + }, + Price: { + name: "Price", + type: FieldType.NUMBER, + visible: true, + }, + Category: { + name: "Category", + type: FieldType.STRING, + }, + } as ViewV2Schema, + } + + await config.api.viewV2.create(newView, { + status: 201, + }) + }) + + it("does not persist non-visible fields", async () => { + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + primaryDisplay: "id", + schema: { + id: { visible: true }, + Price: { visible: true }, + Category: { visible: false }, + }, + } + const res = await config.api.viewV2.create(newView) + + expect(res).toEqual({ + ...newView, + schema: { + id: { visible: true }, + Price: { visible: true }, + Category: { visible: false }, + }, + id: expect.any(String), + version: 2, + }) + }) + + it("throws bad request when the schema fields are not valid", async () => { + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + schema: { + id: { visible: true }, + nonExisting: { + visible: true, + }, + }, + } + await config.api.viewV2.create(newView, { + status: 400, + body: { + message: + 'Field "nonExisting" is not valid for the requested table', + }, + }) + }) + + describe("readonly fields", () => { + it("readonly fields are persisted", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + description: { + name: "description", + type: FieldType.STRING, + }, + }, + }) + ) + + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + schema: { + id: { visible: true }, + name: { + visible: true, + readonly: true, + }, + description: { + visible: true, + readonly: true, + }, + }, + } + + const res = await config.api.viewV2.create(newView) + expect(res.schema).toEqual({ + id: { visible: true }, + name: { + visible: true, + readonly: true, + }, + description: { + visible: true, + readonly: true, + }, + }) + }) + + it("required fields cannot be marked as readonly", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: true }, + }, + description: { + name: "description", + type: FieldType.STRING, + }, + }, + }) + ) + + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + schema: { + id: { visible: true }, + name: { + visible: true, + readonly: true, + }, + }, + } + + await config.api.viewV2.create(newView, { + status: 400, + body: { + message: + 'You can\'t make "name" readonly because it is a required field.', + status: 400, + }, + }) + }) + + it("readonly fields must be visible", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + description: { + name: "description", + type: FieldType.STRING, + }, + }, + }) + ) + + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + schema: { + id: { visible: true }, + name: { + visible: false, + readonly: true, + }, + }, + } + + await config.api.viewV2.create(newView, { + status: 400, + body: { + message: + 'Field "name" must be visible if you want to make it readonly', + status: 400, + }, + }) + }) + + it("readonly fields can be used on free license", async () => { + mocks.licenses.useCloudFree() + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + description: { + name: "description", + type: FieldType.STRING, + }, + }, + }) + ) + + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + schema: { + id: { visible: true }, + name: { + visible: true, + readonly: true, + }, + }, + } + + await config.api.viewV2.create(newView, { + status: 201, + }) + }) + }) + + it("display fields must be visible", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + description: { + name: "description", + type: FieldType.STRING, + }, + }, + }) + ) + + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + primaryDisplay: "name", + schema: { + id: { visible: true }, + name: { + visible: false, + }, + }, + } + + await config.api.viewV2.create(newView, { + status: 400, + body: { + message: + 'You can\'t hide "name" because it is the display column.', + status: 400, + }, + }) + }) + + it("display fields can be readonly", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + description: { + name: "description", + type: FieldType.STRING, + }, + }, + }) + ) + + const newView: CreateViewRequest = { + name: generator.name(), + tableId: table._id!, + primaryDisplay: "name", + schema: { + id: { visible: true }, + name: { + visible: true, + readonly: true, + }, + }, + } + + await config.api.viewV2.create(newView, { + status: 201, + }) + }) + + it("can create a view with calculation fields", async () => { + let view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "Price", }, }, }) - ) - await config.api.row.bulkImport(table._id!, { - rows: [{ age: 1 }, { age: 2 }, { age: 3 }], + expect(Object.keys(view.schema!)).toHaveLength(1) + + let sum = view.schema!.sum as NumericCalculationFieldMetadata + expect(sum).toBeDefined() + expect(sum.calculationType).toEqual(CalculationType.SUM) + expect(sum.field).toEqual("Price") + + view = await config.api.viewV2.get(view.id) + sum = view.schema!.sum as NumericCalculationFieldMetadata + expect(sum).toBeDefined() + expect(sum.calculationType).toEqual(CalculationType.SUM) + expect(sum.field).toEqual("Price") }) - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - age: { - visible: true, - calculationType: CalculationType.SUM, - field: "age", + it("cannot create a view with calculation fields unless it has the right type", async () => { + await config.api.viewV2.create( + { + tableId: table._id!, + name: generator.guid(), + schema: { + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "Price", + }, + }, }, - }, + { + status: 400, + body: { + message: + "Calculation fields are not allowed in non-calculation views", + }, + } + ) }) - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(1) - expect(rows[0].age).toEqual(6) - }) - - // We don't allow the creation of tables with most JsonTypes when using - // external datasources. - isInternal && - it("cannot use complex types as group-by fields", async () => { - for (const type of JsonTypes) { - const field = { name: "field", type } as FieldSchema - const table = await config.api.table.save( - saveTableRequest({ schema: { field } }) - ) + it("cannot create a calculation view with more than 5 aggregations", async () => { await config.api.viewV2.create( { tableId: table._id!, name: generator.guid(), type: ViewV2Type.CALCULATION, schema: { - field: { visible: true }, + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "Price", + }, + count: { + visible: true, + calculationType: CalculationType.COUNT, + field: "Price", + }, + countDistinct: { + visible: true, + calculationType: CalculationType.COUNT, + distinct: true, + field: "Price", + }, + min: { + visible: true, + calculationType: CalculationType.MIN, + field: "Price", + }, + max: { + visible: true, + calculationType: CalculationType.MAX, + field: "Price", + }, + avg: { + visible: true, + calculationType: CalculationType.AVG, + field: "Price", + }, }, }, { status: 400, body: { - message: `Grouping by fields of type "${type}" is not supported`, + message: + "Calculation views can only have a maximum of 5 fields", }, } ) - } - }) + }) - isInternal && - it("shouldn't trigger a complex type check on a group by field if field is invisible", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - field: { - name: "field", - type: FieldType.JSON, + it("cannot create a calculation view with duplicate calculations", async () => { + await config.api.viewV2.create( + { + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "Price", + }, + sum2: { + visible: true, + calculationType: CalculationType.SUM, + field: "Price", + }, }, }, - }) - ) + { + status: 400, + body: { + message: + 'Duplicate calculation on field "Price", calculation type "sum"', + }, + } + ) + }) - await config.api.viewV2.create( - { + it("finds duplicate counts", async () => { + await config.api.viewV2.create( + { + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + count: { + visible: true, + calculationType: CalculationType.COUNT, + field: "Price", + }, + count2: { + visible: true, + calculationType: CalculationType.COUNT, + field: "Price", + }, + }, + }, + { + status: 400, + body: { + message: + 'Duplicate calculation on field "Price", calculation type "count"', + }, + } + ) + }) + + it("finds duplicate count distincts", async () => { + await config.api.viewV2.create( + { + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + count: { + visible: true, + calculationType: CalculationType.COUNT, + distinct: true, + field: "Price", + }, + count2: { + visible: true, + calculationType: CalculationType.COUNT, + distinct: true, + field: "Price", + }, + }, + }, + { + status: 400, + body: { + message: + 'Duplicate calculation on field "Price", calculation type "count distinct"', + }, + } + ) + }) + + it("does not confuse counts and count distincts in the duplicate check", async () => { + await config.api.viewV2.create({ tableId: table._id!, name: generator.guid(), type: ViewV2Type.CALCULATION, schema: { - field: { visible: false }, + count: { + visible: true, + calculationType: CalculationType.COUNT, + field: "Price", + }, + count2: { + visible: true, + calculationType: CalculationType.COUNT, + distinct: true, + field: "Price", + }, }, - }, - { - status: 201, - } - ) - }) - }) - - describe("update", () => { - let view: ViewV2 - let table: Table - - beforeEach(async () => { - table = await config.api.table.save(priceTable()) - - view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - }, - }) - }) - - it("can update an existing view data", async () => { - const tableId = table._id! - await config.api.viewV2.update({ - ...view, - query: [ - { - operator: BasicOperator.EQUAL, - field: "newField", - value: "thatValue", - }, - ], - }) - - const expected: ViewV2 = { - ...view, - query: [ - { - operator: BasicOperator.EQUAL, - field: "newField", - value: "thatValue", - }, - ], - // Should also update queryUI because query was not previously set. - queryUI: { - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - logicalOperator: UILogicalOperator.ALL, - groups: [ - { - logicalOperator: UILogicalOperator.ALL, - filters: [ - { - operator: BasicOperator.EQUAL, - field: "newField", - value: "thatValue", - }, - ], - }, - ], - }, - schema: expect.anything(), - } - - expect((await config.api.table.get(tableId)).views).toEqual({ - [view.name]: expected, - }) - }) - - it("can update all fields", async () => { - const tableId = table._id! - - const updatedData: Required< - Omit - > = { - version: view.version, - id: view.id, - tableId, - name: view.name, - primaryDisplay: "Price", - query: [ - { - operator: BasicOperator.EQUAL, - field: "newField", - value: "newValue", - }, - ], - sort: { - field: generator.word(), - order: SortOrder.DESCENDING, - type: SortType.STRING, - }, - schema: { - id: { visible: true }, - Category: { - visible: false, - }, - Price: { - visible: true, - readonly: true, - }, - }, - } - await config.api.viewV2.update(updatedData) - - const expected: ViewV2 = { - ...updatedData, - // queryUI gets generated from query - queryUI: { - logicalOperator: UILogicalOperator.ALL, - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - groups: [ - { - logicalOperator: UILogicalOperator.ALL, - filters: [ - { - operator: BasicOperator.EQUAL, - field: "newField", - value: "newValue", - }, - ], - }, - ], - }, - schema: { - ...table.schema, - id: expect.objectContaining({ - visible: true, - }), - Category: expect.objectContaining({ - visible: false, - }), - Price: expect.objectContaining({ - visible: true, - readonly: true, - }), - }, - } - - expect((await config.api.table.get(tableId)).views).toEqual({ - [view.name]: expected, - }) - }) - - it("can update an existing view name", async () => { - const tableId = table._id! - const newName = generator.guid() - await config.api.viewV2.update({ ...view, name: newName }) - - expect(await config.api.table.get(tableId)).toEqual( - expect.objectContaining({ - views: { - [newName]: { ...view, name: newName, schema: expect.anything() }, - }, - }) - ) - }) - - it("cannot update an unexisting views nor edit ids", async () => { - const tableId = table._id! - await config.api.viewV2.update( - { ...view, id: generator.guid() }, - { status: 404 } - ) - - expect(await config.api.table.get(tableId)).toEqual( - expect.objectContaining({ - views: { - [view.name]: { - ...view, - schema: expect.anything(), - }, - }, - }) - ) - }) - - it("cannot update views with the wrong tableId", async () => { - const tableId = table._id! - await config.api.viewV2.update( - { - ...view, - tableId: generator.guid(), - query: [ - { - operator: BasicOperator.EQUAL, - field: "newField", - value: "thatValue", - }, - ], - }, - { status: 404 } - ) - - expect(await config.api.table.get(tableId)).toEqual( - expect.objectContaining({ - views: { - [view.name]: { - ...view, - schema: expect.anything(), - }, - }, - }) - ) - }) - - isInternal && - it("cannot update views v1", async () => { - const viewV1 = await config.api.legacyView.save({ - tableId: table._id!, - name: generator.guid(), - filters: [], - schema: {}, + }) }) - await config.api.viewV2.update(viewV1 as unknown as ViewV2, { - status: 400, - body: { - message: "Only views V2 can be updated", - status: 400, - }, - }) - }) - - it("cannot update the a view with unmatching ids between url and body", async () => { - const anotherView = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - }, - }) - const result = await config - .request!.put(`/api/v2/views/${anotherView.id}`) - .send(view) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) - .expect(400) - - expect(result.body).toEqual({ - message: "View id does not match between the body and the uri path", - status: 400, - }) - }) - - it("updates only UI schema overrides", async () => { - const updatedView = await config.api.viewV2.update({ - ...view, - schema: { - ...view.schema, - Price: { - name: "Price", - type: FieldType.NUMBER, - visible: true, - order: 1, - width: 100, - }, - Category: { - name: "Category", - type: FieldType.STRING, - visible: false, - icon: "ic", - }, - } as ViewV2Schema, - }) - - expect(updatedView).toEqual({ - ...view, - schema: { - id: { visible: true }, - Price: { - visible: true, - order: 1, - width: 100, - }, - Category: { visible: false, icon: "ic" }, - }, - id: view.id, - version: 2, - }) - }) - - it("will not throw an exception if the schema is 'deleting' non UI fields", async () => { - await config.api.viewV2.update( - { - ...view, - schema: { - ...view.schema, - Price: { - name: "Price", - type: FieldType.NUMBER, - visible: true, - }, - Category: { - name: "Category", - type: FieldType.STRING, - }, - } as ViewV2Schema, - }, - { - status: 200, - } - ) - }) - - it("cannot update view type after creation", async () => { - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - Price: { - visible: true, - }, - }, - }) - - await config.api.viewV2.update( - { - ...view, - type: ViewV2Type.CALCULATION, - }, - { - status: 400, - body: { - message: "Cannot update view type after creation", - }, - } - ) - }) - - isInternal && - it("updating schema will only validate modified field", async () => { - let view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - Price: { - visible: true, - }, - Category: { visible: true }, - }, - }) - - // Update the view to an invalid state - const tableToUpdate = await config.api.table.get(table._id!) - ;(tableToUpdate.views![view.name] as ViewV2).schema!.id.visible = - false - await db.getDB(config.appId!).put(tableToUpdate) - - view = await config.api.viewV2.get(view.id) - await config.api.viewV2.update( - { - ...view, + it("does not confuse counts on different fields in the duplicate check", async () => { + await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, schema: { - ...view.schema, - Price: { - visible: false, + count: { + visible: true, + calculationType: CalculationType.COUNT, + field: "Price", + }, + count2: { + visible: true, + calculationType: CalculationType.COUNT, + field: "Category", }, }, - }, - { - status: 400, - body: { - message: 'You can\'t hide "id" because it is a required field.', - status: 400, - }, - } - ) - }) + }) + }) - it("can update queryUI field and query gets regenerated", async () => { - await config.api.viewV2.update({ - ...view, - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.EQUAL, - field: "field", - value: "value", - }, - ], - }, - ], - }, - }) - - let updatedView = await config.api.viewV2.get(view.id) - let expected: SearchFilters = { - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - $and: { - conditions: [ - { - $and: { - conditions: [ - { - equal: { field: "value" }, - }, - ], - }, - }, - ], - }, - } - expect(updatedView.query).toEqual(expected) - - await config.api.viewV2.update({ - ...updatedView, - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.EQUAL, - field: "newField", - value: "newValue", - }, - ], - }, - ], - }, - }) - - updatedView = await config.api.viewV2.get(view.id) - expected = { - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - $and: { - conditions: [ - { - $and: { - conditions: [ - { - equal: { newField: "newValue" }, - }, - ], - }, - }, - ], - }, - } - expect(updatedView.query).toEqual(expected) - }) - - it("can delete either query and it will get regenerated from queryUI", async () => { - await config.api.viewV2.update({ - ...view, - query: [ - { - operator: BasicOperator.EQUAL, - field: "field", - value: "value", - }, - ], - }) - - let updatedView = await config.api.viewV2.get(view.id) - expect(updatedView.queryUI).toBeDefined() - - await config.api.viewV2.update({ - ...updatedView, - query: undefined, - }) - - updatedView = await config.api.viewV2.get(view.id) - expect(updatedView.query).toBeDefined() - }) - - // This is because the conversion from queryUI -> query loses data, so you - // can't accurately reproduce the original queryUI from the query. If - // query is a LegacyFilter[] we allow it, because for Budibase v3 - // everything in the db had query set to a LegacyFilter[], and there's no - // loss of information converting from a LegacyFilter[] to a - // UISearchFilter. But we convert to a SearchFilters and that can't be - // accurately converted to a UISearchFilter. - it("can't regenerate queryUI from a query once it has been generated from a queryUI", async () => { - await config.api.viewV2.update({ - ...view, - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.EQUAL, - field: "field", - value: "value", - }, - ], - }, - ], - }, - }) - - let updatedView = await config.api.viewV2.get(view.id) - expect(updatedView.query).toBeDefined() - - await config.api.viewV2.update( - { - ...updatedView, - queryUI: undefined, - }, - { - status: 400, - body: { - message: "view is missing queryUI field", - }, - } - ) - }) - - !isLucene && - describe("calculation views", () => { - let table: Table - let view: ViewV2 - - beforeEach(async () => { - table = await config.api.table.save( + it("does not get confused when a calculation field shadows a basic one", async () => { + const table = await config.api.table.save( saveTableRequest({ schema: { - name: { - name: "name", - type: FieldType.STRING, - constraints: { - presence: true, - }, - }, - country: { - name: "country", - type: FieldType.STRING, - }, age: { name: "age", type: FieldType.NUMBER, @@ -1481,14 +850,15 @@ describe.each([ }) ) - view = await config.api.viewV2.create({ + await config.api.row.bulkImport(table._id!, { + rows: [{ age: 1 }, { age: 2 }, { age: 3 }], + }) + + const view = await config.api.viewV2.create({ tableId: table._id!, name: generator.guid(), type: ViewV2Type.CALCULATION, schema: { - country: { - visible: true, - }, age: { visible: true, calculationType: CalculationType.SUM, @@ -1497,866 +867,1849 @@ describe.each([ }, }) - await config.api.row.bulkImport(table._id!, { - rows: [ + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(1) + expect(rows[0].age).toEqual(6) + }) + + // We don't allow the creation of tables with most JsonTypes when using + // external datasources. + isInternal && + it("cannot use complex types as group-by fields", async () => { + for (const type of JsonTypes) { + const field = { name: "field", type } as FieldSchema + const table = await config.api.table.save( + saveTableRequest({ schema: { field } }) + ) + await config.api.viewV2.create( + { + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + field: { visible: true }, + }, + }, + { + status: 400, + body: { + message: `Grouping by fields of type "${type}" is not supported`, + }, + } + ) + } + }) + + isInternal && + it("shouldn't trigger a complex type check on a group by field if field is invisible", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + field: { + name: "field", + type: FieldType.JSON, + }, + }, + }) + ) + + await config.api.viewV2.create( { - name: "Steve", - age: 30, - country: "UK", + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + field: { visible: false }, + }, }, { - name: "Jane", - age: 31, - country: "UK", - }, - { - name: "Ruari", - age: 32, - country: "USA", - }, - { - name: "Alice", - age: 33, - country: "USA", - }, - ], + status: 201, + } + ) + }) + }) + + describe("update", () => { + let view: ViewV2 + let table: Table + + beforeEach(async () => { + table = await config.api.table.save(priceTable()) + + view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + id: { visible: true }, + }, }) }) - it("returns the expected rows prior to modification", async () => { - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(2) - expect(rows).toEqual( - expect.arrayContaining([ + it("can update an existing view data", async () => { + const tableId = table._id! + await config.api.viewV2.update({ + ...view, + query: [ { - country: "USA", - age: 65, + operator: BasicOperator.EQUAL, + field: "newField", + value: "thatValue", }, + ], + }) + + const expected: ViewV2 = { + ...view, + query: [ { - country: "UK", - age: 61, + operator: BasicOperator.EQUAL, + field: "newField", + value: "thatValue", }, - ]) - ) - }) - - it("can remove a group by field", async () => { - delete view.schema!.country - await config.api.viewV2.update(view) - - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(1) - expect(rows).toEqual( - expect.arrayContaining([ - { - age: 126, - }, - ]) - ) - }) - - it("can remove a calculation field", async () => { - delete view.schema!.age - await config.api.viewV2.update(view) - - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(4) - - // Because the removal of the calculation field actually makes this - // no longer a calculation view, these rows will now have _id and - // _rev fields. - expect(rows).toEqual( - expect.arrayContaining([ - expect.objectContaining({ country: "UK" }), - expect.objectContaining({ country: "UK" }), - expect.objectContaining({ country: "USA" }), - expect.objectContaining({ country: "USA" }), - ]) - ) - }) - - it("can add a new group by field", async () => { - view.schema!.name = { visible: true } - await config.api.viewV2.update(view) - - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(4) - expect(rows).toEqual( - expect.arrayContaining([ - { - name: "Steve", - age: 30, - country: "UK", - }, - { - name: "Jane", - age: 31, - country: "UK", - }, - { - name: "Ruari", - age: 32, - country: "USA", - }, - { - name: "Alice", - age: 33, - country: "USA", - }, - ]) - ) - }) - - it("can add a new group by field that is invisible, even if required on the table", async () => { - view.schema!.name = { visible: false } - await config.api.viewV2.update(view) - - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(2) - expect(rows).toEqual( - expect.arrayContaining([ - { - country: "USA", - age: 65, - }, - { - country: "UK", - age: 61, - }, - ]) - ) - }) - - it("can add a new calculation field", async () => { - view.schema!.count = { - visible: true, - calculationType: CalculationType.COUNT, - field: "age", + ], + // Should also update queryUI because query was not previously set. + queryUI: { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + logicalOperator: UILogicalOperator.ALL, + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "newField", + value: "thatValue", + }, + ], + }, + ], + }, + schema: expect.anything(), } - await config.api.viewV2.update(view) - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(2) - expect(rows).toEqual( - expect.arrayContaining([ + expect((await config.api.table.get(tableId)).views).toEqual({ + [view.name]: expected, + }) + }) + + it("can update all fields", async () => { + const tableId = table._id! + + const updatedData: Required< + Omit + > = { + version: view.version, + id: view.id, + tableId, + name: view.name, + primaryDisplay: "Price", + query: [ { - country: "USA", - age: 65, - count: 2, + operator: BasicOperator.EQUAL, + field: "newField", + value: "newValue", }, - { - country: "UK", - age: 61, - count: 2, + ], + sort: { + field: generator.word(), + order: SortOrder.DESCENDING, + type: SortType.STRING, + }, + schema: { + id: { visible: true }, + Category: { + visible: false, }, - ]) + Price: { + visible: true, + readonly: true, + }, + }, + } + await config.api.viewV2.update(updatedData) + + const expected: ViewV2 = { + ...updatedData, + // queryUI gets generated from query + queryUI: { + logicalOperator: UILogicalOperator.ALL, + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "newField", + value: "newValue", + }, + ], + }, + ], + }, + schema: { + ...table.schema, + id: expect.objectContaining({ + visible: true, + }), + Category: expect.objectContaining({ + visible: false, + }), + Price: expect.objectContaining({ + visible: true, + readonly: true, + }), + }, + } + + expect((await config.api.table.get(tableId)).views).toEqual({ + [view.name]: expected, + }) + }) + + it("can update an existing view name", async () => { + const tableId = table._id! + const newName = generator.guid() + await config.api.viewV2.update({ ...view, name: newName }) + + expect(await config.api.table.get(tableId)).toEqual( + expect.objectContaining({ + views: { + [newName]: { + ...view, + name: newName, + schema: expect.anything(), + }, + }, + }) ) }) - }) - }) - describe("delete", () => { - let view: ViewV2 + it("cannot update an unexisting views nor edit ids", async () => { + const tableId = table._id! + await config.api.viewV2.update( + { ...view, id: generator.guid() }, + { status: 404 } + ) - beforeAll(async () => { - view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - }, - }) - }) - - it("can delete an existing view", async () => { - const tableId = table._id! - const getPersistedView = async () => - (await config.api.table.get(tableId)).views![view.name] - - expect(await getPersistedView()).toBeDefined() - - await config.api.viewV2.delete(view.id) - - expect(await getPersistedView()).toBeUndefined() - }) - }) - - describe.each([ - ["from view api", (view: ViewV2) => config.api.viewV2.get(view.id)], - [ - "from table", - async (view: ViewV2) => { - const table = await config.api.table.get(view.tableId) - return table.views![view.name] as ViewV2 - }, - ], - ])("read (%s)", (_, getDelegate) => { - let table: Table - let tableId: string - - beforeEach(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - one: { - type: FieldType.STRING, - name: "one", - }, - two: { - type: FieldType.STRING, - name: "two", - }, - three: { - type: FieldType.STRING, - name: "three", - }, - }, + expect(await config.api.table.get(tableId)).toEqual( + expect.objectContaining({ + views: { + [view.name]: { + ...view, + schema: expect.anything(), + }, + }, + }) + ) }) - ) - tableId = table._id! - }) - it("retrieves the view data with the enriched schema", async () => { - const view = await config.api.viewV2.create({ - tableId, - name: generator.guid(), - schema: { - id: { visible: true }, - one: { visible: true }, - two: { visible: true }, - }, + it("cannot update views with the wrong tableId", async () => { + const tableId = table._id! + await config.api.viewV2.update( + { + ...view, + tableId: generator.guid(), + query: [ + { + operator: BasicOperator.EQUAL, + field: "newField", + value: "thatValue", + }, + ], + }, + { status: 404 } + ) + + expect(await config.api.table.get(tableId)).toEqual( + expect.objectContaining({ + views: { + [view.name]: { + ...view, + schema: expect.anything(), + }, + }, + }) + ) + }) + + isInternal && + it("cannot update views v1", async () => { + const viewV1 = await config.api.legacyView.save({ + tableId: table._id!, + name: generator.guid(), + filters: [], + schema: {}, + }) + + await config.api.viewV2.update(viewV1 as unknown as ViewV2, { + status: 400, + body: { + message: "Only views V2 can be updated", + status: 400, + }, + }) + }) + + it("cannot update the a view with unmatching ids between url and body", async () => { + const anotherView = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + id: { visible: true }, + }, + }) + const result = await config + .request!.put(`/api/v2/views/${anotherView.id}`) + .send(view) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(400) + + expect(result.body).toEqual({ + message: + "View id does not match between the body and the uri path", + status: 400, + }) + }) + + it("updates only UI schema overrides", async () => { + const updatedView = await config.api.viewV2.update({ + ...view, + schema: { + ...view.schema, + Price: { + name: "Price", + type: FieldType.NUMBER, + visible: true, + order: 1, + width: 100, + }, + Category: { + name: "Category", + type: FieldType.STRING, + visible: false, + icon: "ic", + }, + } as ViewV2Schema, + }) + + expect(updatedView).toEqual({ + ...view, + schema: { + id: { visible: true }, + Price: { + visible: true, + order: 1, + width: 100, + }, + Category: { visible: false, icon: "ic" }, + }, + id: view.id, + version: 2, + }) + }) + + it("will not throw an exception if the schema is 'deleting' non UI fields", async () => { + await config.api.viewV2.update( + { + ...view, + schema: { + ...view.schema, + Price: { + name: "Price", + type: FieldType.NUMBER, + visible: true, + }, + Category: { + name: "Category", + type: FieldType.STRING, + }, + } as ViewV2Schema, + }, + { + status: 200, + } + ) + }) + + it("cannot update view type after creation", async () => { + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + id: { visible: true }, + Price: { + visible: true, + }, + }, + }) + + await config.api.viewV2.update( + { + ...view, + type: ViewV2Type.CALCULATION, + }, + { + status: 400, + body: { + message: "Cannot update view type after creation", + }, + } + ) + }) + + isInternal && + it("updating schema will only validate modified field", async () => { + let view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + id: { visible: true }, + Price: { + visible: true, + }, + Category: { visible: true }, + }, + }) + + // Update the view to an invalid state + const tableToUpdate = await config.api.table.get(table._id!) + ;(tableToUpdate.views![view.name] as ViewV2).schema!.id.visible = + false + await db.getDB(config.appId!).put(tableToUpdate) + + view = await config.api.viewV2.get(view.id) + await config.api.viewV2.update( + { + ...view, + schema: { + ...view.schema, + Price: { + visible: false, + }, + }, + }, + { + status: 400, + body: { + message: + 'You can\'t hide "id" because it is a required field.', + status: 400, + }, + } + ) + }) + + it("can update queryUI field and query gets regenerated", async () => { + await config.api.viewV2.update({ + ...view, + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", + }, + ], + }, + ], + }, + }) + + let updatedView = await config.api.viewV2.get(view.id) + let expected: SearchFilters = { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + $and: { + conditions: [ + { + $and: { + conditions: [ + { + equal: { field: "value" }, + }, + ], + }, + }, + ], + }, + } + expect(updatedView.query).toEqual(expected) + + await config.api.viewV2.update({ + ...updatedView, + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.EQUAL, + field: "newField", + value: "newValue", + }, + ], + }, + ], + }, + }) + + updatedView = await config.api.viewV2.get(view.id) + expected = { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + $and: { + conditions: [ + { + $and: { + conditions: [ + { + equal: { newField: "newValue" }, + }, + ], + }, + }, + ], + }, + } + expect(updatedView.query).toEqual(expected) + }) + + it("can delete either query and it will get regenerated from queryUI", async () => { + await config.api.viewV2.update({ + ...view, + query: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", + }, + ], + }) + + let updatedView = await config.api.viewV2.get(view.id) + expect(updatedView.queryUI).toBeDefined() + + await config.api.viewV2.update({ + ...updatedView, + query: undefined, + }) + + updatedView = await config.api.viewV2.get(view.id) + expect(updatedView.query).toBeDefined() + }) + + // This is because the conversion from queryUI -> query loses data, so you + // can't accurately reproduce the original queryUI from the query. If + // query is a LegacyFilter[] we allow it, because for Budibase v3 + // everything in the db had query set to a LegacyFilter[], and there's no + // loss of information converting from a LegacyFilter[] to a + // UISearchFilter. But we convert to a SearchFilters and that can't be + // accurately converted to a UISearchFilter. + it("can't regenerate queryUI from a query once it has been generated from a queryUI", async () => { + await config.api.viewV2.update({ + ...view, + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.EQUAL, + field: "field", + value: "value", + }, + ], + }, + ], + }, + }) + + let updatedView = await config.api.viewV2.get(view.id) + expect(updatedView.query).toBeDefined() + + await config.api.viewV2.update( + { + ...updatedView, + queryUI: undefined, + }, + { + status: 400, + body: { + message: "view is missing queryUI field", + }, + } + ) + }) + + describe("calculation views", () => { + let table: Table + let view: ViewV2 + + beforeEach(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + constraints: { + presence: true, + }, + }, + country: { + name: "country", + type: FieldType.STRING, + }, + age: { + name: "age", + type: FieldType.NUMBER, + }, + }, + }) + ) + + view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + country: { + visible: true, + }, + age: { + visible: true, + calculationType: CalculationType.SUM, + field: "age", + }, + }, + }) + + await config.api.row.bulkImport(table._id!, { + rows: [ + { + name: "Steve", + age: 30, + country: "UK", + }, + { + name: "Jane", + age: 31, + country: "UK", + }, + { + name: "Ruari", + age: 32, + country: "USA", + }, + { + name: "Alice", + age: 33, + country: "USA", + }, + ], + }) + }) + + it("returns the expected rows prior to modification", async () => { + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(2) + expect(rows).toEqual( + expect.arrayContaining([ + { + country: "USA", + age: 65, + }, + { + country: "UK", + age: 61, + }, + ]) + ) + }) + + it("can remove a group by field", async () => { + delete view.schema!.country + await config.api.viewV2.update(view) + + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(1) + expect(rows).toEqual( + expect.arrayContaining([ + { + age: 126, + }, + ]) + ) + }) + + it("can remove a calculation field", async () => { + delete view.schema!.age + await config.api.viewV2.update(view) + + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(4) + + // Because the removal of the calculation field actually makes this + // no longer a calculation view, these rows will now have _id and + // _rev fields. + expect(rows).toEqual( + expect.arrayContaining([ + expect.objectContaining({ country: "UK" }), + expect.objectContaining({ country: "UK" }), + expect.objectContaining({ country: "USA" }), + expect.objectContaining({ country: "USA" }), + ]) + ) + }) + + it("can add a new group by field", async () => { + view.schema!.name = { visible: true } + await config.api.viewV2.update(view) + + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(4) + expect(rows).toEqual( + expect.arrayContaining([ + { + name: "Steve", + age: 30, + country: "UK", + }, + { + name: "Jane", + age: 31, + country: "UK", + }, + { + name: "Ruari", + age: 32, + country: "USA", + }, + { + name: "Alice", + age: 33, + country: "USA", + }, + ]) + ) + }) + + it("can add a new group by field that is invisible, even if required on the table", async () => { + view.schema!.name = { visible: false } + await config.api.viewV2.update(view) + + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(2) + expect(rows).toEqual( + expect.arrayContaining([ + { + country: "USA", + age: 65, + }, + { + country: "UK", + age: 61, + }, + ]) + ) + }) + + it("can add a new calculation field", async () => { + view.schema!.count = { + visible: true, + calculationType: CalculationType.COUNT, + field: "age", + } + await config.api.viewV2.update(view) + + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(2) + expect(rows).toEqual( + expect.arrayContaining([ + { + country: "USA", + age: 65, + count: 2, + }, + { + country: "UK", + age: 61, + count: 2, + }, + ]) + ) + }) + }) }) - expect(await getDelegate(view)).toEqual({ - ...view, - schema: { - id: { ...table.schema["id"], visible: true }, - one: { ...table.schema["one"], visible: true }, - two: { ...table.schema["two"], visible: true }, - three: { ...table.schema["three"], visible: false }, - }, - }) - }) + describe("delete", () => { + let view: ViewV2 - it("does not include columns removed from the table", async () => { - const view = await config.api.viewV2.create({ - tableId, - name: generator.guid(), - schema: { - id: { visible: true }, - one: { visible: true }, - two: { visible: true }, - }, - }) - const table = await config.api.table.get(tableId) - const { one: _, ...newSchema } = table.schema - await config.api.table.save({ ...table, schema: newSchema }) + beforeAll(async () => { + view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + id: { visible: true }, + }, + }) + }) - expect(await getDelegate(view)).toEqual({ - ...view, - schema: { - id: { ...table.schema["id"], visible: true }, - two: { ...table.schema["two"], visible: true }, - three: { ...table.schema["three"], visible: false }, - }, - }) - }) + it("can delete an existing view", async () => { + const tableId = table._id! + const getPersistedView = async () => + (await config.api.table.get(tableId)).views![view.name] - it("does not include columns hidden from the table", async () => { - const view = await config.api.viewV2.create({ - tableId, - name: generator.guid(), - schema: { - id: { visible: true }, - one: { visible: true }, - two: { visible: true }, - }, - }) - const table = await config.api.table.get(tableId) - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - two: { ...table.schema["two"], visible: false }, - }, + expect(await getPersistedView()).toBeDefined() + + await config.api.viewV2.delete(view.id) + + expect(await getPersistedView()).toBeUndefined() + }) }) - expect(await getDelegate(view)).toEqual({ - ...view, - schema: { - id: { ...table.schema["id"], visible: true }, - one: { ...table.schema["one"], visible: true }, - three: { ...table.schema["three"], visible: false }, - }, - }) - }) - - it("should be able to fetch readonly config after downgrades", async () => { - const res = await config.api.viewV2.create({ - name: generator.name(), - tableId: table._id!, - schema: { - id: { visible: true }, - one: { visible: true, readonly: true }, - }, - }) - - mocks.licenses.useCloudFree() - const view = await getDelegate(res) - expect(view.schema?.one).toEqual( - expect.objectContaining({ visible: true, readonly: true }) - ) - }) - - it("should fill in the queryUI field if it's missing", async () => { - const res = await config.api.viewV2.create({ - name: generator.name(), - tableId: tableId, - query: [ - { - operator: BasicOperator.EQUAL, - field: "one", - value: "1", + describe.each([ + ["from view api", (view: ViewV2) => config.api.viewV2.get(view.id)], + [ + "from table", + async (view: ViewV2) => { + const table = await config.api.table.get(view.tableId) + return table.views![view.name] as ViewV2 }, ], - schema: { - id: { visible: true }, - one: { visible: true }, - }, - }) + ])("read (%s)", (_, getDelegate) => { + let table: Table + let tableId: string - const table = await config.api.table.get(tableId) - const rawView = table.views![res.name] as ViewV2 - delete rawView.queryUI + beforeEach(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + one: { + type: FieldType.STRING, + name: "one", + }, + two: { + type: FieldType.STRING, + name: "two", + }, + three: { + type: FieldType.STRING, + name: "three", + }, + }, + }) + ) + tableId = table._id! + }) - await context.doInAppContext(config.getAppId(), async () => { - const db = context.getAppDB() + it("retrieves the view data with the enriched schema", async () => { + const view = await config.api.viewV2.create({ + tableId, + name: generator.guid(), + schema: { + id: { visible: true }, + one: { visible: true }, + two: { visible: true }, + }, + }) - if (!rawDatasource) { - await db.put(table) - } else { - const ds = await config.api.datasource.get(datasource!._id!) - ds.entities![table.name] = table - const updatedDs = { - ...rawDatasource, - _id: ds._id, - _rev: ds._rev, - entities: ds.entities, - } - await db.put(updatedDs) - } - }) + expect(await getDelegate(view)).toEqual({ + ...view, + schema: { + id: { ...table.schema["id"], visible: true }, + one: { ...table.schema["one"], visible: true }, + two: { ...table.schema["two"], visible: true }, + three: { ...table.schema["three"], visible: false }, + }, + }) + }) - const view = await getDelegate(res) - const expected: UISearchFilter = { - onEmptyFilter: EmptyFilterOption.RETURN_ALL, - logicalOperator: UILogicalOperator.ALL, - groups: [ - { - logicalOperator: UILogicalOperator.ALL, - filters: [ + it("does not include columns removed from the table", async () => { + const view = await config.api.viewV2.create({ + tableId, + name: generator.guid(), + schema: { + id: { visible: true }, + one: { visible: true }, + two: { visible: true }, + }, + }) + const table = await config.api.table.get(tableId) + const { one: _, ...newSchema } = table.schema + await config.api.table.save({ ...table, schema: newSchema }) + + expect(await getDelegate(view)).toEqual({ + ...view, + schema: { + id: { ...table.schema["id"], visible: true }, + two: { ...table.schema["two"], visible: true }, + three: { ...table.schema["three"], visible: false }, + }, + }) + }) + + it("does not include columns hidden from the table", async () => { + const view = await config.api.viewV2.create({ + tableId, + name: generator.guid(), + schema: { + id: { visible: true }, + one: { visible: true }, + two: { visible: true }, + }, + }) + const table = await config.api.table.get(tableId) + await config.api.table.save({ + ...table, + schema: { + ...table.schema, + two: { ...table.schema["two"], visible: false }, + }, + }) + + expect(await getDelegate(view)).toEqual({ + ...view, + schema: { + id: { ...table.schema["id"], visible: true }, + one: { ...table.schema["one"], visible: true }, + three: { ...table.schema["three"], visible: false }, + }, + }) + }) + + it("should be able to fetch readonly config after downgrades", async () => { + const res = await config.api.viewV2.create({ + name: generator.name(), + tableId: table._id!, + schema: { + id: { visible: true }, + one: { visible: true, readonly: true }, + }, + }) + + mocks.licenses.useCloudFree() + const view = await getDelegate(res) + expect(view.schema?.one).toEqual( + expect.objectContaining({ visible: true, readonly: true }) + ) + }) + + it("should fill in the queryUI field if it's missing", async () => { + const res = await config.api.viewV2.create({ + name: generator.name(), + tableId: tableId, + query: [ { operator: BasicOperator.EQUAL, field: "one", value: "1", }, ], - }, - ], - } - expect(view.queryUI).toEqual(expected) - }) - }) + schema: { + id: { visible: true }, + one: { visible: true }, + }, + }) - describe("updating table schema", () => { - describe("existing columns changed to required", () => { + const table = await config.api.table.get(tableId) + const rawView = table.views![res.name] as ViewV2 + delete rawView.queryUI + + await context.doInAppContext(config.getAppId(), async () => { + const db = context.getAppDB() + + if (!rawDatasource) { + await db.put(table) + } else { + const ds = await config.api.datasource.get(datasource!._id!) + ds.entities![table.name] = table + const updatedDs = { + ...rawDatasource, + _id: ds._id, + _rev: ds._rev, + entities: ds.entities, + } + await db.put(updatedDs) + } + }) + + const view = await getDelegate(res) + const expected: UISearchFilter = { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + logicalOperator: UILogicalOperator.ALL, + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "one", + value: "1", + }, + ], + }, + ], + } + expect(view.queryUI).toEqual(expected) + }) + }) + + describe("updating table schema", () => { + describe("existing columns changed to required", () => { + beforeEach(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + id: { + name: "id", + type: FieldType.NUMBER, + autocolumn: true, + }, + name: { + name: "name", + type: FieldType.STRING, + }, + }, + }) + ) + }) + + it("allows updating when no views constrains the field", async () => { + await config.api.viewV2.create({ + name: "view a", + tableId: table._id!, + schema: { + id: { visible: true }, + name: { visible: true }, + }, + }) + + table = await config.api.table.get(table._id!) + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: { allowEmpty: false } }, + }, + }, + }, + { status: 200 } + ) + }) + + it("rejects if field is readonly in any view", async () => { + await config.api.viewV2.create({ + name: "view a", + tableId: table._id!, + schema: { + id: { visible: true }, + name: { + visible: true, + readonly: true, + }, + }, + }) + + table = await config.api.table.get(table._id!) + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: true }, + }, + }, + }, + { + status: 400, + body: { + status: 400, + message: + 'To make field "name" required, this field must be present and writable in views: view a.', + }, + } + ) + }) + + it("rejects if field is hidden in any view", async () => { + await config.api.viewV2.create({ + name: "view a", + tableId: table._id!, + schema: { id: { visible: true } }, + }) + + table = await config.api.table.get(table._id!) + await config.api.table.save( + { + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: true }, + }, + }, + }, + { + status: 400, + body: { + status: 400, + message: + 'To make field "name" required, this field must be present and writable in views: view a.', + }, + } + ) + }) + }) + + describe("foreign relationship columns", () => { + const createAuxTable = () => + config.api.table.save( + saveTableRequest({ + primaryDisplay: "name", + schema: { + name: { name: "name", type: FieldType.STRING }, + age: { name: "age", type: FieldType.NUMBER }, + }, + }) + ) + + const createMainTable = async ( + links: { + name: string + tableId: string + fk: string + }[] + ) => { + const table = await config.api.table.save( + saveTableRequest({ + schema: {}, + }) + ) + await config.api.table.save({ + ...table, + schema: { + ...table.schema, + ...links.reduce((acc, c) => { + acc[c.name] = { + name: c.name, + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: c.tableId, + fieldName: c.fk, + constraints: { type: "array" }, + } + return acc + }, {}), + }, + }) + return table + } + + const createView = async (tableId: string, schema: ViewV2Schema) => + await config.api.viewV2.create({ + name: generator.guid(), + tableId, + schema, + }) + + const renameColumn = async ( + table: Table, + renaming: RenameColumn + ) => { + const newSchema = { ...table.schema } + newSchema[renaming.updated] = { + ...table.schema[renaming.old], + name: renaming.updated, + } + delete newSchema[renaming.old] + + await config.api.table.save({ + ...table, + schema: newSchema, + _rename: renaming, + }) + } + + it("updating a column will update link columns configuration", async () => { + let auxTable = await createAuxTable() + + const table = await createMainTable([ + { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, + ]) + // Refetch auxTable + auxTable = await config.api.table.get(auxTable._id!) + + const view = await createView(table._id!, { + aux: { + visible: true, + columns: { + name: { visible: true, readonly: true }, + age: { visible: true, readonly: true }, + }, + }, + }) + + await renameColumn(auxTable, { old: "age", updated: "dob" }) + + const updatedView = await config.api.viewV2.get(view.id) + expect(updatedView).toEqual( + expect.objectContaining({ + schema: expect.objectContaining({ + aux: expect.objectContaining({ + columns: { + id: expect.objectContaining({ + visible: false, + readonly: false, + }), + name: expect.objectContaining({ + visible: true, + readonly: true, + }), + dob: expect.objectContaining({ + visible: true, + readonly: true, + }), + }, + }), + }), + }) + ) + }) + + it("handles multiple fields using the same table", async () => { + let auxTable = await createAuxTable() + + const table = await createMainTable([ + { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, + { name: "aux2", tableId: auxTable._id!, fk: "fk_aux2" }, + ]) + // Refetch auxTable + auxTable = await config.api.table.get(auxTable._id!) + + const view = await createView(table._id!, { + aux: { + visible: true, + columns: { + name: { visible: true, readonly: true }, + age: { visible: true, readonly: true }, + }, + }, + aux2: { + visible: true, + columns: { + name: { visible: true, readonly: true }, + age: { visible: true, readonly: true }, + }, + }, + }) + + await renameColumn(auxTable, { old: "age", updated: "dob" }) + + const updatedView = await config.api.viewV2.get(view.id) + expect(updatedView).toEqual( + expect.objectContaining({ + schema: expect.objectContaining({ + aux: expect.objectContaining({ + columns: { + id: expect.objectContaining({ + visible: false, + readonly: false, + }), + name: expect.objectContaining({ + visible: true, + readonly: true, + }), + dob: expect.objectContaining({ + visible: true, + readonly: true, + }), + }, + }), + aux2: expect.objectContaining({ + columns: { + id: expect.objectContaining({ + visible: false, + readonly: false, + }), + name: expect.objectContaining({ + visible: true, + readonly: true, + }), + dob: expect.objectContaining({ + visible: true, + readonly: true, + }), + }, + }), + }), + }) + ) + }) + + it("does not rename columns with the same name but from other tables", async () => { + let auxTable = await createAuxTable() + let aux2Table = await createAuxTable() + + const table = await createMainTable([ + { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, + { name: "aux2", tableId: aux2Table._id!, fk: "fk_aux2" }, + ]) + + // Refetch auxTable + auxTable = await config.api.table.get(auxTable._id!) + + const view = await createView(table._id!, { + aux: { + visible: true, + columns: { + name: { visible: true, readonly: true }, + }, + }, + aux2: { + visible: true, + columns: { + name: { visible: true, readonly: true }, + }, + }, + }) + + await renameColumn(auxTable, { old: "name", updated: "fullName" }) + + const updatedView = await config.api.viewV2.get(view.id) + expect(updatedView).toEqual( + expect.objectContaining({ + schema: expect.objectContaining({ + aux: expect.objectContaining({ + columns: { + id: expect.objectContaining({ + visible: false, + readonly: false, + }), + fullName: expect.objectContaining({ + visible: true, + readonly: true, + }), + age: expect.objectContaining({ + visible: false, + readonly: false, + }), + }, + }), + aux2: expect.objectContaining({ + columns: { + id: expect.objectContaining({ + visible: false, + readonly: false, + }), + name: expect.objectContaining({ + visible: true, + readonly: true, + }), + age: expect.objectContaining({ + visible: false, + readonly: false, + }), + }, + }), + }), + }) + ) + }) + + it("updates all views references", async () => { + let auxTable = await createAuxTable() + + const table1 = await createMainTable([ + { name: "aux", tableId: auxTable._id!, fk: "fk_aux_table1" }, + ]) + const table2 = await createMainTable([ + { name: "aux", tableId: auxTable._id!, fk: "fk_aux_table2" }, + ]) + + // Refetch auxTable + auxTable = await config.api.table.get(auxTable._id!) + + const viewSchema = { + aux: { + visible: true, + columns: { + name: { visible: true, readonly: true }, + age: { visible: true, readonly: true }, + }, + }, + } + const view1 = await createView(table1._id!, viewSchema) + const view2 = await createView(table1._id!, viewSchema) + const view3 = await createView(table2._id!, viewSchema) + + await renameColumn(auxTable, { old: "age", updated: "dob" }) + + for (const view of [view1, view2, view3]) { + const updatedView = await config.api.viewV2.get(view.id) + expect(updatedView).toEqual( + expect.objectContaining({ + schema: expect.objectContaining({ + aux: expect.objectContaining({ + columns: { + id: expect.objectContaining({ + visible: false, + readonly: false, + }), + name: expect.objectContaining({ + visible: true, + readonly: true, + }), + dob: expect.objectContaining({ + visible: true, + readonly: true, + }), + }, + }), + }), + }) + ) + } + }) + }) + }) + + describe("calculation views", () => { + it("should not remove calculation columns when modifying table schema", async () => { + let table = await config.api.table.save( + saveTableRequest({ + schema: { + name: { + name: "name", + type: FieldType.STRING, + }, + age: { + name: "age", + type: FieldType.NUMBER, + }, + }, + }) + ) + + let view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "age", + }, + }, + }) + + table = await config.api.table.get(table._id!) + await config.api.table.save({ + ...table, + schema: { + ...table.schema, + name: { + name: "name", + type: FieldType.STRING, + constraints: { presence: true }, + }, + }, + }) + + view = await config.api.viewV2.get(view.id) + expect(Object.keys(view.schema!).sort()).toEqual([ + "age", + "id", + "name", + "sum", + ]) + }) + + describe("bigints", () => { + let table: Table + let view: ViewV2 + + beforeEach(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + bigint: { + name: "bigint", + type: FieldType.BIGINT, + }, + }, + }) + ) + + view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + sum: { + visible: true, + calculationType: CalculationType.SUM, + field: "bigint", + }, + }, + }) + }) + + it("should not lose precision handling ints larger than JSs int53", async () => { + // The sum of the following 3 numbers cannot be represented by + // JavaScripts default int53 datatype for numbers, so this is a test + // that makes sure we aren't losing precision between the DB and the + // user. + await config.api.row.bulkImport(table._id!, { + rows: [ + { bigint: "1000000000000000000" }, + { bigint: "123" }, + { bigint: "321" }, + ], + }) + + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(1) + expect(rows[0].sum).toEqual("1000000000000000444") + }) + + it("should be able to handle up to 2**63 - 1 bigints", async () => { + await config.api.row.bulkImport(table._id!, { + rows: [{ bigint: "9223372036854775806" }, { bigint: "1" }], + }) + + const { rows } = await config.api.row.search(view.id) + expect(rows).toHaveLength(1) + expect(rows[0].sum).toEqual("9223372036854775807") + }) + }) + }) + }) + + describe("row operations", () => { + let table: Table, view: ViewV2 beforeEach(async () => { table = await config.api.table.save( saveTableRequest({ schema: { - id: { - name: "id", - type: FieldType.NUMBER, - autocolumn: true, - }, - name: { - name: "name", + one: { type: FieldType.STRING, name: "one" }, + two: { type: FieldType.STRING, name: "two" }, + default: { type: FieldType.STRING, + name: "default", + default: "default", }, }, }) ) - }) - - it("allows updating when no views constrains the field", async () => { - await config.api.viewV2.create({ - name: "view a", + view = await config.api.viewV2.create({ tableId: table._id!, + name: generator.guid(), schema: { id: { visible: true }, - name: { visible: true }, + two: { visible: true }, }, }) - - table = await config.api.table.get(table._id!) - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - name: { - name: "name", - type: FieldType.STRING, - constraints: { presence: { allowEmpty: false } }, - }, - }, - }, - { status: 200 } - ) }) - it("rejects if field is readonly in any view", async () => { - await config.api.viewV2.create({ - name: "view a", - tableId: table._id!, - schema: { - id: { visible: true }, - name: { - visible: true, - readonly: true, - }, - }, + describe("create", () => { + it("should persist a new row with only the provided view fields", async () => { + const newRow = await config.api.row.save(view.id, { + tableId: table!._id, + _viewId: view.id, + one: "foo", + two: "bar", + default: "ohnoes", + }) + + const row = await config.api.row.get(table._id!, newRow._id!) + expect(row.one).toBeUndefined() + expect(row.two).toEqual("bar") + expect(row.default).toEqual("default") }) - table = await config.api.table.get(table._id!) - await config.api.table.save( - { - ...table, + it("can't persist readonly columns", async () => { + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), schema: { - ...table.schema, - name: { - name: "name", - type: FieldType.STRING, - constraints: { presence: true }, - }, - }, - }, - { - status: 400, - body: { - status: 400, - message: - 'To make field "name" required, this field must be present and writable in views: view a.', - }, - } - ) - }) - - it("rejects if field is hidden in any view", async () => { - await config.api.viewV2.create({ - name: "view a", - tableId: table._id!, - schema: { id: { visible: true } }, - }) - - table = await config.api.table.get(table._id!) - await config.api.table.save( - { - ...table, - schema: { - ...table.schema, - name: { - name: "name", - type: FieldType.STRING, - constraints: { presence: true }, - }, - }, - }, - { - status: 400, - body: { - status: 400, - message: - 'To make field "name" required, this field must be present and writable in views: view a.', - }, - } - ) - }) - }) - - describe("foreign relationship columns", () => { - const createAuxTable = () => - config.api.table.save( - saveTableRequest({ - primaryDisplay: "name", - schema: { - name: { name: "name", type: FieldType.STRING }, - age: { name: "age", type: FieldType.NUMBER }, + id: { visible: true }, + one: { visible: true, readonly: true }, + two: { visible: true }, }, }) - ) + const row = await config.api.row.save(view.id, { + tableId: table!._id, + _viewId: view.id, + one: "foo", + two: "bar", + }) - const createMainTable = async ( - links: { - name: string - tableId: string - fk: string - }[] - ) => { - const table = await config.api.table.save( - saveTableRequest({ + expect(row.one).toBeUndefined() + expect(row.two).toEqual("bar") + }) + + it("should not return non-view view fields for a row", async () => { + const newRow = await config.api.row.save(view.id, { + one: "foo", + two: "bar", + }) + + expect(newRow.one).toBeUndefined() + expect(newRow.two).toEqual("bar") + }) + + it("should not be possible to create a row in a calculation view", async () => { + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + id: { visible: true }, + one: { visible: true }, + }, + }) + + await config.api.row.save( + view.id, + { one: "foo" }, + { + status: 400, + body: { + message: "Cannot insert rows through a calculation view", + status: 400, + }, + } + ) + }) + }) + + describe("patch", () => { + it("should not return non-view view fields for a row", async () => { + const newRow = await config.api.row.save(table._id!, { + one: "foo", + two: "bar", + }) + const row = await config.api.row.patch(view.id, { + tableId: table._id!, + _id: newRow._id!, + _rev: newRow._rev!, + one: "newFoo", + two: "newBar", + }) + + expect(row.one).toBeUndefined() + expect(row.two).toEqual("newBar") + }) + + it("should update only the view fields for a row", async () => { + const newRow = await config.api.row.save(table._id!, { + one: "foo", + two: "bar", + }) + await config.api.row.patch(view.id, { + tableId: table._id!, + _id: newRow._id!, + _rev: newRow._rev!, + one: "newFoo", + two: "newBar", + }) + + const row = await config.api.row.get(table._id!, newRow._id!) + expect(row.one).toEqual("foo") + expect(row.two).toEqual("newBar") + }) + + it("can't update readonly columns", async () => { + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + id: { visible: true }, + one: { visible: true, readonly: true }, + two: { visible: true }, + }, + }) + const newRow = await config.api.row.save(table._id!, { + one: "foo", + two: "bar", + }) + await config.api.row.patch(view.id, { + tableId: table._id!, + _id: newRow._id!, + _rev: newRow._rev!, + one: "newFoo", + two: "newBar", + }) + + const row = await config.api.row.get(table._id!, newRow._id!) + expect(row.one).toEqual("foo") + expect(row.two).toEqual("newBar") + }) + + it("should not be possible to modify a row in a calculation view", async () => { + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, + schema: { + id: { visible: true }, + one: { visible: true }, + }, + }) + + const newRow = await config.api.row.save(table._id!, { + one: "foo", + two: "bar", + }) + + await config.api.row.patch( + view.id, + { + tableId: table._id!, + _id: newRow._id!, + _rev: newRow._rev!, + one: "newFoo", + two: "newBar", + }, + { + status: 400, + body: { + message: "Cannot update rows through a calculation view", + }, + } + ) + }) + }) + + describe("fetch", () => { + let view: ViewV2, view2: ViewV2 + let table: Table, table2: Table + beforeEach(async () => { + table = await config.api.table.save(saveTableRequest()) + table2 = await config.api.table.save(saveTableRequest()) + view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), schema: {}, }) - ) - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - ...links.reduce((acc, c) => { - acc[c.name] = { - name: c.name, - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: c.tableId, - fieldName: c.fk, - constraints: { type: "array" }, - } - return acc - }, {}), - }, - }) - return table - } - - const createView = async (tableId: string, schema: ViewV2Schema) => - await config.api.viewV2.create({ - name: generator.guid(), - tableId, - schema, - }) - - const renameColumn = async (table: Table, renaming: RenameColumn) => { - const newSchema = { ...table.schema } - newSchema[renaming.updated] = { - ...table.schema[renaming.old], - name: renaming.updated, - } - delete newSchema[renaming.old] - - await config.api.table.save({ - ...table, - schema: newSchema, - _rename: renaming, - }) - } - - it("updating a column will update link columns configuration", async () => { - let auxTable = await createAuxTable() - - const table = await createMainTable([ - { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, - ]) - // Refetch auxTable - auxTable = await config.api.table.get(auxTable._id!) - - const view = await createView(table._id!, { - aux: { - visible: true, - columns: { - name: { visible: true, readonly: true }, - age: { visible: true, readonly: true }, - }, - }, - }) - - await renameColumn(auxTable, { old: "age", updated: "dob" }) - - const updatedView = await config.api.viewV2.get(view.id) - expect(updatedView).toEqual( - expect.objectContaining({ - schema: expect.objectContaining({ - aux: expect.objectContaining({ - columns: { - id: expect.objectContaining({ - visible: false, - readonly: false, - }), - name: expect.objectContaining({ - visible: true, - readonly: true, - }), - dob: expect.objectContaining({ - visible: true, - readonly: true, - }), - }, - }), - }), + view2 = await config.api.viewV2.create({ + tableId: table2._id!, + name: generator.guid(), + schema: {}, }) - ) + }) + + it("should be able to list views", async () => { + const response = await config.api.viewV2.fetch({ + status: 200, + }) + expect(response.data.find(v => v.id === view.id)).toBeDefined() + expect(response.data.find(v => v.id === view2.id)).toBeDefined() + }) }) - it("handles multiple fields using the same table", async () => { - let auxTable = await createAuxTable() - - const table = await createMainTable([ - { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, - { name: "aux2", tableId: auxTable._id!, fk: "fk_aux2" }, - ]) - // Refetch auxTable - auxTable = await config.api.table.get(auxTable._id!) - - const view = await createView(table._id!, { - aux: { - visible: true, - columns: { - name: { visible: true, readonly: true }, - age: { visible: true, readonly: true }, - }, - }, - aux2: { - visible: true, - columns: { - name: { visible: true, readonly: true }, - age: { visible: true, readonly: true }, - }, - }, - }) - - await renameColumn(auxTable, { old: "age", updated: "dob" }) - - const updatedView = await config.api.viewV2.get(view.id) - expect(updatedView).toEqual( - expect.objectContaining({ - schema: expect.objectContaining({ - aux: expect.objectContaining({ - columns: { - id: expect.objectContaining({ - visible: false, - readonly: false, - }), - name: expect.objectContaining({ - visible: true, - readonly: true, - }), - dob: expect.objectContaining({ - visible: true, - readonly: true, - }), - }, - }), - aux2: expect.objectContaining({ - columns: { - id: expect.objectContaining({ - visible: false, - readonly: false, - }), - name: expect.objectContaining({ - visible: true, - readonly: true, - }), - dob: expect.objectContaining({ - visible: true, - readonly: true, - }), - }, - }), - }), - }) - ) - }) - - it("does not rename columns with the same name but from other tables", async () => { - let auxTable = await createAuxTable() - let aux2Table = await createAuxTable() - - const table = await createMainTable([ - { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, - { name: "aux2", tableId: aux2Table._id!, fk: "fk_aux2" }, - ]) - - // Refetch auxTable - auxTable = await config.api.table.get(auxTable._id!) - - const view = await createView(table._id!, { - aux: { - visible: true, - columns: { - name: { visible: true, readonly: true }, - }, - }, - aux2: { - visible: true, - columns: { - name: { visible: true, readonly: true }, - }, - }, - }) - - await renameColumn(auxTable, { old: "name", updated: "fullName" }) - - const updatedView = await config.api.viewV2.get(view.id) - expect(updatedView).toEqual( - expect.objectContaining({ - schema: expect.objectContaining({ - aux: expect.objectContaining({ - columns: { - id: expect.objectContaining({ - visible: false, - readonly: false, - }), - fullName: expect.objectContaining({ - visible: true, - readonly: true, - }), - age: expect.objectContaining({ - visible: false, - readonly: false, - }), - }, - }), - aux2: expect.objectContaining({ - columns: { - id: expect.objectContaining({ - visible: false, - readonly: false, - }), - name: expect.objectContaining({ - visible: true, - readonly: true, - }), - age: expect.objectContaining({ - visible: false, - readonly: false, - }), - }, - }), - }), - }) - ) - }) - - it("updates all views references", async () => { - let auxTable = await createAuxTable() - - const table1 = await createMainTable([ - { name: "aux", tableId: auxTable._id!, fk: "fk_aux_table1" }, - ]) - const table2 = await createMainTable([ - { name: "aux", tableId: auxTable._id!, fk: "fk_aux_table2" }, - ]) - - // Refetch auxTable - auxTable = await config.api.table.get(auxTable._id!) - - const viewSchema = { - aux: { - visible: true, - columns: { - name: { visible: true, readonly: true }, - age: { visible: true, readonly: true }, - }, - }, - } - const view1 = await createView(table1._id!, viewSchema) - const view2 = await createView(table1._id!, viewSchema) - const view3 = await createView(table2._id!, viewSchema) - - await renameColumn(auxTable, { old: "age", updated: "dob" }) - - for (const view of [view1, view2, view3]) { - const updatedView = await config.api.viewV2.get(view.id) - expect(updatedView).toEqual( - expect.objectContaining({ - schema: expect.objectContaining({ - aux: expect.objectContaining({ - columns: { - id: expect.objectContaining({ - visible: false, - readonly: false, - }), - name: expect.objectContaining({ - visible: true, - readonly: true, - }), - dob: expect.objectContaining({ - visible: true, - readonly: true, - }), - }, - }), - }), - }) + describe("destroy", () => { + const getRowUsage = async () => { + const { total } = await config.doInContext(undefined, () => + quotas.getCurrentUsageValues( + QuotaUsageType.STATIC, + StaticQuotaName.ROWS + ) ) + return total } - }) - }) - }) - !isLucene && - describe("calculation views", () => { - it("should not remove calculation columns when modifying table schema", async () => { - let table = await config.api.table.save( - saveTableRequest({ + const assertRowUsage = async (expected: number) => { + const usage = await getRowUsage() + expect(usage).toBe(expected) + } + + it("should be able to delete a row", async () => { + const createdRow = await config.api.row.save(table._id!, {}) + const rowUsage = await getRowUsage() + await config.api.row.bulkDelete(view.id, { rows: [createdRow] }) + await assertRowUsage(isInternal ? rowUsage - 1 : rowUsage) + await config.api.row.get(table._id!, createdRow._id!, { + status: 404, + }) + }) + + it("should be able to delete multiple rows", async () => { + const rows = await Promise.all([ + config.api.row.save(table._id!, {}), + config.api.row.save(table._id!, {}), + config.api.row.save(table._id!, {}), + ]) + const rowUsage = await getRowUsage() + + await config.api.row.bulkDelete(view.id, { + rows: [rows[0], rows[2]], + }) + + await assertRowUsage(isInternal ? rowUsage - 2 : rowUsage) + + await config.api.row.get(table._id!, rows[0]._id!, { + status: 404, + }) + await config.api.row.get(table._id!, rows[2]._id!, { + status: 404, + }) + await config.api.row.get(table._id!, rows[1]._id!, { status: 200 }) + }) + + it("should not be possible to delete a row in a calculation view", async () => { + const row = await config.api.row.save(table._id!, {}) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + type: ViewV2Type.CALCULATION, schema: { - name: { - name: "name", - type: FieldType.STRING, - }, - age: { - name: "age", - type: FieldType.NUMBER, - }, + id: { visible: true }, + one: { visible: true }, }, }) - ) - let view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - sum: { - visible: true, - calculationType: CalculationType.SUM, - field: "age", - }, - }, + await config.api.row.delete( + view.id, + { _id: row._id! }, + { + status: 400, + body: { + message: "Cannot delete rows through a calculation view", + status: 400, + }, + } + ) }) - - table = await config.api.table.get(table._id!) - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - name: { - name: "name", - type: FieldType.STRING, - constraints: { presence: true }, - }, - }, - }) - - view = await config.api.viewV2.get(view.id) - expect(Object.keys(view.schema!).sort()).toEqual([ - "age", - "id", - "name", - "sum", - ]) }) - describe("bigints", () => { - let table: Table + describe("read", () => { let view: ViewV2 + let table: Table - beforeEach(async () => { + beforeAll(async () => { table = await config.api.table.save( saveTableRequest({ schema: { - bigint: { - name: "bigint", - type: FieldType.BIGINT, + Country: { + type: FieldType.STRING, + name: "Country", + }, + Story: { + type: FieldType.STRING, + name: "Story", }, }, }) @@ -2365,853 +2718,513 @@ describe.each([ view = await config.api.viewV2.create({ tableId: table._id!, name: generator.guid(), - type: ViewV2Type.CALCULATION, schema: { - sum: { + id: { visible: true }, + Country: { visible: true, - calculationType: CalculationType.SUM, - field: "bigint", }, }, }) }) - it("should not lose precision handling ints larger than JSs int53", async () => { - // The sum of the following 3 numbers cannot be represented by - // JavaScripts default int53 datatype for numbers, so this is a test - // that makes sure we aren't losing precision between the DB and the - // user. - await config.api.row.bulkImport(table._id!, { - rows: [ - { bigint: "1000000000000000000" }, - { bigint: "123" }, - { bigint: "321" }, - ], + it("views have extra data trimmed", async () => { + let row = await config.api.row.save(view.id, { + Country: "Aussy", + Story: "aaaaa", }) - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(1) - expect(rows[0].sum).toEqual("1000000000000000444") - }) - - it("should be able to handle up to 2**63 - 1 bigints", async () => { - await config.api.row.bulkImport(table._id!, { - rows: [{ bigint: "9223372036854775806" }, { bigint: "1" }], - }) - - const { rows } = await config.api.row.search(view.id) - expect(rows).toHaveLength(1) - expect(rows[0].sum).toEqual("9223372036854775807") + row = await config.api.row.get(table._id!, row._id!) + expect(row.Story).toBeUndefined() + expect(row.Country).toEqual("Aussy") }) }) - }) - }) - describe("row operations", () => { - let table: Table, view: ViewV2 - beforeEach(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - one: { type: FieldType.STRING, name: "one" }, - two: { type: FieldType.STRING, name: "two" }, - default: { - type: FieldType.STRING, - name: "default", - default: "default", - }, - }, - }) - ) - view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - two: { visible: true }, - }, - }) - }) - - describe("create", () => { - it("should persist a new row with only the provided view fields", async () => { - const newRow = await config.api.row.save(view.id, { - tableId: table!._id, - _viewId: view.id, - one: "foo", - two: "bar", - default: "ohnoes", - }) - - const row = await config.api.row.get(table._id!, newRow._id!) - expect(row.one).toBeUndefined() - expect(row.two).toEqual("bar") - expect(row.default).toEqual("default") - }) - - it("can't persist readonly columns", async () => { - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - one: { visible: true, readonly: true }, - two: { visible: true }, - }, - }) - const row = await config.api.row.save(view.id, { - tableId: table!._id, - _viewId: view.id, - one: "foo", - two: "bar", - }) - - expect(row.one).toBeUndefined() - expect(row.two).toEqual("bar") - }) - - it("should not return non-view view fields for a row", async () => { - const newRow = await config.api.row.save(view.id, { - one: "foo", - two: "bar", - }) - - expect(newRow.one).toBeUndefined() - expect(newRow.two).toEqual("bar") - }) - - it("should not be possible to create a row in a calculation view", async () => { - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - id: { visible: true }, - one: { visible: true }, - }, - }) - - await config.api.row.save( - view.id, - { one: "foo" }, - { - status: 400, - body: { - message: "Cannot insert rows through a calculation view", - status: 400, - }, - } - ) - }) - }) - - describe("patch", () => { - it("should not return non-view view fields for a row", async () => { - const newRow = await config.api.row.save(table._id!, { - one: "foo", - two: "bar", - }) - const row = await config.api.row.patch(view.id, { - tableId: table._id!, - _id: newRow._id!, - _rev: newRow._rev!, - one: "newFoo", - two: "newBar", - }) - - expect(row.one).toBeUndefined() - expect(row.two).toEqual("newBar") - }) - - it("should update only the view fields for a row", async () => { - const newRow = await config.api.row.save(table._id!, { - one: "foo", - two: "bar", - }) - await config.api.row.patch(view.id, { - tableId: table._id!, - _id: newRow._id!, - _rev: newRow._rev!, - one: "newFoo", - two: "newBar", - }) - - const row = await config.api.row.get(table._id!, newRow._id!) - expect(row.one).toEqual("foo") - expect(row.two).toEqual("newBar") - }) - - it("can't update readonly columns", async () => { - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - one: { visible: true, readonly: true }, - two: { visible: true }, - }, - }) - const newRow = await config.api.row.save(table._id!, { - one: "foo", - two: "bar", - }) - await config.api.row.patch(view.id, { - tableId: table._id!, - _id: newRow._id!, - _rev: newRow._rev!, - one: "newFoo", - two: "newBar", - }) - - const row = await config.api.row.get(table._id!, newRow._id!) - expect(row.one).toEqual("foo") - expect(row.two).toEqual("newBar") - }) - - it("should not be possible to modify a row in a calculation view", async () => { - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - id: { visible: true }, - one: { visible: true }, - }, - }) - - const newRow = await config.api.row.save(table._id!, { - one: "foo", - two: "bar", - }) - - await config.api.row.patch( - view.id, - { - tableId: table._id!, - _id: newRow._id!, - _rev: newRow._rev!, - one: "newFoo", - two: "newBar", - }, - { - status: 400, - body: { - message: "Cannot update rows through a calculation view", - }, - } - ) - }) - }) - - describe("destroy", () => { - const getRowUsage = async () => { - const { total } = await config.doInContext(undefined, () => - quotas.getCurrentUsageValues( - QuotaUsageType.STATIC, - StaticQuotaName.ROWS - ) - ) - return total - } - - const assertRowUsage = async (expected: number) => { - const usage = await getRowUsage() - expect(usage).toBe(expected) - } - - it("should be able to delete a row", async () => { - const createdRow = await config.api.row.save(table._id!, {}) - const rowUsage = await getRowUsage() - await config.api.row.bulkDelete(view.id, { rows: [createdRow] }) - await assertRowUsage(isInternal ? rowUsage - 1 : rowUsage) - await config.api.row.get(table._id!, createdRow._id!, { - status: 404, - }) - }) - - it("should be able to delete multiple rows", async () => { - const rows = await Promise.all([ - config.api.row.save(table._id!, {}), - config.api.row.save(table._id!, {}), - config.api.row.save(table._id!, {}), - ]) - const rowUsage = await getRowUsage() - - await config.api.row.bulkDelete(view.id, { rows: [rows[0], rows[2]] }) - - await assertRowUsage(isInternal ? rowUsage - 2 : rowUsage) - - await config.api.row.get(table._id!, rows[0]._id!, { - status: 404, - }) - await config.api.row.get(table._id!, rows[2]._id!, { - status: 404, - }) - await config.api.row.get(table._id!, rows[1]._id!, { status: 200 }) - }) - - it("should not be possible to delete a row in a calculation view", async () => { - const row = await config.api.row.save(table._id!, {}) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - type: ViewV2Type.CALCULATION, - schema: { - id: { visible: true }, - one: { visible: true }, - }, - }) - - await config.api.row.delete( - view.id, - { _id: row._id! }, - { - status: 400, - body: { - message: "Cannot delete rows through a calculation view", - status: 400, - }, - } - ) - }) - }) - - describe("read", () => { - let view: ViewV2 - let table: Table - - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - schema: { - Country: { - type: FieldType.STRING, - name: "Country", - }, - Story: { - type: FieldType.STRING, - name: "Story", - }, - }, - }) - ) - - view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - id: { visible: true }, - Country: { - visible: true, - }, - }, - }) - }) - - it("views have extra data trimmed", async () => { - let row = await config.api.row.save(view.id, { - Country: "Aussy", - Story: "aaaaa", - }) - - row = await config.api.row.get(table._id!, row._id!) - expect(row.Story).toBeUndefined() - expect(row.Country).toEqual("Aussy") - }) - }) - - !isLucene && - describe("search", () => { - it("returns empty rows from view when no schema is passed", async () => { - const rows = await Promise.all( - Array.from({ length: 10 }, () => - config.api.row.save(table._id!, {}) + describe("search", () => { + it("returns empty rows from view when no schema is passed", async () => { + const rows = await Promise.all( + Array.from({ length: 10 }, () => + config.api.row.save(table._id!, {}) + ) ) - ) - const response = await config.api.viewV2.search(view.id) - expect(response.rows).toHaveLength(10) - expect(response).toEqual({ - rows: expect.arrayContaining( - rows.map(r => ({ - _viewId: view.id, - tableId: table._id, - id: r.id, - _id: r._id, - _rev: r._rev, - ...(isInternal - ? { - type: "row", - updatedAt: expect.any(String), - createdAt: expect.any(String), - } - : {}), - })) - ), - ...(isInternal - ? {} - : { - hasNextPage: false, - }), - }) - }) - - it("searching respects the view filters", async () => { - await config.api.row.save(table._id!, { - one: "foo", - two: "bar", - }) - const two = await config.api.row.save(table._id!, { - one: "foo2", - two: "bar2", - }) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.EQUAL, - field: "two", - value: "bar2", - }, - ], - }, - ], - }, - schema: { - id: { visible: true }, - one: { visible: false }, - two: { visible: true }, - }, - }) - - const response = await config.api.viewV2.search(view.id) - expect(response.rows).toHaveLength(1) - expect(response).toEqual({ - rows: expect.arrayContaining([ - { - _viewId: view.id, - tableId: table._id, - id: two.id, - two: two.two, - _id: two._id, - _rev: two._rev, - ...(isInternal - ? { - type: "row", - createdAt: expect.any(String), - updatedAt: expect.any(String), - } - : {}), - }, - ]), - ...(isInternal - ? {} - : { - hasNextPage: false, - }), - }) - }) - - it("views filters are respected even if the column is hidden", async () => { - await config.api.row.save(table._id!, { - one: "foo", - two: "bar", - }) - const two = await config.api.row.save(table._id!, { - one: "foo2", - two: "bar2", - }) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.EQUAL, - field: "two", - value: "bar2", - }, - ], - }, - ], - }, - schema: { - id: { visible: true }, - one: { visible: false }, - two: { visible: false }, - }, - }) - - const response = await config.api.viewV2.search(view.id) - expect(response.rows).toHaveLength(1) - expect(response.rows).toEqual([ - expect.objectContaining({ _id: two._id }), - ]) - }) - - it("views without data can be returned", async () => { - const response = await config.api.viewV2.search(view.id) - expect(response.rows).toHaveLength(0) - }) - - it("respects the limit parameter", async () => { - await Promise.all( - Array.from({ length: 10 }, () => - config.api.row.save(table._id!, {}) - ) - ) - const limit = generator.integer({ min: 1, max: 8 }) - const response = await config.api.viewV2.search(view.id, { - limit, - query: {}, - }) - expect(response.rows).toHaveLength(limit) - }) - - it("can handle pagination", async () => { - await Promise.all( - Array.from({ length: 10 }, () => - config.api.row.save(table._id!, {}) - ) - ) - const rows = (await config.api.viewV2.search(view.id)).rows - - const page1 = await config.api.viewV2.search(view.id, { - paginate: true, - limit: 4, - query: {}, - countRows: true, - }) - expect(page1).toEqual({ - rows: expect.arrayContaining(rows.slice(0, 4)), - hasNextPage: true, - bookmark: expect.anything(), - totalRows: 10, - }) - - const page2 = await config.api.viewV2.search(view.id, { - paginate: true, - limit: 4, - bookmark: page1.bookmark, - query: {}, - countRows: true, - }) - expect(page2).toEqual({ - rows: expect.arrayContaining(rows.slice(4, 8)), - hasNextPage: true, - bookmark: expect.anything(), - totalRows: 10, - }) - - const page3 = await config.api.viewV2.search(view.id, { - paginate: true, - limit: 4, - bookmark: page2.bookmark, - query: {}, - countRows: true, - }) - const expectation: SearchResponse = { - rows: expect.arrayContaining(rows.slice(8)), - hasNextPage: false, - totalRows: 10, - } - if (isLucene) { - expectation.bookmark = expect.anything() - } - expect(page3).toEqual(expectation) - }) - - const sortTestOptions: [ - { - field: string - order?: SortOrder - type?: SortType - }, - string[] - ][] = [ - [ - { - field: "name", - order: SortOrder.ASCENDING, - type: SortType.STRING, - }, - ["Alice", "Bob", "Charly", "Danny"], - ], - [ - { - field: "name", - }, - ["Alice", "Bob", "Charly", "Danny"], - ], - [ - { - field: "name", - order: SortOrder.DESCENDING, - }, - ["Danny", "Charly", "Bob", "Alice"], - ], - [ - { - field: "name", - order: SortOrder.DESCENDING, - type: SortType.STRING, - }, - ["Danny", "Charly", "Bob", "Alice"], - ], - [ - { - field: "age", - order: SortOrder.ASCENDING, - type: SortType.NUMBER, - }, - ["Danny", "Alice", "Charly", "Bob"], - ], - [ - { - field: "age", - order: SortOrder.ASCENDING, - }, - ["Danny", "Alice", "Charly", "Bob"], - ], - [ - { - field: "age", - order: SortOrder.DESCENDING, - }, - ["Bob", "Charly", "Alice", "Danny"], - ], - [ - { - field: "age", - order: SortOrder.DESCENDING, - type: SortType.NUMBER, - }, - ["Bob", "Charly", "Alice", "Danny"], - ], - ] - - describe("sorting", () => { - let table: Table - const viewSchema = { - id: { visible: true }, - age: { visible: true }, - name: { visible: true }, - } - - beforeAll(async () => { - table = await config.api.table.save( - saveTableRequest({ - type: "table", - schema: { - name: { - type: FieldType.STRING, - name: "name", - }, - surname: { - type: FieldType.STRING, - name: "surname", - }, - age: { - type: FieldType.NUMBER, - name: "age", - }, - address: { - type: FieldType.STRING, - name: "address", - }, - jobTitle: { - type: FieldType.STRING, - name: "jobTitle", - }, - }, - }) - ) - - const users = [ - { name: "Alice", age: 25 }, - { name: "Bob", age: 30 }, - { name: "Charly", age: 27 }, - { name: "Danny", age: 15 }, - ] - await Promise.all( - users.map(u => - config.api.row.save(table._id!, { + const response = await config.api.viewV2.search(view.id) + expect(response.rows).toHaveLength(10) + expect(response).toEqual({ + rows: expect.arrayContaining( + rows.map(r => ({ + _viewId: view.id, tableId: table._id, - ...u, + id: r.id, + _id: r._id, + _rev: r._rev, + ...(isInternal + ? { + type: "row", + updatedAt: expect.any(String), + createdAt: expect.any(String), + } + : {}), + })) + ), + ...(isInternal + ? {} + : { + hasNextPage: false, + }), + }) + }) + + it("searching respects the view filters", async () => { + await config.api.row.save(table._id!, { + one: "foo", + two: "bar", + }) + const two = await config.api.row.save(table._id!, { + one: "foo2", + two: "bar2", + }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.EQUAL, + field: "two", + value: "bar2", + }, + ], + }, + ], + }, + schema: { + id: { visible: true }, + one: { visible: false }, + two: { visible: true }, + }, + }) + + const response = await config.api.viewV2.search(view.id) + expect(response.rows).toHaveLength(1) + expect(response).toEqual({ + rows: expect.arrayContaining([ + { + _viewId: view.id, + tableId: table._id, + id: two.id, + two: two.two, + _id: two._id, + _rev: two._rev, + ...(isInternal + ? { + type: "row", + createdAt: expect.any(String), + updatedAt: expect.any(String), + } + : {}), + }, + ]), + ...(isInternal + ? {} + : { + hasNextPage: false, + }), + }) + }) + + it("views filters are respected even if the column is hidden", async () => { + await config.api.row.save(table._id!, { + one: "foo", + two: "bar", + }) + const two = await config.api.row.save(table._id!, { + one: "foo2", + two: "bar2", + }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.EQUAL, + field: "two", + value: "bar2", + }, + ], + }, + ], + }, + schema: { + id: { visible: true }, + one: { visible: false }, + two: { visible: false }, + }, + }) + + const response = await config.api.viewV2.search(view.id) + expect(response.rows).toHaveLength(1) + expect(response.rows).toEqual([ + expect.objectContaining({ _id: two._id }), + ]) + }) + + it("views without data can be returned", async () => { + const response = await config.api.viewV2.search(view.id) + expect(response.rows).toHaveLength(0) + }) + + it("respects the limit parameter", async () => { + await Promise.all( + Array.from({ length: 10 }, () => + config.api.row.save(table._id!, {}) + ) + ) + const limit = generator.integer({ min: 1, max: 8 }) + const response = await config.api.viewV2.search(view.id, { + limit, + query: {}, + }) + expect(response.rows).toHaveLength(limit) + }) + + it("can handle pagination", async () => { + await Promise.all( + Array.from({ length: 10 }, () => + config.api.row.save(table._id!, {}) + ) + ) + const rows = (await config.api.viewV2.search(view.id)).rows + + const page1 = await config.api.viewV2.search(view.id, { + paginate: true, + limit: 4, + query: {}, + countRows: true, + }) + expect(page1).toEqual({ + rows: expect.arrayContaining(rows.slice(0, 4)), + hasNextPage: true, + bookmark: expect.anything(), + totalRows: 10, + }) + + const page2 = await config.api.viewV2.search(view.id, { + paginate: true, + limit: 4, + bookmark: page1.bookmark, + query: {}, + countRows: true, + }) + expect(page2).toEqual({ + rows: expect.arrayContaining(rows.slice(4, 8)), + hasNextPage: true, + bookmark: expect.anything(), + totalRows: 10, + }) + + const page3 = await config.api.viewV2.search(view.id, { + paginate: true, + limit: 4, + bookmark: page2.bookmark, + query: {}, + countRows: true, + }) + const expectation: SearchResponse = { + rows: expect.arrayContaining(rows.slice(8)), + hasNextPage: false, + totalRows: 10, + } + expect(page3).toEqual(expectation) + }) + + const sortTestOptions: [ + { + field: string + order?: SortOrder + type?: SortType + }, + string[] + ][] = [ + [ + { + field: "name", + order: SortOrder.ASCENDING, + type: SortType.STRING, + }, + ["Alice", "Bob", "Charly", "Danny"], + ], + [ + { + field: "name", + }, + ["Alice", "Bob", "Charly", "Danny"], + ], + [ + { + field: "name", + order: SortOrder.DESCENDING, + }, + ["Danny", "Charly", "Bob", "Alice"], + ], + [ + { + field: "name", + order: SortOrder.DESCENDING, + type: SortType.STRING, + }, + ["Danny", "Charly", "Bob", "Alice"], + ], + [ + { + field: "age", + order: SortOrder.ASCENDING, + type: SortType.NUMBER, + }, + ["Danny", "Alice", "Charly", "Bob"], + ], + [ + { + field: "age", + order: SortOrder.ASCENDING, + }, + ["Danny", "Alice", "Charly", "Bob"], + ], + [ + { + field: "age", + order: SortOrder.DESCENDING, + }, + ["Bob", "Charly", "Alice", "Danny"], + ], + [ + { + field: "age", + order: SortOrder.DESCENDING, + type: SortType.NUMBER, + }, + ["Bob", "Charly", "Alice", "Danny"], + ], + ] + + describe("sorting", () => { + let table: Table + const viewSchema = { + id: { visible: true }, + age: { visible: true }, + name: { visible: true }, + } + + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + type: "table", + schema: { + name: { + type: FieldType.STRING, + name: "name", + }, + surname: { + type: FieldType.STRING, + name: "surname", + }, + age: { + type: FieldType.NUMBER, + name: "age", + }, + address: { + type: FieldType.STRING, + name: "address", + }, + jobTitle: { + type: FieldType.STRING, + name: "jobTitle", + }, + }, }) ) + + const users = [ + { name: "Alice", age: 25 }, + { name: "Bob", age: 30 }, + { name: "Charly", age: 27 }, + { name: "Danny", age: 15 }, + ] + await Promise.all( + users.map(u => + config.api.row.save(table._id!, { + tableId: table._id, + ...u, + }) + ) + ) + }) + + it.each(sortTestOptions)( + "allow sorting (%s)", + async (sortParams, expected) => { + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + sort: sortParams, + schema: viewSchema, + }) + + const response = await config.api.viewV2.search(view.id) + + expect(response.rows).toHaveLength(4) + expect(response.rows).toEqual( + expected.map(name => expect.objectContaining({ name })) + ) + } + ) + + it.each(sortTestOptions)( + "allow override the default view sorting (%s)", + async (sortParams, expected) => { + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + sort: { + field: "name", + order: SortOrder.ASCENDING, + type: SortType.STRING, + }, + schema: viewSchema, + }) + + const response = await config.api.viewV2.search(view.id, { + sort: sortParams.field, + sortOrder: sortParams.order, + sortType: sortParams.type, + query: {}, + }) + + expect(response.rows).toHaveLength(4) + expect(response.rows).toEqual( + expected.map(name => expect.objectContaining({ name })) + ) + } ) }) - it.each(sortTestOptions)( - "allow sorting (%s)", - async (sortParams, expected) => { - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - sort: sortParams, - schema: viewSchema, - }) + it("can query on top of the view filters", async () => { + await config.api.row.save(table._id!, { + one: "foo", + two: "bar", + }) + await config.api.row.save(table._id!, { + one: "foo2", + two: "bar2", + }) + const three = await config.api.row.save(table._id!, { + one: "foo3", + two: "bar3", + }) - const response = await config.api.viewV2.search(view.id) + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.NOT_EQUAL, + field: "one", + value: "foo2", + }, + ], + }, + ], + }, + schema: { + id: { visible: true }, + one: { visible: true }, + two: { visible: true }, + }, + }) - expect(response.rows).toHaveLength(4) - expect(response.rows).toEqual( - expected.map(name => expect.objectContaining({ name })) - ) - } - ) - - it.each(sortTestOptions)( - "allow override the default view sorting (%s)", - async (sortParams, expected) => { - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - sort: { - field: "name", - order: SortOrder.ASCENDING, - type: SortType.STRING, + const response = await config.api.viewV2.search(view.id, { + query: { + [BasicOperator.EQUAL]: { + two: "bar3", }, - schema: viewSchema, - }) - - const response = await config.api.viewV2.search(view.id, { - sort: sortParams.field, - sortOrder: sortParams.order, - sortType: sortParams.type, - query: {}, - }) - - expect(response.rows).toHaveLength(4) - expect(response.rows).toEqual( - expected.map(name => expect.objectContaining({ name })) - ) - } - ) - }) - - it("can query on top of the view filters", async () => { - await config.api.row.save(table._id!, { - one: "foo", - two: "bar", - }) - await config.api.row.save(table._id!, { - one: "foo2", - two: "bar2", - }) - const three = await config.api.row.save(table._id!, { - one: "foo3", - two: "bar3", - }) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.NOT_EQUAL, - field: "one", - value: "foo2", - }, - ], + [BasicOperator.NOT_EMPTY]: { + two: null, }, - ], - }, - schema: { - id: { visible: true }, - one: { visible: true }, - two: { visible: true }, - }, - }) - - const response = await config.api.viewV2.search(view.id, { - query: { - [BasicOperator.EQUAL]: { - two: "bar3", }, - [BasicOperator.NOT_EMPTY]: { - two: null, + }) + expect(response.rows).toHaveLength(1) + expect(response.rows).toEqual( + expect.arrayContaining([ + expect.objectContaining({ _id: three._id }), + ]) + ) + }) + + it("can query on top of the view filters (using or filters)", async () => { + const one = await config.api.row.save(table._id!, { + one: "foo", + two: "bar", + }) + await config.api.row.save(table._id!, { + one: "foo2", + two: "bar2", + }) + const three = await config.api.row.save(table._id!, { + one: "foo3", + two: "bar3", + }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.NOT_EQUAL, + field: "one", + value: "foo2", + }, + ], + }, + ], }, - }, - }) - expect(response.rows).toHaveLength(1) - expect(response.rows).toEqual( - expect.arrayContaining([ - expect.objectContaining({ _id: three._id }), - ]) - ) - }) + schema: { + id: { visible: true }, + one: { visible: false }, + two: { visible: true }, + }, + }) - it("can query on top of the view filters (using or filters)", async () => { - const one = await config.api.row.save(table._id!, { - one: "foo", - two: "bar", - }) - await config.api.row.save(table._id!, { - one: "foo2", - two: "bar2", - }) - const three = await config.api.row.save(table._id!, { - one: "foo3", - two: "bar3", - }) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.NOT_EQUAL, - field: "one", - value: "foo2", - }, - ], + const response = await config.api.viewV2.search(view.id, { + query: { + allOr: true, + [BasicOperator.NOT_EQUAL]: { + two: "bar", }, - ], - }, - schema: { - id: { visible: true }, - one: { visible: false }, - two: { visible: true }, - }, + [BasicOperator.NOT_EMPTY]: { + two: null, + }, + }, + }) + expect(response.rows).toHaveLength(2) + expect(response.rows).toEqual( + expect.arrayContaining([ + expect.objectContaining({ _id: one._id }), + expect.objectContaining({ _id: three._id }), + ]) + ) }) - const response = await config.api.viewV2.search(view.id, { - query: { - allOr: true, - [BasicOperator.NOT_EQUAL]: { - two: "bar", - }, - [BasicOperator.NOT_EMPTY]: { - two: null, - }, - }, - }) - expect(response.rows).toHaveLength(2) - expect(response.rows).toEqual( - expect.arrayContaining([ - expect.objectContaining({ _id: one._id }), - expect.objectContaining({ _id: three._id }), - ]) - ) - }) - - !isLucene && it.each([true, false])( "can filter a view without a view filter", async allOr => { @@ -3249,7 +3262,6 @@ describe.each([ } ) - !isLucene && it.each([true, false])("cannot bypass a view filter", async allOr => { await config.api.row.save(table._id!, { one: "foo", @@ -3294,168 +3306,156 @@ describe.each([ expect(response.rows).toHaveLength(0) }) - describe("foreign relationship columns", () => { - let envCleanup: () => void - beforeAll(() => { - envCleanup = features.testutils.setFeatureFlags("*", { - ENRICHED_RELATIONSHIPS: true, - }) - }) - - afterAll(() => { - envCleanup?.() - }) - - const createMainTable = async ( - links: { - name: string - tableId: string - fk: string - }[] - ) => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { title: { name: "title", type: FieldType.STRING } }, - }) - ) - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - ...links.reduce((acc, c) => { - acc[c.name] = { - name: c.name, - relationshipType: RelationshipType.ONE_TO_MANY, - type: FieldType.LINK, - tableId: c.tableId, - fieldName: c.fk, - constraints: { type: "array" }, - } - return acc - }, {}), - }, - }) - return table - } - const createAuxTable = (schema: TableSchema) => - config.api.table.save( - saveTableRequest({ - primaryDisplay: "name", + describe("foreign relationship columns", () => { + const createMainTable = async ( + links: { + name: string + tableId: string + fk: string + }[] + ) => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { title: { name: "title", type: FieldType.STRING } }, + }) + ) + await config.api.table.save({ + ...table, schema: { - ...schema, - name: { name: "name", type: FieldType.STRING }, + ...table.schema, + ...links.reduce((acc, c) => { + acc[c.name] = { + name: c.name, + relationshipType: RelationshipType.ONE_TO_MANY, + type: FieldType.LINK, + tableId: c.tableId, + fieldName: c.fk, + constraints: { type: "array" }, + } + return acc + }, {}), }, }) - ) - - it("returns squashed fields respecting the view config", async () => { - const auxTable = await createAuxTable({ - age: { name: "age", type: FieldType.NUMBER }, - }) - const auxRow = await config.api.row.save(auxTable._id!, { - name: generator.name(), - age: generator.age(), - }) - - const table = await createMainTable([ - { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, - ]) - await config.api.row.save(table._id!, { - title: generator.word(), - aux: [auxRow], - }) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - title: { visible: true }, - aux: { - visible: true, - columns: { - name: { visible: false, readonly: false }, - age: { visible: true, readonly: true }, + return table + } + const createAuxTable = (schema: TableSchema) => + config.api.table.save( + saveTableRequest({ + primaryDisplay: "name", + schema: { + ...schema, + name: { name: "name", type: FieldType.STRING }, }, - }, - }, - }) + }) + ) - const response = await config.api.viewV2.search(view.id) - expect(response.rows).toEqual([ - expect.objectContaining({ - aux: [ - { - _id: auxRow._id, - primaryDisplay: auxRow.name, - age: auxRow.age, - }, - ], - }), - ]) - }) + it("returns squashed fields respecting the view config", async () => { + const auxTable = await createAuxTable({ + age: { name: "age", type: FieldType.NUMBER }, + }) + const auxRow = await config.api.row.save(auxTable._id!, { + name: generator.name(), + age: generator.age(), + }) - it("enriches squashed fields", async () => { - const auxTable = await createAuxTable({ - user: { - name: "user", - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - constraints: { presence: true }, - }, - }) - const table = await createMainTable([ - { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, - ]) + const table = await createMainTable([ + { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, + ]) + await config.api.row.save(table._id!, { + title: generator.word(), + aux: [auxRow], + }) - const user = config.getUser() - const auxRow = await config.api.row.save(auxTable._id!, { - name: generator.name(), - user: user._id, - }) - await config.api.row.save(table._id!, { - title: generator.word(), - aux: [auxRow], - }) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - schema: { - title: { visible: true }, - aux: { - visible: true, - columns: { - name: { visible: true, readonly: true }, - user: { visible: true, readonly: true }, - }, - }, - }, - }) - - const response = await config.api.viewV2.search(view.id) - - expect(response.rows).toEqual([ - expect.objectContaining({ - aux: [ - { - _id: auxRow._id, - primaryDisplay: auxRow.name, - name: auxRow.name, - user: { - _id: user._id, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, - primaryDisplay: user.email, + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + title: { visible: true }, + aux: { + visible: true, + columns: { + name: { visible: false, readonly: false }, + age: { visible: true, readonly: true }, }, }, - ], - }), - ]) - }) - }) + }, + }) + + const response = await config.api.viewV2.search(view.id) + expect(response.rows).toEqual([ + expect.objectContaining({ + aux: [ + { + _id: auxRow._id, + primaryDisplay: auxRow.name, + age: auxRow.age, + }, + ], + }), + ]) + }) + + it("enriches squashed fields", async () => { + const auxTable = await createAuxTable({ + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + constraints: { presence: true }, + }, + }) + const table = await createMainTable([ + { name: "aux", tableId: auxTable._id!, fk: "fk_aux" }, + ]) + + const user = config.getUser() + const auxRow = await config.api.row.save(auxTable._id!, { + name: generator.name(), + user: user._id, + }) + await config.api.row.save(table._id!, { + title: generator.word(), + aux: [auxRow], + }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: { + title: { visible: true }, + aux: { + visible: true, + columns: { + name: { visible: true, readonly: true }, + user: { visible: true, readonly: true }, + }, + }, + }, + }) + + const response = await config.api.viewV2.search(view.id) + + expect(response.rows).toEqual([ + expect.objectContaining({ + aux: [ + { + _id: auxRow._id, + primaryDisplay: auxRow.name, + name: auxRow.name, + user: { + _id: user._id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + primaryDisplay: user.email, + }, + }, + ], + }), + ]) + }) + }) - !isLucene && describe("calculations", () => { let table: Table let rows: Row[] @@ -4075,7 +4075,6 @@ describe.each([ }) }) - !isLucene && it("should not need required fields to be present", async () => { const table = await config.api.table.save( saveTableRequest({ @@ -4121,558 +4120,564 @@ describe.each([ expect(response.rows[0].sum).toEqual(61) }) - it("should be able to filter on a single user field in both the view query and search query", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - user: { - name: "user", - type: FieldType.BB_REFERENCE_SINGLE, - subtype: BBReferenceFieldSubType.USER, - }, - }, - }) - ) - - await config.api.row.save(table._id!, { - user: config.getUser()._id, - }) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - queryUI: { - groups: [ - { - filters: [ - { - operator: BasicOperator.EQUAL, - field: "user", - value: "{{ [user].[_id] }}", - }, - ], - }, - ], - }, - schema: { - user: { - visible: true, - }, - }, - }) - - const { rows } = await config.api.viewV2.search(view.id, { - query: { - equal: { - user: "{{ [user].[_id] }}", - }, - }, - }) - - expect(rows).toHaveLength(1) - expect(rows[0].user._id).toEqual(config.getUser()._id) - }) - - describe("search operators", () => { - let table: Table - beforeEach(async () => { - table = await config.api.table.save( + it("should be able to filter on a single user field in both the view query and search query", async () => { + const table = await config.api.table.save( saveTableRequest({ schema: { - string: { name: "string", type: FieldType.STRING }, - longform: { name: "longform", type: FieldType.LONGFORM }, - options: { - name: "options", - type: FieldType.OPTIONS, - constraints: { inclusion: ["a", "b", "c"] }, - }, - array: { - name: "array", - type: FieldType.ARRAY, - constraints: { - type: JsonFieldSubType.ARRAY, - inclusion: ["a", "b", "c"], - }, - }, - number: { name: "number", type: FieldType.NUMBER }, - bigint: { name: "bigint", type: FieldType.BIGINT }, - datetime: { name: "datetime", type: FieldType.DATETIME }, - boolean: { name: "boolean", type: FieldType.BOOLEAN }, user: { name: "user", type: FieldType.BB_REFERENCE_SINGLE, subtype: BBReferenceFieldSubType.USER, }, - users: { - name: "users", - type: FieldType.BB_REFERENCE, - subtype: BBReferenceFieldSubType.USER, - constraints: { - type: JsonFieldSubType.ARRAY, - }, - }, }, }) ) + + await config.api.row.save(table._id!, { + user: config.getUser()._id, + }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + queryUI: { + groups: [ + { + filters: [ + { + operator: BasicOperator.EQUAL, + field: "user", + value: "{{ [user].[_id] }}", + }, + ], + }, + ], + }, + schema: { + user: { + visible: true, + }, + }, + }) + + const { rows } = await config.api.viewV2.search(view.id, { + query: { + equal: { + user: "{{ [user].[_id] }}", + }, + }, + }) + + expect(rows).toHaveLength(1) + expect(rows[0].user._id).toEqual(config.getUser()._id) }) - interface TestCase { - name: string - query: UISearchFilter | (() => UISearchFilter) - insert: Row[] | (() => Row[]) - expected: Row[] | (() => Row[]) - searchOpts?: Partial - } + describe("search operators", () => { + let table: Table + beforeEach(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + string: { name: "string", type: FieldType.STRING }, + longform: { name: "longform", type: FieldType.LONGFORM }, + options: { + name: "options", + type: FieldType.OPTIONS, + constraints: { inclusion: ["a", "b", "c"] }, + }, + array: { + name: "array", + type: FieldType.ARRAY, + constraints: { + type: JsonFieldSubType.ARRAY, + inclusion: ["a", "b", "c"], + }, + }, + number: { name: "number", type: FieldType.NUMBER }, + bigint: { name: "bigint", type: FieldType.BIGINT }, + datetime: { name: "datetime", type: FieldType.DATETIME }, + boolean: { name: "boolean", type: FieldType.BOOLEAN }, + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + }, + users: { + name: "users", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + constraints: { + type: JsonFieldSubType.ARRAY, + }, + }, + }, + }) + ) + }) - function simpleQuery(...filters: LegacyFilter[]): UISearchFilter { - return { groups: [{ filters }] } - } + interface TestCase { + name: string + query: UISearchFilter | (() => UISearchFilter) + insert: Row[] | (() => Row[]) + expected: Row[] | (() => Row[]) + searchOpts?: Partial + } - const testCases: TestCase[] = [ - { - name: "empty query return all", - insert: [{ string: "foo" }], - query: { - onEmptyFilter: EmptyFilterOption.RETURN_ALL, + function simpleQuery(...filters: LegacyFilter[]): UISearchFilter { + return { groups: [{ filters }] } + } + + const testCases: TestCase[] = [ + { + name: "empty query return all", + insert: [{ string: "foo" }], + query: { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + }, + expected: [{ string: "foo" }], }, - expected: [{ string: "foo" }], - }, - { - name: "empty query return none", - insert: [{ string: "foo" }], - query: { - onEmptyFilter: EmptyFilterOption.RETURN_NONE, + { + name: "empty query return none", + insert: [{ string: "foo" }], + query: { + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + }, + expected: [], }, - expected: [], - }, - { - name: "simple string search", - insert: [{ string: "foo" }], - query: simpleQuery({ - operator: BasicOperator.EQUAL, - field: "string", - value: "foo", - }), - expected: [{ string: "foo" }], - }, - { - name: "non matching string search", - insert: [{ string: "foo" }], - query: simpleQuery({ - operator: BasicOperator.EQUAL, - field: "string", - value: "bar", - }), - expected: [], - }, - { - name: "allOr", - insert: [{ string: "bar" }, { string: "foo" }], - query: simpleQuery( - { + { + name: "simple string search", + insert: [{ string: "foo" }], + query: simpleQuery({ operator: BasicOperator.EQUAL, field: "string", value: "foo", - }, - { + }), + expected: [{ string: "foo" }], + }, + { + name: "non matching string search", + insert: [{ string: "foo" }], + query: simpleQuery({ operator: BasicOperator.EQUAL, field: "string", value: "bar", + }), + expected: [], + }, + { + name: "allOr", + insert: [{ string: "bar" }, { string: "foo" }], + query: simpleQuery( + { + operator: BasicOperator.EQUAL, + field: "string", + value: "foo", + }, + { + operator: BasicOperator.EQUAL, + field: "string", + value: "bar", + }, + { + operator: "allOr", + } + ), + searchOpts: { + sort: "string", + sortOrder: SortOrder.ASCENDING, }, - { - operator: "allOr", - } - ), - searchOpts: { - sort: "string", - sortOrder: SortOrder.ASCENDING, + expected: [{ string: "bar" }, { string: "foo" }], }, - expected: [{ string: "bar" }, { string: "foo" }], - }, - { - name: "can find rows with fuzzy search", - insert: [{ string: "foo" }, { string: "bar" }], - query: simpleQuery({ - operator: BasicOperator.FUZZY, - field: "string", - value: "fo", - }), - expected: [{ string: "foo" }], - }, - { - name: "can find nothing with fuzzy search", - insert: [{ string: "foo" }, { string: "bar" }], - query: simpleQuery({ - operator: BasicOperator.FUZZY, - field: "string", - value: "baz", - }), - expected: [], - }, - { - name: "can find numeric rows", - insert: [{ number: 1 }, { number: 2 }], - query: simpleQuery({ - operator: BasicOperator.EQUAL, - field: "number", - value: 1, - }), - expected: [{ number: 1 }], - }, - { - name: "can find numeric values with rangeHigh", - insert: [{ number: 1 }, { number: 2 }, { number: 3 }], - query: simpleQuery({ - operator: "rangeHigh", - field: "number", - value: 2, - }), - searchOpts: { - sort: "number", - sortOrder: SortOrder.ASCENDING, + { + name: "can find rows with fuzzy search", + insert: [{ string: "foo" }, { string: "bar" }], + query: simpleQuery({ + operator: BasicOperator.FUZZY, + field: "string", + value: "fo", + }), + expected: [{ string: "foo" }], }, - expected: [{ number: 1 }, { number: 2 }], - }, - { - name: "can find numeric values with rangeLow", - insert: [{ number: 1 }, { number: 2 }, { number: 3 }], - query: simpleQuery({ - operator: "rangeLow", - field: "number", - value: 2, - }), - searchOpts: { - sort: "number", - sortOrder: SortOrder.ASCENDING, + { + name: "can find nothing with fuzzy search", + insert: [{ string: "foo" }, { string: "bar" }], + query: simpleQuery({ + operator: BasicOperator.FUZZY, + field: "string", + value: "baz", + }), + expected: [], }, - expected: [{ number: 2 }, { number: 3 }], - }, - { - name: "can find numeric values with full range", - insert: [{ number: 1 }, { number: 2 }, { number: 3 }], - query: simpleQuery( - { + { + name: "can find numeric rows", + insert: [{ number: 1 }, { number: 2 }], + query: simpleQuery({ + operator: BasicOperator.EQUAL, + field: "number", + value: 1, + }), + expected: [{ number: 1 }], + }, + { + name: "can find numeric values with rangeHigh", + insert: [{ number: 1 }, { number: 2 }, { number: 3 }], + query: simpleQuery({ operator: "rangeHigh", field: "number", value: 2, + }), + searchOpts: { + sort: "number", + sortOrder: SortOrder.ASCENDING, }, - { + expected: [{ number: 1 }, { number: 2 }], + }, + { + name: "can find numeric values with rangeLow", + insert: [{ number: 1 }, { number: 2 }, { number: 3 }], + query: simpleQuery({ operator: "rangeLow", field: "number", value: 2, - } - ), - expected: [{ number: 2 }], - }, - { - name: "can find longform values", - insert: [{ longform: "foo" }, { longform: "bar" }], - query: simpleQuery({ - operator: BasicOperator.EQUAL, - field: "longform", - value: "foo", - }), - expected: [{ longform: "foo" }], - }, - { - name: "can find options values", - insert: [{ options: "a" }, { options: "b" }], - query: simpleQuery({ - operator: BasicOperator.EQUAL, - field: "options", - value: "a", - }), - expected: [{ options: "a" }], - }, - { - name: "can find array values", - insert: [ - // Number field here is just to guarantee order. - { number: 1, array: ["a"] }, - { number: 2, array: ["b"] }, - { number: 3, array: ["a", "c"] }, - ], - query: simpleQuery({ - operator: ArrayOperator.CONTAINS, - field: "array", - value: "a", - }), - searchOpts: { - sort: "number", - sortOrder: SortOrder.ASCENDING, - }, - expected: [{ array: ["a"] }, { array: ["a", "c"] }], - }, - { - name: "can find bigint values", - insert: [{ bigint: "1" }, { bigint: "2" }], - query: simpleQuery({ - operator: BasicOperator.EQUAL, - field: "bigint", - type: FieldType.BIGINT, - value: "1", - }), - expected: [{ bigint: "1" }], - }, - { - name: "can find datetime values", - insert: [ - { datetime: "2021-01-01T00:00:00.000Z" }, - { datetime: "2021-01-02T00:00:00.000Z" }, - ], - query: simpleQuery({ - operator: BasicOperator.EQUAL, - field: "datetime", - type: FieldType.DATETIME, - value: "2021-01-01", - }), - expected: [{ datetime: "2021-01-01T00:00:00.000Z" }], - }, - { - name: "can find boolean values", - insert: [{ boolean: true }, { boolean: false }], - query: simpleQuery({ - operator: BasicOperator.EQUAL, - field: "boolean", - value: true, - }), - expected: [{ boolean: true }], - }, - { - name: "can find user values", - insert: () => [{ user: config.getUser() }], - query: () => - simpleQuery({ - operator: BasicOperator.EQUAL, - field: "user", - value: config.getUser()._id, }), - expected: () => [ - { - user: expect.objectContaining({ _id: config.getUser()._id }), + searchOpts: { + sort: "number", + sortOrder: SortOrder.ASCENDING, }, - ], - }, - { - name: "can find users values", - insert: () => [{ users: [config.getUser()] }], - query: () => - simpleQuery({ - operator: ArrayOperator.CONTAINS, - field: "users", - value: [config.getUser()._id], + expected: [{ number: 2 }, { number: 3 }], + }, + { + name: "can find numeric values with full range", + insert: [{ number: 1 }, { number: 2 }, { number: 3 }], + query: simpleQuery( + { + operator: "rangeHigh", + field: "number", + value: 2, + }, + { + operator: "rangeLow", + field: "number", + value: 2, + } + ), + expected: [{ number: 2 }], + }, + { + name: "can find longform values", + insert: [{ longform: "foo" }, { longform: "bar" }], + query: simpleQuery({ + operator: BasicOperator.EQUAL, + field: "longform", + value: "foo", }), - expected: () => [ - { - users: [ - expect.objectContaining({ _id: config.getUser()._id }), + expected: [{ longform: "foo" }], + }, + { + name: "can find options values", + insert: [{ options: "a" }, { options: "b" }], + query: simpleQuery({ + operator: BasicOperator.EQUAL, + field: "options", + value: "a", + }), + expected: [{ options: "a" }], + }, + { + name: "can find array values", + insert: [ + // Number field here is just to guarantee order. + { number: 1, array: ["a"] }, + { number: 2, array: ["b"] }, + { number: 3, array: ["a", "c"] }, + ], + query: simpleQuery({ + operator: ArrayOperator.CONTAINS, + field: "array", + value: "a", + }), + searchOpts: { + sort: "number", + sortOrder: SortOrder.ASCENDING, + }, + expected: [{ array: ["a"] }, { array: ["a", "c"] }], + }, + { + name: "can find bigint values", + insert: [{ bigint: "1" }, { bigint: "2" }], + query: simpleQuery({ + operator: BasicOperator.EQUAL, + field: "bigint", + type: FieldType.BIGINT, + value: "1", + }), + expected: [{ bigint: "1" }], + }, + { + name: "can find datetime values", + insert: [ + { datetime: "2021-01-01T00:00:00.000Z" }, + { datetime: "2021-01-02T00:00:00.000Z" }, + ], + query: simpleQuery({ + operator: BasicOperator.EQUAL, + field: "datetime", + type: FieldType.DATETIME, + value: "2021-01-01", + }), + expected: [{ datetime: "2021-01-01T00:00:00.000Z" }], + }, + { + name: "can find boolean values", + insert: [{ boolean: true }, { boolean: false }], + query: simpleQuery({ + operator: BasicOperator.EQUAL, + field: "boolean", + value: true, + }), + expected: [{ boolean: true }], + }, + { + name: "can find user values", + insert: () => [{ user: config.getUser() }], + query: () => + simpleQuery({ + operator: BasicOperator.EQUAL, + field: "user", + value: config.getUser()._id, + }), + expected: () => [ + { + user: expect.objectContaining({ + _id: config.getUser()._id, + }), + }, + ], + }, + { + name: "can find users values", + insert: () => [{ users: [config.getUser()] }], + query: () => + simpleQuery({ + operator: ArrayOperator.CONTAINS, + field: "users", + value: [config.getUser()._id], + }), + expected: () => [ + { + users: [ + expect.objectContaining({ _id: config.getUser()._id }), + ], + }, + ], + }, + { + name: "can handle logical operator any", + insert: [{ string: "bar" }, { string: "foo" }], + query: { + groups: [ + { + logicalOperator: UILogicalOperator.ANY, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "string", + value: "foo", + }, + { + operator: BasicOperator.EQUAL, + field: "string", + value: "bar", + }, + ], + }, ], }, - ], - }, - { - name: "can handle logical operator any", - insert: [{ string: "bar" }, { string: "foo" }], - query: { - groups: [ - { - logicalOperator: UILogicalOperator.ANY, - filters: [ - { - operator: BasicOperator.EQUAL, - field: "string", - value: "foo", - }, - { - operator: BasicOperator.EQUAL, - field: "string", - value: "bar", - }, - ], - }, - ], - }, - searchOpts: { - sort: "string", - sortOrder: SortOrder.ASCENDING, - }, - expected: [{ string: "bar" }, { string: "foo" }], - }, - { - name: "can handle logical operator all", - insert: [ - { string: "bar", number: 1 }, - { string: "foo", number: 2 }, - ], - query: { - groups: [ - { - logicalOperator: UILogicalOperator.ALL, - filters: [ - { - operator: BasicOperator.EQUAL, - field: "string", - value: "foo", - }, - { - operator: BasicOperator.EQUAL, - field: "number", - value: 2, - }, - ], - }, - ], - }, - searchOpts: { - sort: "string", - sortOrder: SortOrder.ASCENDING, - }, - expected: [{ string: "foo", number: 2 }], - }, - { - name: "overrides allOr with logical operators", - insert: [ - { string: "bar", number: 1 }, - { string: "foo", number: 1 }, - ], - query: { - groups: [ - { - logicalOperator: UILogicalOperator.ALL, - filters: [ - { operator: "allOr" }, - { - operator: BasicOperator.EQUAL, - field: "string", - value: "foo", - }, - { - operator: BasicOperator.EQUAL, - field: "number", - value: 1, - }, - ], - }, - ], - }, - searchOpts: { - sort: "string", - sortOrder: SortOrder.ASCENDING, - }, - expected: [{ string: "foo", number: 1 }], - }, - ] - - it.each(testCases)( - "$name", - async ({ query, insert, expected, searchOpts }) => { - // Some values can't be specified outside of a test (e.g. getting - // config.getUser(), it won't be initialised), so we use functions - // in those cases. - if (typeof insert === "function") { - insert = insert() - } - if (typeof expected === "function") { - expected = expected() - } - if (typeof query === "function") { - query = query() - } - - await config.api.row.bulkImport(table._id!, { rows: insert }) - - const view = await config.api.viewV2.create({ - tableId: table._id!, - name: generator.guid(), - queryUI: query, - schema: { - string: { visible: true }, - longform: { visible: true }, - options: { visible: true }, - array: { visible: true }, - number: { visible: true }, - bigint: { visible: true }, - datetime: { visible: true }, - boolean: { visible: true }, - user: { visible: true }, - users: { visible: true }, + searchOpts: { + sort: "string", + sortOrder: SortOrder.ASCENDING, }, - }) + expected: [{ string: "bar" }, { string: "foo" }], + }, + { + name: "can handle logical operator all", + insert: [ + { string: "bar", number: 1 }, + { string: "foo", number: 2 }, + ], + query: { + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { + operator: BasicOperator.EQUAL, + field: "string", + value: "foo", + }, + { + operator: BasicOperator.EQUAL, + field: "number", + value: 2, + }, + ], + }, + ], + }, + searchOpts: { + sort: "string", + sortOrder: SortOrder.ASCENDING, + }, + expected: [{ string: "foo", number: 2 }], + }, + { + name: "overrides allOr with logical operators", + insert: [ + { string: "bar", number: 1 }, + { string: "foo", number: 1 }, + ], + query: { + groups: [ + { + logicalOperator: UILogicalOperator.ALL, + filters: [ + { operator: "allOr" }, + { + operator: BasicOperator.EQUAL, + field: "string", + value: "foo", + }, + { + operator: BasicOperator.EQUAL, + field: "number", + value: 1, + }, + ], + }, + ], + }, + searchOpts: { + sort: "string", + sortOrder: SortOrder.ASCENDING, + }, + expected: [{ string: "foo", number: 1 }], + }, + ] - const { rows } = await config.api.viewV2.search(view.id, { - query: {}, - ...searchOpts, - }) - expect(rows).toEqual( - expected.map(r => expect.objectContaining(r)) + it.each(testCases)( + "$name", + async ({ query, insert, expected, searchOpts }) => { + // Some values can't be specified outside of a test (e.g. getting + // config.getUser(), it won't be initialised), so we use functions + // in those cases. + if (typeof insert === "function") { + insert = insert() + } + if (typeof expected === "function") { + expected = expected() + } + if (typeof query === "function") { + query = query() + } + + await config.api.row.bulkImport(table._id!, { rows: insert }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + queryUI: query, + schema: { + string: { visible: true }, + longform: { visible: true }, + options: { visible: true }, + array: { visible: true }, + number: { visible: true }, + bigint: { visible: true }, + datetime: { visible: true }, + boolean: { visible: true }, + user: { visible: true }, + users: { visible: true }, + }, + }) + + const { rows } = await config.api.viewV2.search(view.id, { + query: {}, + ...searchOpts, + }) + expect(rows).toEqual( + expected.map(r => expect.objectContaining(r)) + ) + } + ) + }) + }) + + describe("permissions", () => { + beforeEach(async () => { + await Promise.all( + Array.from({ length: 10 }, () => + config.api.row.save(table._id!, {}) ) - } - ) + ) + }) + + it("does not allow public users to fetch by default", async () => { + await config.publish() + await config.api.viewV2.publicSearch(view.id, undefined, { + status: 401, + }) + }) + + it("allow public users to fetch when permissions are explicit", async () => { + await config.api.permission.add({ + roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, + level: PermissionLevel.READ, + resourceId: view.id, + }) + await config.publish() + + const response = await config.api.viewV2.publicSearch(view.id) + + expect(response.rows).toHaveLength(10) + }) + + it("allow public users to fetch when permissions are inherited", async () => { + await config.api.permission.add({ + roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, + level: PermissionLevel.READ, + resourceId: table._id!, + }) + await config.api.permission.revoke({ + roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, // Don't think this matters since we are revoking the permission + level: PermissionLevel.READ, + resourceId: view.id, + }) + await config.publish() + + const response = await config.api.viewV2.publicSearch(view.id) + + expect(response.rows).toHaveLength(10) + }) + + it("respects inherited permissions, not allowing not public views from public tables", async () => { + await config.api.permission.add({ + roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, + level: PermissionLevel.READ, + resourceId: table._id!, + }) + await config.api.permission.add({ + roleId: roles.BUILTIN_ROLE_IDS.POWER, + level: PermissionLevel.READ, + resourceId: view.id, + }) + await config.publish() + + await config.api.viewV2.publicSearch(view.id, undefined, { + status: 401, + }) + }) }) }) - - describe("permissions", () => { - beforeEach(async () => { - await Promise.all( - Array.from({ length: 10 }, () => config.api.row.save(table._id!, {})) - ) - }) - - it("does not allow public users to fetch by default", async () => { - await config.publish() - await config.api.viewV2.publicSearch(view.id, undefined, { - status: 401, - }) - }) - - it("allow public users to fetch when permissions are explicit", async () => { - await config.api.permission.add({ - roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, - level: PermissionLevel.READ, - resourceId: view.id, - }) - await config.publish() - - const response = await config.api.viewV2.publicSearch(view.id) - - expect(response.rows).toHaveLength(10) - }) - - it("allow public users to fetch when permissions are inherited", async () => { - await config.api.permission.add({ - roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, - level: PermissionLevel.READ, - resourceId: table._id!, - }) - await config.api.permission.revoke({ - roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, // Don't think this matters since we are revoking the permission - level: PermissionLevel.READ, - resourceId: view.id, - }) - await config.publish() - - const response = await config.api.viewV2.publicSearch(view.id) - - expect(response.rows).toHaveLength(10) - }) - - it("respects inherited permissions, not allowing not public views from public tables", async () => { - await config.api.permission.add({ - roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, - level: PermissionLevel.READ, - resourceId: table._id!, - }) - await config.api.permission.add({ - roleId: roles.BUILTIN_ROLE_IDS.POWER, - level: PermissionLevel.READ, - resourceId: view.id, - }) - await config.publish() - - await config.api.viewV2.publicSearch(view.id, undefined, { - status: 401, - }) - }) - }) - }) -}) + } + ) +} diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index 0343cc186c..3bee4f88ce 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -9,6 +9,13 @@ import { Table, WebhookActionType, BuiltinPermissionID, + ViewV2Type, + SortOrder, + SortType, + UILogicalOperator, + BasicOperator, + ArrayOperator, + RangeOperator, } from "@budibase/types" import Joi, { CustomValidator } from "joi" import { ValidSnippetNameRegex, helpers } from "@budibase/shared-core" @@ -66,6 +73,66 @@ export function tableValidator() { ) } +function searchUIFilterValidator() { + const logicalOperator = Joi.string().valid( + ...Object.values(UILogicalOperator) + ) + const operators = [ + ...Object.values(BasicOperator), + ...Object.values(ArrayOperator), + ...Object.values(RangeOperator), + ] + const filters = Joi.array().items( + Joi.object({ + operator: Joi.string() + .valid(...operators) + .required(), + field: Joi.string().required(), + // could do with better validation of value based on operator + value: Joi.any().required(), + }) + ) + return Joi.object({ + logicalOperator, + onEmptyFilter: Joi.string().valid(...Object.values(EmptyFilterOption)), + groups: Joi.array().items( + Joi.object({ + logicalOperator, + filters, + groups: Joi.array().items( + Joi.object({ + filters, + logicalOperator, + }) + ), + }) + ), + }) +} + +export function viewValidator() { + return auth.joiValidator.body( + Joi.object({ + id: OPTIONAL_STRING, + tableId: Joi.string().required(), + name: Joi.string().required(), + type: Joi.string().optional().valid(null, ViewV2Type.CALCULATION), + primaryDisplay: OPTIONAL_STRING, + schema: Joi.object().required(), + query: searchUIFilterValidator().optional(), + sort: Joi.object({ + field: Joi.string().required(), + order: Joi.string() + .optional() + .valid(...Object.values(SortOrder)), + type: Joi.string() + .optional() + .valid(...Object.values(SortType)), + }).optional(), + }) + ) +} + export function nameValidator() { return auth.joiValidator.body( Joi.object({ @@ -91,8 +158,7 @@ export function datasourceValidator() { ) } -function filterObject(opts?: { unknown: boolean }) { - const { unknown = true } = opts || {} +function searchFiltersValidator() { const conditionalFilteringObject = () => Joi.object({ conditions: Joi.array().items(Joi.link("#schema")).required(), @@ -119,7 +185,14 @@ function filterObject(opts?: { unknown: boolean }) { fuzzyOr: Joi.forbidden(), documentType: Joi.forbidden(), } - return Joi.object(filtersValidators).unknown(unknown).id("schema") + + return Joi.object(filtersValidators) +} + +function filterObject(opts?: { unknown: boolean }) { + const { unknown = true } = opts || {} + + return searchFiltersValidator().unknown(unknown).id("schema") } export function internalSearchValidator() { diff --git a/packages/server/src/api/routes/view.ts b/packages/server/src/api/routes/view.ts index 807d8e2f28..92e5f02a18 100644 --- a/packages/server/src/api/routes/view.ts +++ b/packages/server/src/api/routes/view.ts @@ -8,6 +8,11 @@ import { permissions } from "@budibase/backend-core" const router: Router = new Router() router + .get( + "/api/v2/views", + authorized(permissions.BUILDER), + viewController.v2.fetch + ) .get( "/api/v2/views/:viewId", authorizedResource( diff --git a/packages/server/src/appMigrations/migrations/tests/20240604153647_initial_sqs.spec.ts b/packages/server/src/appMigrations/migrations/tests/20240604153647_initial_sqs.spec.ts index fe44b7b901..1ce519b0b0 100644 --- a/packages/server/src/appMigrations/migrations/tests/20240604153647_initial_sqs.spec.ts +++ b/packages/server/src/appMigrations/migrations/tests/20240604153647_initial_sqs.spec.ts @@ -1,10 +1,6 @@ import * as setup from "../../../api/routes/tests/utilities" import { basicTable } from "../../../tests/utilities/structures" -import { - db as dbCore, - features, - SQLITE_DESIGN_DOC_ID, -} from "@budibase/backend-core" +import { db as dbCore, SQLITE_DESIGN_DOC_ID } from "@budibase/backend-core" import { LinkDocument, DocumentType, @@ -70,24 +66,14 @@ function oldLinkDocument(): Omit { } } -async function sqsDisabled(cb: () => Promise) { - await features.testutils.withFeatureFlags("*", { SQS: false }, cb) -} - -async function sqsEnabled(cb: () => Promise) { - await features.testutils.withFeatureFlags("*", { SQS: true }, cb) -} - describe("SQS migration", () => { beforeAll(async () => { - await sqsDisabled(async () => { - await config.init() - const table = await config.api.table.save(basicTable()) - tableId = table._id! - const db = dbCore.getDB(config.appId!) - // old link document - await db.put(oldLinkDocument()) - }) + await config.init() + const table = await config.api.table.save(basicTable()) + tableId = table._id! + const db = dbCore.getDB(config.appId!) + // old link document + await db.put(oldLinkDocument()) }) beforeEach(async () => { @@ -101,43 +87,32 @@ describe("SQS migration", () => { it("test migration runs as expected against an older DB", async () => { const db = dbCore.getDB(config.appId!) - // confirm nothing exists initially - await sqsDisabled(async () => { - let error: any | undefined - try { - await db.get(SQLITE_DESIGN_DOC_ID) - } catch (err: any) { - error = err - } - expect(error).toBeDefined() - expect(error.status).toBe(404) + + // remove sqlite design doc to simulate it comes from an older installation + const doc = await db.get(SQLITE_DESIGN_DOC_ID) + await db.remove({ _id: doc._id, _rev: doc._rev }) + + await processMigrations(config.appId!, MIGRATIONS) + const designDoc = await db.get(SQLITE_DESIGN_DOC_ID) + expect(designDoc.sql.tables).toBeDefined() + const mainTableDef = designDoc.sql.tables[tableId] + expect(mainTableDef).toBeDefined() + expect(mainTableDef.fields[prefix("name")]).toEqual({ + field: "name", + type: SQLiteType.TEXT, + }) + expect(mainTableDef.fields[prefix("description")]).toEqual({ + field: "description", + type: SQLiteType.TEXT, }) - await sqsEnabled(async () => { - await processMigrations(config.appId!, MIGRATIONS) - const designDoc = await db.get(SQLITE_DESIGN_DOC_ID) - expect(designDoc.sql.tables).toBeDefined() - const mainTableDef = designDoc.sql.tables[tableId] - expect(mainTableDef).toBeDefined() - expect(mainTableDef.fields[prefix("name")]).toEqual({ - field: "name", - type: SQLiteType.TEXT, - }) - expect(mainTableDef.fields[prefix("description")]).toEqual({ - field: "description", - type: SQLiteType.TEXT, - }) - - const { tableId1, tableId2, rowId1, rowId2 } = oldLinkDocInfo() - const linkDoc = await db.get(oldLinkDocID()) - expect(linkDoc.tableId).toEqual( - generateJunctionTableID(tableId1, tableId2) - ) - // should have swapped the documents - expect(linkDoc.doc1.tableId).toEqual(tableId2) - expect(linkDoc.doc1.rowId).toEqual(rowId2) - expect(linkDoc.doc2.tableId).toEqual(tableId1) - expect(linkDoc.doc2.rowId).toEqual(rowId1) - }) + const { tableId1, tableId2, rowId1, rowId2 } = oldLinkDocInfo() + const linkDoc = await db.get(oldLinkDocID()) + expect(linkDoc.tableId).toEqual(generateJunctionTableID(tableId1, tableId2)) + // should have swapped the documents + expect(linkDoc.doc1.tableId).toEqual(tableId2) + expect(linkDoc.doc1.rowId).toEqual(rowId2) + expect(linkDoc.doc2.tableId).toEqual(tableId1) + expect(linkDoc.doc2.rowId).toEqual(rowId1) }) }) diff --git a/packages/server/src/automations/tests/bash.spec.js b/packages/server/src/automations/tests/bash.spec.ts similarity index 66% rename from packages/server/src/automations/tests/bash.spec.js rename to packages/server/src/automations/tests/bash.spec.ts index 61b96a5e35..472d1092d6 100644 --- a/packages/server/src/automations/tests/bash.spec.js +++ b/packages/server/src/automations/tests/bash.spec.ts @@ -1,15 +1,15 @@ -const setup = require("./utilities") +import { getConfig, afterAll as _afterAll, runStep } from "./utilities" describe("test the bash action", () => { - let config = setup.getConfig() + let config = getConfig() beforeAll(async () => { await config.init() }) - afterAll(setup.afterAll) + afterAll(_afterAll) it("should be able to execute a script", async () => { - let res = await setup.runStep("EXECUTE_BASH", { + let res = await runStep(config, "EXECUTE_BASH", { code: "echo 'test'", }) expect(res.stdout).toEqual("test\n") @@ -17,7 +17,7 @@ describe("test the bash action", () => { }) it("should handle a null value", async () => { - let res = await setup.runStep("EXECUTE_BASH", { + let res = await runStep(config, "EXECUTE_BASH", { code: null, }) expect(res.stdout).toEqual( diff --git a/packages/server/src/automations/tests/createRow.spec.ts b/packages/server/src/automations/tests/createRow.spec.ts index 62e9e24f9e..bcf9845669 100644 --- a/packages/server/src/automations/tests/createRow.spec.ts +++ b/packages/server/src/automations/tests/createRow.spec.ts @@ -31,7 +31,7 @@ describe("test the create row action", () => { afterAll(setup.afterAll) it("should be able to run the action", async () => { - const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, { + const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { row, }) expect(res.id).toBeDefined() @@ -43,7 +43,7 @@ describe("test the create row action", () => { }) it("should return an error (not throw) when bad info provided", async () => { - const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, { + const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { row: { tableId: "invalid", invalid: "invalid", @@ -53,7 +53,7 @@ describe("test the create row action", () => { }) it("should check invalid inputs return an error", async () => { - const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, {}) + const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, {}) expect(res.success).toEqual(false) }) @@ -76,7 +76,7 @@ describe("test the create row action", () => { ] attachmentRow.file_attachment = attachmentObject - const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, { + const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { row: attachmentRow, }) @@ -111,7 +111,7 @@ describe("test the create row action", () => { } attachmentRow.single_file_attachment = attachmentObject - const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, { + const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { row: attachmentRow, }) @@ -146,7 +146,7 @@ describe("test the create row action", () => { } attachmentRow.single_file_attachment = attachmentObject - const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, { + const res = await setup.runStep(config, setup.actions.CREATE_ROW.stepId, { row: attachmentRow, }) diff --git a/packages/server/src/automations/tests/delay.spec.js b/packages/server/src/automations/tests/delay.spec.ts similarity index 59% rename from packages/server/src/automations/tests/delay.spec.js rename to packages/server/src/automations/tests/delay.spec.ts index 5f6e0fd164..7ed5fe7482 100644 --- a/packages/server/src/automations/tests/delay.spec.js +++ b/packages/server/src/automations/tests/delay.spec.ts @@ -1,14 +1,20 @@ -const setup = require("./utilities") +import { runStep, actions, getConfig } from "./utilities" +import { reset } from "timekeeper" // need real Date for this test -const tk = require("timekeeper") -tk.reset() +reset() describe("test the delay logic", () => { + const config = getConfig() + + beforeAll(async () => { + await config.init() + }) + it("should be able to run the delay", async () => { const time = 100 const before = Date.now() - await setup.runStep(setup.actions.DELAY.stepId, { time: time }) + await runStep(config, actions.DELAY.stepId, { time: time }) const now = Date.now() // divide by two just so that test will always pass as long as there was some sort of delay expect(now - before).toBeGreaterThanOrEqual(time / 2) diff --git a/packages/server/src/automations/tests/deleteRow.spec.ts b/packages/server/src/automations/tests/deleteRow.spec.ts index a96aa7329f..dd13aa49c1 100644 --- a/packages/server/src/automations/tests/deleteRow.spec.ts +++ b/packages/server/src/automations/tests/deleteRow.spec.ts @@ -1,4 +1,4 @@ -const setup = require("./utilities") +import * as setup from "./utilities" describe("test the delete row action", () => { let table: any @@ -20,32 +20,29 @@ describe("test the delete row action", () => { afterAll(setup.afterAll) it("should be able to run the action", async () => { - const res = await setup.runStep(setup.actions.DELETE_ROW.stepId, inputs) + const res = await setup.runStep( + config, + setup.actions.DELETE_ROW.stepId, + inputs + ) expect(res.success).toEqual(true) expect(res.response).toBeDefined() expect(res.row._id).toEqual(row._id) - let error - try { - await config.getRow(table._id, res.row._id) - } catch (err) { - error = err - } - expect(error).toBeDefined() }) it("check usage quota attempts", async () => { await setup.runInProd(async () => { - await setup.runStep(setup.actions.DELETE_ROW.stepId, inputs) + await setup.runStep(config, setup.actions.DELETE_ROW.stepId, inputs) }) }) it("should check invalid inputs return an error", async () => { - const res = await setup.runStep(setup.actions.DELETE_ROW.stepId, {}) + const res = await setup.runStep(config, setup.actions.DELETE_ROW.stepId, {}) expect(res.success).toEqual(false) }) it("should return an error when table doesn't exist", async () => { - const res = await setup.runStep(setup.actions.DELETE_ROW.stepId, { + const res = await setup.runStep(config, setup.actions.DELETE_ROW.stepId, { tableId: "invalid", id: "invalid", revision: "invalid", diff --git a/packages/server/src/automations/tests/discord.spec.ts b/packages/server/src/automations/tests/discord.spec.ts index 07eab7205c..491fe0fb25 100644 --- a/packages/server/src/automations/tests/discord.spec.ts +++ b/packages/server/src/automations/tests/discord.spec.ts @@ -16,7 +16,7 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) - const res = await runStep(actions.discord.stepId, { + const res = await runStep(config, actions.discord.stepId, { url: "http://www.example.com", username: "joe_bloggs", }) diff --git a/packages/server/src/automations/tests/executeQuery.spec.ts b/packages/server/src/automations/tests/executeQuery.spec.ts index 5598641866..2d65be6e58 100644 --- a/packages/server/src/automations/tests/executeQuery.spec.ts +++ b/packages/server/src/automations/tests/executeQuery.spec.ts @@ -1,65 +1,80 @@ import { Datasource, Query } from "@budibase/types" import * as setup from "./utilities" -import { DatabaseName } from "../../integrations/tests/utils" +import { + DatabaseName, + datasourceDescribe, +} from "../../integrations/tests/utils" import { Knex } from "knex" +import { generator } from "@budibase/backend-core/tests" -describe.each([ - DatabaseName.POSTGRES, - DatabaseName.MYSQL, - DatabaseName.SQL_SERVER, - DatabaseName.MARIADB, - DatabaseName.ORACLE, -])("execute query action (%s)", name => { - let tableName: string - let client: Knex - let datasource: Datasource - let query: Query - const config = setup.getConfig() - - beforeAll(async () => { - await config.init() - - const testSetup = await setup.setupTestDatasource(config, name) - datasource = testSetup.datasource - client = testSetup.client - }) - - beforeEach(async () => { - tableName = await setup.createTestTable(client, { - a: { type: "string" }, - b: { type: "number" }, - }) - await setup.insertTestData(client, tableName, [{ a: "string", b: 1 }]) - query = await setup.saveTestQuery(config, client, tableName, datasource) - }) - - afterEach(async () => { - await client.schema.dropTable(tableName) - }) - - afterAll(setup.afterAll) - - it("should be able to execute a query", async () => { - let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, { - query: { queryId: query._id }, - }) - expect(res.response).toEqual([{ a: "string", b: 1 }]) - expect(res.success).toEqual(true) - }) - - it("should handle a null query value", async () => { - let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, { - query: null, - }) - expect(res.response.message).toEqual("Invalid inputs") - expect(res.success).toEqual(false) - }) - - it("should handle an error executing a query", async () => { - let res = await setup.runStep(setup.actions.EXECUTE_QUERY.stepId, { - query: { queryId: "wrong_id" }, - }) - expect(res.response).toBeDefined() - expect(res.success).toEqual(false) - }) +const descriptions = datasourceDescribe({ + exclude: [DatabaseName.MONGODB, DatabaseName.SQS], }) + +if (descriptions.length) { + describe.each(descriptions)( + "execute query action ($dbName)", + ({ config, dsProvider }) => { + let tableName: string + let client: Knex + let datasource: Datasource + let query: Query + + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource! + client = ds.client! + }) + + beforeEach(async () => { + tableName = generator.guid() + await client.schema.createTable(tableName, table => { + table.string("a") + table.integer("b") + }) + await client(tableName).insert({ a: "string", b: 1 }) + query = await setup.saveTestQuery(config, client, tableName, datasource) + }) + + afterEach(async () => { + await client.schema.dropTable(tableName) + }) + + it("should be able to execute a query", async () => { + let res = await setup.runStep( + config, + setup.actions.EXECUTE_QUERY.stepId, + { + query: { queryId: query._id }, + } + ) + expect(res.response).toEqual([{ a: "string", b: 1 }]) + expect(res.success).toEqual(true) + }) + + it("should handle a null query value", async () => { + let res = await setup.runStep( + config, + setup.actions.EXECUTE_QUERY.stepId, + { + query: null, + } + ) + expect(res.response.message).toEqual("Invalid inputs") + expect(res.success).toEqual(false) + }) + + it("should handle an error executing a query", async () => { + let res = await setup.runStep( + config, + setup.actions.EXECUTE_QUERY.stepId, + { + query: { queryId: "wrong_id" }, + } + ) + expect(res.response).toBeDefined() + expect(res.success).toEqual(false) + }) + } + ) +} diff --git a/packages/server/src/automations/tests/executeScript.spec.js b/packages/server/src/automations/tests/executeScript.spec.ts similarity index 70% rename from packages/server/src/automations/tests/executeScript.spec.js rename to packages/server/src/automations/tests/executeScript.spec.ts index 42f4f5776f..fa30e642bc 100644 --- a/packages/server/src/automations/tests/executeScript.spec.js +++ b/packages/server/src/automations/tests/executeScript.spec.ts @@ -1,15 +1,15 @@ -const setup = require("./utilities") +import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities" describe("test the execute script action", () => { - let config = setup.getConfig() + let config = getConfig() beforeAll(async () => { await config.init() }) - afterAll(setup.afterAll) + afterAll(_afterAll) it("should be able to execute a script", async () => { - const res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId, { + const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, { code: "return 1 + 1", }) expect(res.value).toEqual(2) @@ -17,7 +17,7 @@ describe("test the execute script action", () => { }) it("should handle a null value", async () => { - const res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId, { + const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, { code: null, }) expect(res.response.message).toEqual("Invalid inputs") @@ -25,8 +25,9 @@ describe("test the execute script action", () => { }) it("should be able to get a value from context", async () => { - const res = await setup.runStep( - setup.actions.EXECUTE_SCRIPT.stepId, + const res = await runStep( + config, + actions.EXECUTE_SCRIPT.stepId, { code: "return steps.map(d => d.value)", }, @@ -40,7 +41,7 @@ describe("test the execute script action", () => { }) it("should be able to handle an error gracefully", async () => { - const res = await setup.runStep(setup.actions.EXECUTE_SCRIPT.stepId, { + const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, { code: "return something.map(x => x.name)", }) expect(res.response).toEqual("ReferenceError: something is not defined") diff --git a/packages/server/src/automations/tests/filter.spec.ts b/packages/server/src/automations/tests/filter.spec.ts index c2c5d13e2d..a1ed72dd74 100644 --- a/packages/server/src/automations/tests/filter.spec.ts +++ b/packages/server/src/automations/tests/filter.spec.ts @@ -2,13 +2,19 @@ import * as setup from "./utilities" import { FilterConditions } from "../steps/filter" describe("test the filter logic", () => { + const config = setup.getConfig() + + beforeAll(async () => { + await config.init() + }) + async function checkFilter( field: any, condition: string, value: any, pass = true ) { - let res = await setup.runStep(setup.actions.FILTER.stepId, { + let res = await setup.runStep(config, setup.actions.FILTER.stepId, { field, condition, value, diff --git a/packages/server/src/automations/tests/make.spec.ts b/packages/server/src/automations/tests/make.spec.ts index 388b197c7f..414ac676d5 100644 --- a/packages/server/src/automations/tests/make.spec.ts +++ b/packages/server/src/automations/tests/make.spec.ts @@ -16,7 +16,7 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) - const res = await runStep(actions.integromat.stepId, { + const res = await runStep(config, actions.integromat.stepId, { url: "http://www.example.com", }) expect(res.response.foo).toEqual("bar") @@ -38,7 +38,7 @@ describe("test the outgoing webhook action", () => { .post("/", payload) .reply(200, { foo: "bar" }) - const res = await runStep(actions.integromat.stepId, { + const res = await runStep(config, actions.integromat.stepId, { body: { value: JSON.stringify(payload) }, url: "http://www.example.com", }) @@ -47,7 +47,7 @@ describe("test the outgoing webhook action", () => { }) it("should return a 400 if the JSON payload string is malformed", async () => { - const res = await runStep(actions.integromat.stepId, { + const res = await runStep(config, actions.integromat.stepId, { body: { value: "{ invalid json }" }, url: "http://www.example.com", }) diff --git a/packages/server/src/automations/tests/n8n.spec.ts b/packages/server/src/automations/tests/n8n.spec.ts index 0c18f313b1..5f27e4323a 100644 --- a/packages/server/src/automations/tests/n8n.spec.ts +++ b/packages/server/src/automations/tests/n8n.spec.ts @@ -16,7 +16,7 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action and default to 'get'", async () => { nock("http://www.example.com/").get("/").reply(200, { foo: "bar" }) - const res = await runStep(actions.n8n.stepId, { + const res = await runStep(config, actions.n8n.stepId, { url: "http://www.example.com", body: { test: "IGNORE_ME", @@ -30,7 +30,7 @@ describe("test the outgoing webhook action", () => { nock("http://www.example.com/") .post("/", { name: "Adam", age: 9 }) .reply(200) - const res = await runStep(actions.n8n.stepId, { + const res = await runStep(config, actions.n8n.stepId, { body: { value: JSON.stringify({ name: "Adam", age: 9 }), }, @@ -42,7 +42,7 @@ describe("test the outgoing webhook action", () => { it("should return a 400 if the JSON payload string is malformed", async () => { const payload = `{ value1 1 }` - const res = await runStep(actions.n8n.stepId, { + const res = await runStep(config, actions.n8n.stepId, { value1: "ONE", body: { value: payload, @@ -59,7 +59,7 @@ describe("test the outgoing webhook action", () => { nock("http://www.example.com/") .head("/", body => body === "") .reply(200) - const res = await runStep(actions.n8n.stepId, { + const res = await runStep(config, actions.n8n.stepId, { url: "http://www.example.com", method: "HEAD", body: { diff --git a/packages/server/src/automations/tests/openai.spec.ts b/packages/server/src/automations/tests/openai.spec.ts index 909a3e0b8b..8119750f8b 100644 --- a/packages/server/src/automations/tests/openai.spec.ts +++ b/packages/server/src/automations/tests/openai.spec.ts @@ -62,13 +62,13 @@ describe("test the openai action", () => { afterAll(_afterAll) it("should be able to receive a response from ChatGPT given a prompt", async () => { - const res = await runStep("OPENAI", { prompt: OPENAI_PROMPT }) + const res = await runStep(config, "OPENAI", { prompt: OPENAI_PROMPT }) expect(res.response).toEqual("This is a test") expect(res.success).toBeTruthy() }) it("should present the correct error message when a prompt is not provided", async () => { - const res = await runStep("OPENAI", { prompt: null }) + const res = await runStep(config, "OPENAI", { prompt: null }) expect(res.response).toEqual( "Budibase OpenAI Automation Failed: No prompt supplied" ) @@ -91,7 +91,7 @@ describe("test the openai action", () => { } as any) ) - const res = await runStep("OPENAI", { + const res = await runStep(config, "OPENAI", { prompt: OPENAI_PROMPT, }) @@ -106,7 +106,7 @@ describe("test the openai action", () => { jest.spyOn(pro.features, "isAICustomConfigsEnabled").mockResolvedValue(true) const prompt = "What is the meaning of life?" - await runStep("OPENAI", { + await runStep(config, "OPENAI", { model: "gpt-4o-mini", prompt, }) diff --git a/packages/server/src/automations/tests/outgoingWebhook.spec.ts b/packages/server/src/automations/tests/outgoingWebhook.spec.ts index 0e26927c55..995ab24bac 100644 --- a/packages/server/src/automations/tests/outgoingWebhook.spec.ts +++ b/packages/server/src/automations/tests/outgoingWebhook.spec.ts @@ -18,7 +18,7 @@ describe("test the outgoing webhook action", () => { nock("http://www.example.com") .post("/", { a: 1 }) .reply(200, { foo: "bar" }) - const res = await runStep(actions.OUTGOING_WEBHOOK.stepId, { + const res = await runStep(config, actions.OUTGOING_WEBHOOK.stepId, { requestMethod: "POST", url: "www.example.com", requestBody: JSON.stringify({ a: 1 }), @@ -28,7 +28,7 @@ describe("test the outgoing webhook action", () => { }) it("should return an error if something goes wrong in fetch", async () => { - const res = await runStep(actions.OUTGOING_WEBHOOK.stepId, { + const res = await runStep(config, actions.OUTGOING_WEBHOOK.stepId, { requestMethod: "GET", url: "www.invalid.com", }) diff --git a/packages/server/src/automations/tests/queryRows.spec.ts b/packages/server/src/automations/tests/queryRows.spec.ts index 6b9113a309..12611d3f90 100644 --- a/packages/server/src/automations/tests/queryRows.spec.ts +++ b/packages/server/src/automations/tests/queryRows.spec.ts @@ -33,7 +33,11 @@ describe("Test a query step automation", () => { sortOrder: "ascending", limit: 10, } - const res = await setup.runStep(setup.actions.QUERY_ROWS.stepId, inputs) + const res = await setup.runStep( + config, + setup.actions.QUERY_ROWS.stepId, + inputs + ) expect(res.success).toBe(true) expect(res.rows).toBeDefined() expect(res.rows.length).toBe(2) @@ -48,7 +52,11 @@ describe("Test a query step automation", () => { sortOrder: "ascending", limit: 10, } - const res = await setup.runStep(setup.actions.QUERY_ROWS.stepId, inputs) + const res = await setup.runStep( + config, + setup.actions.QUERY_ROWS.stepId, + inputs + ) expect(res.success).toBe(true) expect(res.rows).toBeDefined() expect(res.rows.length).toBe(2) @@ -65,7 +73,11 @@ describe("Test a query step automation", () => { limit: 10, onEmptyFilter: "none", } - const res = await setup.runStep(setup.actions.QUERY_ROWS.stepId, inputs) + const res = await setup.runStep( + config, + setup.actions.QUERY_ROWS.stepId, + inputs + ) expect(res.success).toBe(false) expect(res.rows).toBeDefined() expect(res.rows.length).toBe(0) @@ -85,7 +97,11 @@ describe("Test a query step automation", () => { sortOrder: "ascending", limit: 10, } - const res = await setup.runStep(setup.actions.QUERY_ROWS.stepId, inputs) + const res = await setup.runStep( + config, + setup.actions.QUERY_ROWS.stepId, + inputs + ) expect(res.success).toBe(false) expect(res.rows).toBeDefined() expect(res.rows.length).toBe(0) @@ -100,7 +116,11 @@ describe("Test a query step automation", () => { sortOrder: "ascending", limit: 10, } - const res = await setup.runStep(setup.actions.QUERY_ROWS.stepId, inputs) + const res = await setup.runStep( + config, + setup.actions.QUERY_ROWS.stepId, + inputs + ) expect(res.success).toBe(true) expect(res.rows).toBeDefined() expect(res.rows.length).toBe(2) diff --git a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts index 358aba35c8..45b251f4c1 100644 --- a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts @@ -1,9 +1,14 @@ import * as automation from "../../index" import * as setup from "../utilities" -import { LoopStepType, FieldType, Table } from "@budibase/types" +import { LoopStepType, FieldType, Table, Datasource } from "@budibase/types" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" -import { DatabaseName } from "../../../integrations/tests/utils" +import { + DatabaseName, + datasourceDescribe, +} from "../../../integrations/tests/utils" import { FilterConditions } from "../../../automations/steps/filter" +import { Knex } from "knex" +import { generator } from "@budibase/backend-core/tests" describe("Automation Scenarios", () => { let config = setup.getConfig() @@ -107,96 +112,6 @@ describe("Automation Scenarios", () => { expect(results.steps[2].outputs.rows).toHaveLength(1) }) - it("should query an external database for some data then insert than into an internal table", async () => { - const { datasource, client } = await setup.setupTestDatasource( - config, - DatabaseName.MYSQL - ) - - const newTable = await config.createTable({ - name: "table", - type: "table", - schema: { - name: { - name: "name", - type: FieldType.STRING, - constraints: { - presence: true, - }, - }, - age: { - name: "age", - type: FieldType.NUMBER, - constraints: { - presence: true, - }, - }, - }, - }) - - const tableName = await setup.createTestTable(client, { - name: { type: "string" }, - age: { type: "number" }, - }) - - const rows = [ - { name: "Joe", age: 20 }, - { name: "Bob", age: 25 }, - { name: "Paul", age: 30 }, - ] - - await setup.insertTestData(client, tableName, rows) - - const query = await setup.saveTestQuery( - config, - client, - tableName, - datasource - ) - - const builder = createAutomationBuilder({ - name: "Test external query and save", - }) - - const results = await builder - .appAction({ - fields: {}, - }) - .executeQuery({ - query: { - queryId: query._id!, - }, - }) - .loop({ - option: LoopStepType.ARRAY, - binding: "{{ steps.1.response }}", - }) - .createRow({ - row: { - name: "{{ loop.currentItem.name }}", - age: "{{ loop.currentItem.age }}", - tableId: newTable._id!, - }, - }) - .queryRows({ - tableId: newTable._id!, - }) - .run() - - expect(results.steps).toHaveLength(3) - - expect(results.steps[1].outputs.iterations).toBe(3) - expect(results.steps[1].outputs.items).toHaveLength(3) - - expect(results.steps[2].outputs.rows).toHaveLength(3) - - rows.forEach(expectedRow => { - expect(results.steps[2].outputs.rows).toEqual( - expect.arrayContaining([expect.objectContaining(expectedRow)]) - ) - }) - }) - it("should trigger an automation which creates and then updates a row", async () => { const table = await config.createTable({ name: "TestTable", @@ -517,3 +432,105 @@ describe("Automation Scenarios", () => { expect(results.steps[0].outputs.message).toContain("example.com") }) }) + +const descriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] }) + +if (descriptions.length) { + describe.each(descriptions)("/rows ($dbName)", ({ config, dsProvider }) => { + let datasource: Datasource + let client: Knex + + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource! + client = ds.client! + }) + + it("should query an external database for some data then insert than into an internal table", async () => { + const newTable = await config.createTable({ + name: "table", + type: "table", + schema: { + name: { + name: "name", + type: FieldType.STRING, + constraints: { + presence: true, + }, + }, + age: { + name: "age", + type: FieldType.NUMBER, + constraints: { + presence: true, + }, + }, + }, + }) + + const tableName = generator.guid() + await client.schema.createTable(tableName, table => { + table.string("name") + table.integer("age") + }) + + const rows = [ + { name: "Joe", age: 20 }, + { name: "Bob", age: 25 }, + { name: "Paul", age: 30 }, + ] + + await client(tableName).insert(rows) + + const query = await setup.saveTestQuery( + config, + client, + tableName, + datasource + ) + + const builder = createAutomationBuilder({ + name: "Test external query and save", + config, + }) + + const results = await builder + .appAction({ + fields: {}, + }) + .executeQuery({ + query: { + queryId: query._id!, + }, + }) + .loop({ + option: LoopStepType.ARRAY, + binding: "{{ steps.1.response }}", + }) + .createRow({ + row: { + name: "{{ loop.currentItem.name }}", + age: "{{ loop.currentItem.age }}", + tableId: newTable._id!, + }, + }) + .queryRows({ + tableId: newTable._id!, + }) + .run() + + expect(results.steps).toHaveLength(3) + + expect(results.steps[1].outputs.iterations).toBe(3) + expect(results.steps[1].outputs.items).toHaveLength(3) + + expect(results.steps[2].outputs.rows).toHaveLength(3) + + rows.forEach(expectedRow => { + expect(results.steps[2].outputs.rows).toEqual( + expect.arrayContaining([expect.objectContaining(expectedRow)]) + ) + }) + }) + }) +} diff --git a/packages/server/src/automations/tests/sendSmtpEmail.spec.ts b/packages/server/src/automations/tests/sendSmtpEmail.spec.ts index f96abde4e6..2977e8d64f 100644 --- a/packages/server/src/automations/tests/sendSmtpEmail.spec.ts +++ b/packages/server/src/automations/tests/sendSmtpEmail.spec.ts @@ -18,7 +18,7 @@ function generateResponse(to: string, from: string) { } } -const setup = require("./utilities") +import * as setup from "./utilities" describe("test the outgoing webhook action", () => { let inputs @@ -58,6 +58,7 @@ describe("test the outgoing webhook action", () => { } let resp = generateResponse(inputs.to, inputs.from) const res = await setup.runStep( + config, setup.actions.SEND_EMAIL_SMTP.stepId, inputs ) diff --git a/packages/server/src/automations/tests/serverLog.spec.js b/packages/server/src/automations/tests/serverLog.spec.ts similarity index 60% rename from packages/server/src/automations/tests/serverLog.spec.js rename to packages/server/src/automations/tests/serverLog.spec.ts index bb2531a65a..c2c1c385b6 100644 --- a/packages/server/src/automations/tests/serverLog.spec.js +++ b/packages/server/src/automations/tests/serverLog.spec.ts @@ -1,8 +1,8 @@ -const setup = require("./utilities") +import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities" describe("test the server log action", () => { - let config = setup.getConfig() - let inputs + let config = getConfig() + let inputs: any beforeAll(async () => { await config.init() @@ -10,10 +10,10 @@ describe("test the server log action", () => { text: "log message", } }) - afterAll(setup.afterAll) + afterAll(_afterAll) it("should be able to log the text", async () => { - let res = await setup.runStep(setup.actions.SERVER_LOG.stepId, inputs) + let res = await runStep(config, actions.SERVER_LOG.stepId, inputs) expect(res.message).toEqual(`App ${config.getAppId()} - ${inputs.text}`) expect(res.success).toEqual(true) }) diff --git a/packages/server/src/automations/tests/triggerAutomationRun.spec.ts b/packages/server/src/automations/tests/triggerAutomationRun.spec.ts index 9d699e15fa..e4d93d200f 100644 --- a/packages/server/src/automations/tests/triggerAutomationRun.spec.ts +++ b/packages/server/src/automations/tests/triggerAutomationRun.spec.ts @@ -29,6 +29,7 @@ describe("Test triggering an automation from another automation", () => { }, } const res = await setup.runStep( + config, setup.actions.TRIGGER_AUTOMATION_RUN.stepId, inputs ) @@ -44,6 +45,7 @@ describe("Test triggering an automation from another automation", () => { }, } const res = await setup.runStep( + config, setup.actions.TRIGGER_AUTOMATION_RUN.stepId, inputs ) diff --git a/packages/server/src/automations/tests/updateRow.spec.ts b/packages/server/src/automations/tests/updateRow.spec.ts index 76823e8a11..457bf60533 100644 --- a/packages/server/src/automations/tests/updateRow.spec.ts +++ b/packages/server/src/automations/tests/updateRow.spec.ts @@ -34,7 +34,11 @@ describe("test the update row action", () => { afterAll(setup.afterAll) it("should be able to run the action", async () => { - const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, inputs) + const res = await setup.runStep( + config, + setup.actions.UPDATE_ROW.stepId, + inputs + ) expect(res.success).toEqual(true) const updatedRow = await config.api.row.get(table._id!, res.id) expect(updatedRow.name).toEqual("Updated name") @@ -42,12 +46,12 @@ describe("test the update row action", () => { }) it("should check invalid inputs return an error", async () => { - const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, {}) + const res = await setup.runStep(config, setup.actions.UPDATE_ROW.stepId, {}) expect(res.success).toEqual(false) }) it("should return an error when table doesn't exist", async () => { - const res = await setup.runStep(setup.actions.UPDATE_ROW.stepId, { + const res = await setup.runStep(config, setup.actions.UPDATE_ROW.stepId, { row: { _id: "invalid" }, rowId: "invalid", }) @@ -90,16 +94,20 @@ describe("test the update row action", () => { expect(getResp.user1[0]._id).toEqual(user1._id) expect(getResp.user2[0]._id).toEqual(user2._id) - let stepResp = await setup.runStep(setup.actions.UPDATE_ROW.stepId, { - rowId: row._id, - row: { - _id: row._id, - _rev: row._rev, - tableId: row.tableId, - user1: [user2._id], - user2: "", - }, - }) + let stepResp = await setup.runStep( + config, + setup.actions.UPDATE_ROW.stepId, + { + rowId: row._id, + row: { + _id: row._id, + _rev: row._rev, + tableId: row.tableId, + user1: [user2._id], + user2: "", + }, + } + ) expect(stepResp.success).toEqual(true) getResp = await config.api.row.get(table._id!, row._id!) @@ -143,23 +151,27 @@ describe("test the update row action", () => { expect(getResp.user1[0]._id).toEqual(user1._id) expect(getResp.user2[0]._id).toEqual(user2._id) - let stepResp = await setup.runStep(setup.actions.UPDATE_ROW.stepId, { - rowId: row._id, - row: { - _id: row._id, - _rev: row._rev, - tableId: row.tableId, - user1: [user2._id], - user2: "", - }, - meta: { - fields: { - user2: { - clearRelationships: true, + let stepResp = await setup.runStep( + config, + setup.actions.UPDATE_ROW.stepId, + { + rowId: row._id, + row: { + _id: row._id, + _rev: row._rev, + tableId: row.tableId, + user1: [user2._id], + user2: "", + }, + meta: { + fields: { + user2: { + clearRelationships: true, + }, }, }, - }, - }) + } + ) expect(stepResp.success).toEqual(true) getResp = await config.api.row.get(table._id!, row._id!) diff --git a/packages/server/src/automations/tests/utilities/index.ts b/packages/server/src/automations/tests/utilities/index.ts index 8e3bbfe409..16c23f5db1 100644 --- a/packages/server/src/automations/tests/utilities/index.ts +++ b/packages/server/src/automations/tests/utilities/index.ts @@ -1,22 +1,16 @@ -import TestConfig from "../../../tests/utilities/TestConfiguration" +import TestConfiguration from "../../../tests/utilities/TestConfiguration" import { context } from "@budibase/backend-core" import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions" import emitter from "../../../events/index" import env from "../../../environment" import { AutomationActionStepId, Datasource } from "@budibase/types" import { Knex } from "knex" -import { generator } from "@budibase/backend-core/tests" -import { - getDatasource, - knexClient, - DatabaseName, -} from "../../../integrations/tests/utils" -let config: TestConfig +let config: TestConfiguration -export function getConfig(): TestConfig { +export function getConfig(): TestConfiguration { if (!config) { - config = new TestConfig(true) + config = new TestConfiguration(true) } return config } @@ -39,7 +33,12 @@ export async function runInProd(fn: any) { } } -export async function runStep(stepId: string, inputs: any, stepContext?: any) { +export async function runStep( + config: TestConfiguration, + stepId: string, + inputs: any, + stepContext?: any +) { async function run() { let step = await getAction(stepId as AutomationActionStepId) expect(step).toBeDefined() @@ -55,7 +54,7 @@ export async function runStep(stepId: string, inputs: any, stepContext?: any) { emitter, }) } - if (config?.appId) { + if (config.appId) { return context.doInContext(config?.appId, async () => { return run() }) @@ -64,31 +63,8 @@ export async function runStep(stepId: string, inputs: any, stepContext?: any) { } } -export async function createTestTable(client: Knex, schema: any) { - const tableName = generator.guid() - await client.schema.createTable(tableName, table => { - for (const fieldName in schema) { - const field = schema[fieldName] - if (field.type === "string") { - table.string(fieldName) - } else if (field.type === "number") { - table.integer(fieldName) - } - } - }) - return tableName -} - -export async function insertTestData( - client: Knex, - tableName: string, - rows: any[] -) { - await client(tableName).insert(rows) -} - export async function saveTestQuery( - config: TestConfig, + config: TestConfiguration, client: Knex, tableName: string, datasource: Datasource @@ -107,15 +83,5 @@ export async function saveTestQuery( }) } -export async function setupTestDatasource( - config: TestConfig, - dbName: DatabaseName -) { - const db = await getDatasource(dbName) - const datasource = await config.api.datasource.create(db) - const client = await knexClient(db) - return { datasource, client } -} - export const apiKey = "test" export const actions = BUILTIN_ACTION_DEFINITIONS diff --git a/packages/server/src/automations/tests/zapier.spec.ts b/packages/server/src/automations/tests/zapier.spec.ts index a7dc7d3eae..1288e7efec 100644 --- a/packages/server/src/automations/tests/zapier.spec.ts +++ b/packages/server/src/automations/tests/zapier.spec.ts @@ -16,7 +16,7 @@ describe("test the outgoing webhook action", () => { it("should be able to run the action", async () => { nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) - const res = await runStep(actions.zapier.stepId, { + const res = await runStep(config, actions.zapier.stepId, { url: "http://www.example.com", }) expect(res.response.foo).toEqual("bar") @@ -38,7 +38,7 @@ describe("test the outgoing webhook action", () => { .post("/", { ...payload, platform: "budibase" }) .reply(200, { foo: "bar" }) - const res = await runStep(actions.zapier.stepId, { + const res = await runStep(config, actions.zapier.stepId, { body: { value: JSON.stringify(payload) }, url: "http://www.example.com", }) @@ -47,7 +47,7 @@ describe("test the outgoing webhook action", () => { }) it("should return a 400 if the JSON payload string is malformed", async () => { - const res = await runStep(actions.zapier.stepId, { + const res = await runStep(config, actions.zapier.stepId, { body: { value: "{ invalid json }" }, url: "http://www.example.com", }) diff --git a/packages/server/src/db/linkedRows/index.ts b/packages/server/src/db/linkedRows/index.ts index 73ac695878..06a4005061 100644 --- a/packages/server/src/db/linkedRows/index.ts +++ b/packages/server/src/db/linkedRows/index.ts @@ -14,11 +14,10 @@ import { coreOutputProcessing, processFormulas, } from "../../utilities/rowProcessor" -import { context, features } from "@budibase/backend-core" +import { context } from "@budibase/backend-core" import { ContextUser, EventType, - FeatureFlag, FieldType, LinkDocumentValue, Row, @@ -251,19 +250,13 @@ export async function squashLinks( source: Table | ViewV2, enriched: T ): Promise { - const allowRelationshipSchemas = await features.flags.isEnabled( - FeatureFlag.ENRICHED_RELATIONSHIPS - ) - let viewSchema: ViewV2Schema = {} if (sdk.views.isView(source)) { if (helpers.views.isCalculationView(source)) { return enriched } - if (allowRelationshipSchemas) { - viewSchema = source.schema || {} - } + viewSchema = source.schema || {} } let table: Table diff --git a/packages/server/src/definitions/openapi.ts b/packages/server/src/definitions/openapi.ts index 4479c89d9d..b82229130b 100644 --- a/packages/server/src/definitions/openapi.ts +++ b/packages/server/src/definitions/openapi.ts @@ -65,6 +65,9 @@ export interface paths { "/tables/{tableId}/rows/search": { post: operations["rowSearch"]; }; + "/views/{viewId}/rows/search": { + post: operations["rowViewSearch"]; + }; "/tables": { /** Create a table, this could be internal or external. */ post: operations["tableCreate"]; @@ -93,6 +96,22 @@ export interface paths { /** Based on user properties (currently only name) search for users. */ post: operations["userSearch"]; }; + "/views": { + /** Create a view, this can be against an internal or external table. */ + post: operations["viewCreate"]; + }; + "/views/{viewId}": { + /** Lookup a view, this could be internal or external. */ + get: operations["viewGetById"]; + /** Update a view, this can be against an internal or external table. */ + put: operations["viewUpdate"]; + /** Delete a view, this can be against an internal or external table. */ + delete: operations["viewDestroy"]; + }; + "/views/search": { + /** Based on view properties (currently only name) search for views. */ + post: operations["viewSearch"]; + }; } export interface components { @@ -813,10 +832,442 @@ export interface components { userIds: string[]; }; }; + /** @description The view to be created/updated. */ + view: { + /** @description The name of the view. */ + name: string; + /** @description The ID of the table this view is based on. */ + tableId: string; + /** + * @description The type of view - standard (empty value) or calculation. + * @enum {string} + */ + type?: "calculation"; + /** @description A column used to display rows from this view - usually used when rendered in tables. */ + primaryDisplay?: string; + /** @description Search parameters for view */ + query?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** + * @description If no filters match, should the view return all rows, or no rows. + * @enum {string} + */ + onEmptyFilter?: "all" | "none"; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + }[]; + }[]; + }; + sort?: { + /** @description The field from the table/view schema to sort on. */ + field: string; + /** + * @description The order in which to sort. + * @enum {string} + */ + order?: "ascending" | "descending"; + /** + * @description The type of sort to perform (by number, or by alphabetically). + * @enum {string} + */ + type?: "string" | "number"; + }; + schema: { + [key: string]: + | { + /** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */ + visible?: boolean; + /** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */ + readonly?: boolean; + /** @description A number defining where the column shows up in tables, lowest being first. */ + order?: number; + /** @description A width for the column, defined in pixels - this affects rendering in tables. */ + width?: number; + /** @description If this is a relationship column, we can set the columns we wish to include */ + column?: { + readonly?: boolean; + }[]; + } + | { + /** + * @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping. + * @enum {string} + */ + calculationType?: "sum" | "avg" | "count" | "min" | "max"; + /** @description The field from the table to perform the calculation on. */ + field?: string; + /** @description Can be used in tandem with the count calculation type, to count unique entries. */ + distinct?: boolean; + }; + }; + }; + viewOutput: { + /** @description The view to be created/updated. */ + data: { + /** @description The name of the view. */ + name: string; + /** @description The ID of the table this view is based on. */ + tableId: string; + /** + * @description The type of view - standard (empty value) or calculation. + * @enum {string} + */ + type?: "calculation"; + /** @description A column used to display rows from this view - usually used when rendered in tables. */ + primaryDisplay?: string; + /** @description Search parameters for view */ + query?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** + * @description If no filters match, should the view return all rows, or no rows. + * @enum {string} + */ + onEmptyFilter?: "all" | "none"; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + }[]; + }[]; + }; + sort?: { + /** @description The field from the table/view schema to sort on. */ + field: string; + /** + * @description The order in which to sort. + * @enum {string} + */ + order?: "ascending" | "descending"; + /** + * @description The type of sort to perform (by number, or by alphabetically). + * @enum {string} + */ + type?: "string" | "number"; + }; + schema: { + [key: string]: + | { + /** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */ + visible?: boolean; + /** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */ + readonly?: boolean; + /** @description A number defining where the column shows up in tables, lowest being first. */ + order?: number; + /** @description A width for the column, defined in pixels - this affects rendering in tables. */ + width?: number; + /** @description If this is a relationship column, we can set the columns we wish to include */ + column?: { + readonly?: boolean; + }[]; + } + | { + /** + * @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping. + * @enum {string} + */ + calculationType?: "sum" | "avg" | "count" | "min" | "max"; + /** @description The field from the table to perform the calculation on. */ + field?: string; + /** @description Can be used in tandem with the count calculation type, to count unique entries. */ + distinct?: boolean; + }; + }; + /** @description The ID of the view. */ + id: string; + }; + }; + viewSearch: { + data: { + /** @description The name of the view. */ + name: string; + /** @description The ID of the table this view is based on. */ + tableId: string; + /** + * @description The type of view - standard (empty value) or calculation. + * @enum {string} + */ + type?: "calculation"; + /** @description A column used to display rows from this view - usually used when rendered in tables. */ + primaryDisplay?: string; + /** @description Search parameters for view */ + query?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** + * @description If no filters match, should the view return all rows, or no rows. + * @enum {string} + */ + onEmptyFilter?: "all" | "none"; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + }[]; + }[]; + }; + sort?: { + /** @description The field from the table/view schema to sort on. */ + field: string; + /** + * @description The order in which to sort. + * @enum {string} + */ + order?: "ascending" | "descending"; + /** + * @description The type of sort to perform (by number, or by alphabetically). + * @enum {string} + */ + type?: "string" | "number"; + }; + schema: { + [key: string]: + | { + /** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */ + visible?: boolean; + /** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */ + readonly?: boolean; + /** @description A number defining where the column shows up in tables, lowest being first. */ + order?: number; + /** @description A width for the column, defined in pixels - this affects rendering in tables. */ + width?: number; + /** @description If this is a relationship column, we can set the columns we wish to include */ + column?: { + readonly?: boolean; + }[]; + } + | { + /** + * @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping. + * @enum {string} + */ + calculationType?: "sum" | "avg" | "count" | "min" | "max"; + /** @description The field from the table to perform the calculation on. */ + field?: string; + /** @description Can be used in tandem with the count calculation type, to count unique entries. */ + distinct?: boolean; + }; + }; + /** @description The ID of the view. */ + id: string; + }[]; + }; }; parameters: { /** @description The ID of the table which this request is targeting. */ tableId: string; + /** @description The ID of the view which this request is targeting. */ + viewId: string; /** @description The ID of the row which this request is targeting. */ rowId: string; /** @description The ID of the app which this request is targeting. */ @@ -1213,6 +1664,31 @@ export interface operations { }; }; }; + rowViewSearch: { + parameters: { + path: { + /** The ID of the view which this request is targeting. */ + viewId: components["parameters"]["viewId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** The response will contain an array of rows that match the search parameters. */ + 200: { + content: { + "application/json": components["schemas"]["searchOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["rowSearch"]; + }; + }; + }; /** Create a table, this could be internal or external. */ tableCreate: { parameters: { @@ -1409,6 +1885,118 @@ export interface operations { }; }; }; + /** Create a view, this can be against an internal or external table. */ + viewCreate: { + parameters: { + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the created view, including the ID which has been generated for it. */ + 200: { + content: { + "application/json": components["schemas"]["viewOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["view"]; + }; + }; + }; + /** Lookup a view, this could be internal or external. */ + viewGetById: { + parameters: { + path: { + /** The ID of the view which this request is targeting. */ + viewId: components["parameters"]["viewId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the retrieved view. */ + 200: { + content: { + "application/json": components["schemas"]["viewOutput"]; + }; + }; + }; + }; + /** Update a view, this can be against an internal or external table. */ + viewUpdate: { + parameters: { + path: { + /** The ID of the view which this request is targeting. */ + viewId: components["parameters"]["viewId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the updated view. */ + 200: { + content: { + "application/json": components["schemas"]["viewOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["view"]; + }; + }; + }; + /** Delete a view, this can be against an internal or external table. */ + viewDestroy: { + parameters: { + path: { + /** The ID of the view which this request is targeting. */ + viewId: components["parameters"]["viewId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the deleted view. */ + 200: { + content: { + "application/json": components["schemas"]["viewOutput"]; + }; + }; + }; + }; + /** Based on view properties (currently only name) search for views. */ + viewSearch: { + parameters: { + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the found views, based on the search parameters. */ + 200: { + content: { + "application/json": components["schemas"]["viewSearch"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["nameSearch"]; + }; + }; + }; } export interface external {} diff --git a/packages/server/src/integration-test/mysql.spec.ts b/packages/server/src/integration-test/mysql.spec.ts index eb6c840abc..9cf7242e24 100644 --- a/packages/server/src/integration-test/mysql.spec.ts +++ b/packages/server/src/integration-test/mysql.spec.ts @@ -1,10 +1,5 @@ -import * as setup from "../api/routes/tests/utilities" import { Datasource, FieldType } from "@budibase/types" -import { - DatabaseName, - getDatasource, - knexClient, -} from "../integrations/tests/utils" +import { DatabaseName, datasourceDescribe } from "../integrations/tests/utils" import { generator } from "@budibase/backend-core/tests" import { Knex } from "knex" @@ -15,112 +10,123 @@ function uniqueTableName(length?: number): string { .substring(0, length || 10) } -const config = setup.getConfig()! +const mainDescriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] }) -describe("mysql integrations", () => { - let datasource: Datasource - let client: Knex +if (mainDescriptions.length) { + describe.each(mainDescriptions)( + "/Integration compatibility with mysql search_path ($dbName)", + ({ config, dsProvider }) => { + let rawDatasource: Datasource + let datasource: Datasource + let client: Knex - beforeAll(async () => { - await config.init() - const rawDatasource = await getDatasource(DatabaseName.MYSQL) - datasource = await config.api.datasource.create(rawDatasource) - client = await knexClient(rawDatasource) - }) + const database = generator.guid() + const database2 = generator.guid() - afterAll(config.end) + beforeAll(async () => { + const ds = await dsProvider() + rawDatasource = ds.rawDatasource! + datasource = ds.datasource! + client = ds.client! - describe("Integration compatibility with mysql search_path", () => { - let datasource: Datasource - let rawDatasource: Datasource - let client: Knex - const database = generator.guid() - const database2 = generator.guid() + await client.raw(`CREATE DATABASE \`${database}\`;`) + await client.raw(`CREATE DATABASE \`${database2}\`;`) - beforeAll(async () => { - rawDatasource = await getDatasource(DatabaseName.MYSQL) - client = await knexClient(rawDatasource) - - await client.raw(`CREATE DATABASE \`${database}\`;`) - await client.raw(`CREATE DATABASE \`${database2}\`;`) - - rawDatasource.config!.database = database - datasource = await config.api.datasource.create(rawDatasource) - }) - - afterAll(async () => { - await client.raw(`DROP DATABASE \`${database}\`;`) - await client.raw(`DROP DATABASE \`${database2}\`;`) - }) - - it("discovers tables from any schema in search path", async () => { - await client.schema.createTable(`${database}.table1`, table => { - table.increments("id1").primary() + rawDatasource.config!.database = database + datasource = await config.api.datasource.create(rawDatasource) }) - const res = await config.api.datasource.info(datasource) - expect(res.tableNames).toBeDefined() - expect(res.tableNames).toEqual(expect.arrayContaining(["table1"])) - }) - it("does not mix columns from different tables", async () => { - const repeated_table_name = "table_same_name" - await client.schema.createTable( - `${database}.${repeated_table_name}`, - table => { - table.increments("id").primary() - table.string("val1") - } - ) - await client.schema.createTable( - `${database2}.${repeated_table_name}`, - table => { - table.increments("id2").primary() - table.string("val2") - } - ) - - const res = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - tablesFilter: [repeated_table_name], + afterAll(async () => { + await client.raw(`DROP DATABASE \`${database}\`;`) + await client.raw(`DROP DATABASE \`${database2}\`;`) }) - expect(res.datasource.entities![repeated_table_name].schema).toBeDefined() - const schema = res.datasource.entities![repeated_table_name].schema - expect(Object.keys(schema).sort()).toEqual(["id", "val1"]) - }) - }) - describe("POST /api/datasources/:datasourceId/schema", () => { - let tableName: string + it("discovers tables from any schema in search path", async () => { + await client.schema.createTable(`${database}.table1`, table => { + table.increments("id1").primary() + }) + const res = await config.api.datasource.info(datasource) + expect(res.tableNames).toBeDefined() + expect(res.tableNames).toEqual(expect.arrayContaining(["table1"])) + }) - beforeEach(async () => { - tableName = uniqueTableName() - }) - - afterEach(async () => { - await client.schema.dropTableIfExists(tableName) - }) - - it("recognises enum columns as options", async () => { - const enumColumnName = "status" - - await client.schema.createTable(tableName, table => { - table.increments("order_id").primary() - table.string("customer_name", 100).notNullable() - table.enum( - enumColumnName, - ["pending", "processing", "shipped", "delivered", "cancelled"], - { useNative: true, enumName: `${tableName}_${enumColumnName}` } + it("does not mix columns from different tables", async () => { + const repeated_table_name = "table_same_name" + await client.schema.createTable( + `${database}.${repeated_table_name}`, + table => { + table.increments("id").primary() + table.string("val1") + } ) + await client.schema.createTable( + `${database2}.${repeated_table_name}`, + table => { + table.increments("id2").primary() + table.string("val2") + } + ) + + const res = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + tablesFilter: [repeated_table_name], + }) + expect( + res.datasource.entities![repeated_table_name].schema + ).toBeDefined() + const schema = res.datasource.entities![repeated_table_name].schema + expect(Object.keys(schema).sort()).toEqual(["id", "val1"]) }) + } + ) - const res = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) + const descriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] }) - const table = res.datasource.entities![tableName] + if (descriptions.length) { + describe.each(descriptions)( + "POST /api/datasources/:datasourceId/schema ($dbName)", + ({ config, dsProvider }) => { + let datasource: Datasource + let client: Knex - expect(table).toBeDefined() - expect(table.schema[enumColumnName].type).toEqual(FieldType.OPTIONS) - }) - }) -}) + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource! + client = ds.client! + }) + + let tableName: string + beforeEach(async () => { + tableName = uniqueTableName() + }) + + afterEach(async () => { + await client.schema.dropTableIfExists(tableName) + }) + + it("recognises enum columns as options", async () => { + const enumColumnName = "status" + + await client.schema.createTable(tableName, table => { + table.increments("order_id").primary() + table.string("customer_name", 100).notNullable() + table.enum( + enumColumnName, + ["pending", "processing", "shipped", "delivered", "cancelled"], + { useNative: true, enumName: `${tableName}_${enumColumnName}` } + ) + }) + + const res = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + + const table = res.datasource.entities![tableName] + + expect(table).toBeDefined() + expect(table.schema[enumColumnName].type).toEqual(FieldType.OPTIONS) + }) + } + ) + } +} diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts index 7654d84551..4f63579ba1 100644 --- a/packages/server/src/integration-test/postgres.spec.ts +++ b/packages/server/src/integration-test/postgres.spec.ts @@ -1,282 +1,299 @@ -import * as setup from "../api/routes/tests/utilities" import { Datasource, FieldType, Table } from "@budibase/types" import _ from "lodash" import { generator } from "@budibase/backend-core/tests" import { DatabaseName, - getDatasource, + datasourceDescribe, knexClient, } from "../integrations/tests/utils" import { Knex } from "knex" -const config = setup.getConfig()! +const mainDescriptions = datasourceDescribe({ only: [DatabaseName.POSTGRES] }) -describe("postgres integrations", () => { - let datasource: Datasource - let client: Knex +if (mainDescriptions.length) { + describe.each(mainDescriptions)( + "/postgres integrations", + ({ config, dsProvider }) => { + let datasource: Datasource + let client: Knex - beforeAll(async () => { - await config.init() - const rawDatasource = await getDatasource(DatabaseName.POSTGRES) - datasource = await config.api.datasource.create(rawDatasource) - client = await knexClient(rawDatasource) - }) - - afterAll(config.end) - - describe("POST /api/datasources/:datasourceId/schema", () => { - let tableName: string - - beforeEach(async () => { - tableName = generator.guid().replaceAll("-", "").substring(0, 10) - }) - - afterEach(async () => { - await client.schema.dropTableIfExists(tableName) - }) - - it("recognises when a table has no primary key", async () => { - await client.schema.createTable(tableName, table => { - table.increments("id", { primaryKey: false }) + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource! + client = ds.client! }) - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) + afterAll(config.end) - expect(response.errors).toEqual({ - [tableName]: "Table must have a primary key.", - }) - }) + describe("POST /api/datasources/:datasourceId/schema", () => { + let tableName: string - it("recognises when a table is using a reserved column name", async () => { - await client.schema.createTable(tableName, table => { - table.increments("_id").primary() - }) + beforeEach(async () => { + tableName = generator.guid().replaceAll("-", "").substring(0, 10) + }) - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) + afterEach(async () => { + await client.schema.dropTableIfExists(tableName) + }) - expect(response.errors).toEqual({ - [tableName]: "Table contains invalid columns.", - }) - }) + it("recognises when a table has no primary key", async () => { + await client.schema.createTable(tableName, table => { + table.increments("id", { primaryKey: false }) + }) - it("recognises enum columns as options", async () => { - const tableName = `orders_${generator - .guid() - .replaceAll("-", "") - .substring(0, 6)}` + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) - await client.schema.createTable(tableName, table => { - table.increments("order_id").primary() - table.string("customer_name").notNullable() - table.enum("status", ["pending", "processing", "shipped"], { - useNative: true, - enumName: `${tableName}_status`, + expect(response.errors).toEqual({ + [tableName]: "Table must have a primary key.", + }) + }) + + it("recognises when a table is using a reserved column name", async () => { + await client.schema.createTable(tableName, table => { + table.increments("_id").primary() + }) + + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + + expect(response.errors).toEqual({ + [tableName]: "Table contains invalid columns.", + }) + }) + + it("recognises enum columns as options", async () => { + const tableName = `orders_${generator + .guid() + .replaceAll("-", "") + .substring(0, 6)}` + + await client.schema.createTable(tableName, table => { + table.increments("order_id").primary() + table.string("customer_name").notNullable() + table.enum("status", ["pending", "processing", "shipped"], { + useNative: true, + enumName: `${tableName}_status`, + }) + }) + + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + + const table = response.datasource.entities?.[tableName] + + expect(table).toBeDefined() + expect(table?.schema["status"].type).toEqual(FieldType.OPTIONS) }) }) - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, + describe("check custom column types", () => { + beforeAll(async () => { + await client.schema.createTable("binaryTable", table => { + table.binary("id").primary() + table.string("column1") + table.integer("column2") + }) + }) + + it("should handle binary columns", async () => { + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + expect(response.datasource.entities).toBeDefined() + const table = response.datasource.entities?.["binaryTable"] + expect(table).toBeDefined() + expect(table?.schema.id.externalType).toBe("bytea") + const row = await config.api.row.save(table?._id!, { + id: "1111", + column1: "hello", + column2: 222, + }) + expect(row._id).toBeDefined() + const decoded = decodeURIComponent(row._id!).replace(/'/g, '"') + expect(JSON.parse(decoded)[0]).toBe("1111") + }) }) - const table = response.datasource.entities?.[tableName] + describe("check fetching null/not null table", () => { + beforeAll(async () => { + await client.schema.createTable("nullableTable", table => { + table.increments("order_id").primary() + table.integer("order_number").notNullable() + }) + }) - expect(table).toBeDefined() - expect(table?.schema["status"].type).toEqual(FieldType.OPTIONS) - }) - }) + it("should be able to change the table to allow nullable and refetch this", async () => { + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + const entities = response.datasource.entities + expect(entities).toBeDefined() + const nullableTable = entities?.["nullableTable"] + expect(nullableTable).toBeDefined() + expect( + nullableTable?.schema["order_number"].constraints?.presence + ).toEqual(true) - describe("Integration compatibility with postgres search_path", () => { - let datasource: Datasource - let client: Knex - let schema1: string - let schema2: string + // need to perform these calls raw to the DB so that the external state of the DB differs to what Budibase + // is aware of - therefore we can try to fetch and make sure BB updates correctly + await client.schema.alterTable("nullableTable", table => { + table.setNullable("order_number") + }) - beforeEach(async () => { - schema1 = generator.guid().replaceAll("-", "") - schema2 = generator.guid().replaceAll("-", "") - - const rawDatasource = await getDatasource(DatabaseName.POSTGRES) - client = await knexClient(rawDatasource) - - await client.schema.createSchema(schema1) - await client.schema.createSchema(schema2) - - rawDatasource.config!.schema = `${schema1}, ${schema2}` - - client = await knexClient(rawDatasource) - datasource = await config.api.datasource.create(rawDatasource) - }) - - afterEach(async () => { - await client.schema.dropSchema(schema1, true) - await client.schema.dropSchema(schema2, true) - }) - - it("discovers tables from any schema in search path", async () => { - await client.schema.createTable(`${schema1}.table1`, table => { - table.increments("id1").primary() + const responseAfter = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + const entitiesAfter = responseAfter.datasource.entities + expect(entitiesAfter).toBeDefined() + const nullableTableAfter = entitiesAfter?.["nullableTable"] + expect(nullableTableAfter).toBeDefined() + expect( + nullableTableAfter?.schema["order_number"].constraints?.presence + ).toBeUndefined() + }) }) - await client.schema.createTable(`${schema2}.table2`, table => { - table.increments("id2").primary() - }) + describe("money field 💰", () => { + const tableName = "moneytable" + let table: Table - const response = await config.api.datasource.info(datasource) - expect(response.tableNames).toBeDefined() - expect(response.tableNames).toEqual( - expect.arrayContaining(["table1", "table2"]) - ) - }) - - it("does not mix columns from different tables", async () => { - const repeated_table_name = "table_same_name" - - await client.schema.createTable( - `${schema1}.${repeated_table_name}`, - table => { - table.increments("id").primary() - table.string("val1") - } - ) - - await client.schema.createTable( - `${schema2}.${repeated_table_name}`, - table => { - table.increments("id2").primary() - table.string("val2") - } - ) - - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - tablesFilter: [repeated_table_name], - }) - expect( - response.datasource.entities?.[repeated_table_name].schema - ).toBeDefined() - const schema = response.datasource.entities?.[repeated_table_name].schema - expect(Object.keys(schema || {}).sort()).toEqual(["id", "val1"]) - }) - }) - - describe("check custom column types", () => { - beforeAll(async () => { - await client.schema.createTable("binaryTable", table => { - table.binary("id").primary() - table.string("column1") - table.integer("column2") - }) - }) - - it("should handle binary columns", async () => { - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) - expect(response.datasource.entities).toBeDefined() - const table = response.datasource.entities?.["binaryTable"] - expect(table).toBeDefined() - expect(table?.schema.id.externalType).toBe("bytea") - const row = await config.api.row.save(table?._id!, { - id: "1111", - column1: "hello", - column2: 222, - }) - expect(row._id).toBeDefined() - const decoded = decodeURIComponent(row._id!).replace(/'/g, '"') - expect(JSON.parse(decoded)[0]).toBe("1111") - }) - }) - - describe("check fetching null/not null table", () => { - beforeAll(async () => { - await client.schema.createTable("nullableTable", table => { - table.increments("order_id").primary() - table.integer("order_number").notNullable() - }) - }) - - it("should be able to change the table to allow nullable and refetch this", async () => { - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) - const entities = response.datasource.entities - expect(entities).toBeDefined() - const nullableTable = entities?.["nullableTable"] - expect(nullableTable).toBeDefined() - expect( - nullableTable?.schema["order_number"].constraints?.presence - ).toEqual(true) - - // need to perform these calls raw to the DB so that the external state of the DB differs to what Budibase - // is aware of - therefore we can try to fetch and make sure BB updates correctly - await client.schema.alterTable("nullableTable", table => { - table.setNullable("order_number") - }) - - const responseAfter = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, - }) - const entitiesAfter = responseAfter.datasource.entities - expect(entitiesAfter).toBeDefined() - const nullableTableAfter = entitiesAfter?.["nullableTable"] - expect(nullableTableAfter).toBeDefined() - expect( - nullableTableAfter?.schema["order_number"].constraints?.presence - ).toBeUndefined() - }) - }) - - describe("money field 💰", () => { - const tableName = "moneytable" - let table: Table - - beforeAll(async () => { - await client.raw(` + beforeAll(async () => { + await client.raw(` CREATE TABLE ${tableName} ( id serial PRIMARY KEY, price money ) `) - const response = await config.api.datasource.fetchSchema({ - datasourceId: datasource._id!, + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + }) + table = response.datasource.entities![tableName] + }) + + it("should be able to import a money field", async () => { + expect(table).toBeDefined() + expect(table?.schema.price.type).toBe(FieldType.NUMBER) + }) + + it("should be able to search a money field", async () => { + await config.api.row.bulkImport(table._id!, { + rows: [{ price: 200 }, { price: 300 }], + }) + + const { rows } = await config.api.row.search(table._id!, { + query: { + equal: { + price: 200, + }, + }, + }) + expect(rows).toHaveLength(1) + expect(rows[0].price).toBe("200.00") + }) + + it("should be able to update a money field", async () => { + let row = await config.api.row.save(table._id!, { price: 200 }) + expect(row.price).toBe("200.00") + + row = await config.api.row.save(table._id!, { ...row, price: 300 }) + expect(row.price).toBe("300.00") + + row = await config.api.row.save(table._id!, { + ...row, + price: "400.00", + }) + expect(row.price).toBe("400.00") + }) }) - table = response.datasource.entities![tableName] - }) + } + ) - it("should be able to import a money field", async () => { - expect(table).toBeDefined() - expect(table?.schema.price.type).toBe(FieldType.NUMBER) - }) + const descriptions = datasourceDescribe({ only: [DatabaseName.POSTGRES] }) - it("should be able to search a money field", async () => { - await config.api.row.bulkImport(table._id!, { - rows: [{ price: 200 }, { price: 300 }], - }) + if (descriptions.length) { + describe.each(descriptions)( + "Integration compatibility with postgres search_path", + ({ config, dsProvider }) => { + let datasource: Datasource + let client: Knex + let schema1: string + let schema2: string - const { rows } = await config.api.row.search(table._id!, { - query: { - equal: { - price: 200, - }, - }, - }) - expect(rows).toHaveLength(1) - expect(rows[0].price).toBe("200.00") - }) + beforeEach(async () => { + const ds = await dsProvider() + datasource = ds.datasource! + const rawDatasource = ds.rawDatasource! - it("should be able to update a money field", async () => { - let row = await config.api.row.save(table._id!, { price: 200 }) - expect(row.price).toBe("200.00") + schema1 = generator.guid().replaceAll("-", "") + schema2 = generator.guid().replaceAll("-", "") - row = await config.api.row.save(table._id!, { ...row, price: 300 }) - expect(row.price).toBe("300.00") + client = await knexClient(rawDatasource) - row = await config.api.row.save(table._id!, { ...row, price: "400.00" }) - expect(row.price).toBe("400.00") - }) - }) -}) + await client.schema.createSchema(schema1) + await client.schema.createSchema(schema2) + + rawDatasource.config!.schema = `${schema1}, ${schema2}` + + client = await knexClient(rawDatasource) + datasource = await config.api.datasource.create(rawDatasource) + }) + + afterEach(async () => { + await client.schema.dropSchema(schema1, true) + await client.schema.dropSchema(schema2, true) + }) + + it("discovers tables from any schema in search path", async () => { + await client.schema.createTable(`${schema1}.table1`, table => { + table.increments("id1").primary() + }) + + await client.schema.createTable(`${schema2}.table2`, table => { + table.increments("id2").primary() + }) + + const response = await config.api.datasource.info(datasource) + expect(response.tableNames).toBeDefined() + expect(response.tableNames).toEqual( + expect.arrayContaining(["table1", "table2"]) + ) + }) + + it("does not mix columns from different tables", async () => { + const repeated_table_name = "table_same_name" + + await client.schema.createTable( + `${schema1}.${repeated_table_name}`, + table => { + table.increments("id").primary() + table.string("val1") + } + ) + + await client.schema.createTable( + `${schema2}.${repeated_table_name}`, + table => { + table.increments("id2").primary() + table.string("val2") + } + ) + + const response = await config.api.datasource.fetchSchema({ + datasourceId: datasource._id!, + tablesFilter: [repeated_table_name], + }) + expect( + response.datasource.entities?.[repeated_table_name].schema + ).toBeDefined() + const schema = + response.datasource.entities?.[repeated_table_name].schema + expect(Object.keys(schema || {}).sort()).toEqual(["id", "val1"]) + }) + } + ) + } +} diff --git a/packages/server/src/integrations/index.ts b/packages/server/src/integrations/index.ts index fb892dcc79..de700d631d 100644 --- a/packages/server/src/integrations/index.ts +++ b/packages/server/src/integrations/index.ts @@ -120,7 +120,7 @@ export async function getIntegration(integration: SourceName) { } } } - throw new Error("No datasource implementation found.") + throw new Error(`No datasource implementation found called: "${integration}"`) } export default { diff --git a/packages/server/src/integrations/microsoftSqlServer.ts b/packages/server/src/integrations/microsoftSqlServer.ts index 0a07371cd3..1c74e6b1ff 100644 --- a/packages/server/src/integrations/microsoftSqlServer.ts +++ b/packages/server/src/integrations/microsoftSqlServer.ts @@ -281,8 +281,14 @@ class SqlServerIntegration extends Sql implements DatasourcePlus { case MSSQLConfigAuthType.NTLM: { const { domain, trustServerCertificate } = this.config.ntlmConfig || {} + + if (!domain) { + throw Error("Domain must be provided for NTLM config") + } + clientCfg.authentication = { type: "ntlm", + // @ts-expect-error - username and password not required for NTLM options: { domain, }, diff --git a/packages/server/src/integrations/snowflake.ts b/packages/server/src/integrations/snowflake.ts index 9a1dac10e5..838cbb4106 100644 --- a/packages/server/src/integrations/snowflake.ts +++ b/packages/server/src/integrations/snowflake.ts @@ -6,7 +6,8 @@ import { QueryType, SqlQuery, } from "@budibase/types" -import { Snowflake } from "snowflake-promise" +import snowflakeSdk, { SnowflakeError } from "snowflake-sdk" +import { promisify } from "util" interface SnowflakeConfig { account: string @@ -71,11 +72,52 @@ const SCHEMA: Integration = { }, } -class SnowflakeIntegration { - private client: Snowflake +class SnowflakePromise { + config: SnowflakeConfig + client?: snowflakeSdk.Connection constructor(config: SnowflakeConfig) { - this.client = new Snowflake(config) + this.config = config + } + + async connect() { + if (this.client?.isUp()) return + + this.client = snowflakeSdk.createConnection(this.config) + const connectAsync = promisify(this.client.connect.bind(this.client)) + return connectAsync() + } + + async execute(sql: string) { + return new Promise((resolve, reject) => { + if (!this.client) { + throw Error( + "No snowflake client present to execute query. Run connect() first to initialise." + ) + } + + this.client.execute({ + sqlText: sql, + complete: function ( + err: SnowflakeError | undefined, + statementExecuted: any, + rows: any + ) { + if (err) { + return reject(err) + } + resolve(rows) + }, + }) + }) + } +} + +class SnowflakeIntegration { + private client: SnowflakePromise + + constructor(config: SnowflakeConfig) { + this.client = new SnowflakePromise(config) } async testConnection(): Promise { diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts index 6313556df7..dcdaece191 100644 --- a/packages/server/src/integrations/tests/utils/index.ts +++ b/packages/server/src/integrations/tests/utils/index.ts @@ -7,8 +7,10 @@ import * as mssql from "./mssql" import * as mariadb from "./mariadb" import * as oracle from "./oracle" import { testContainerUtils } from "@budibase/backend-core/tests" +import { Knex } from "knex" +import TestConfiguration from "../../../tests/utilities/TestConfiguration" -export type DatasourceProvider = () => Promise +export type DatasourceProvider = () => Promise export const { startContainer } = testContainerUtils @@ -19,6 +21,7 @@ export enum DatabaseName { SQL_SERVER = "mssql", MARIADB = "mariadb", ORACLE = "oracle", + SQS = "sqs", } const providers: Record = { @@ -28,30 +31,130 @@ const providers: Record = { [DatabaseName.SQL_SERVER]: mssql.getDatasource, [DatabaseName.MARIADB]: mariadb.getDatasource, [DatabaseName.ORACLE]: oracle.getDatasource, + [DatabaseName.SQS]: async () => undefined, } -export function getDatasourceProviders( - ...sourceNames: DatabaseName[] -): Promise[] { - return sourceNames.map(sourceName => providers[sourceName]()) +export interface DatasourceDescribeOpts { + only?: DatabaseName[] + exclude?: DatabaseName[] } -export function getDatasourceProvider( +export interface DatasourceDescribeReturnPromise { + rawDatasource: Datasource | undefined + datasource: Datasource | undefined + client: Knex | undefined +} + +export interface DatasourceDescribeReturn { + name: DatabaseName + config: TestConfiguration + dsProvider: () => Promise + isInternal: boolean + isExternal: boolean + isSql: boolean + isMySQL: boolean + isPostgres: boolean + isMongodb: boolean + isMSSQL: boolean + isOracle: boolean +} + +async function createDatasources( + config: TestConfiguration, + name: DatabaseName +): Promise { + await config.init() + + const rawDatasource = await getDatasource(name) + + let datasource: Datasource | undefined + if (rawDatasource) { + datasource = await config.api.datasource.create(rawDatasource) + } + + let client: Knex | undefined + if (rawDatasource) { + try { + client = await knexClient(rawDatasource) + } catch (e) { + // ignore + } + } + + return { + rawDatasource, + datasource, + client, + } +} + +// Jest doesn't allow test files to exist with no tests in them. When we run +// these tests in CI, we break them out by data source, and there are a bunch of +// test files that only run for a subset of data sources, and for the rest of +// them they will be empty test files. Defining a dummy test makes it so that +// Jest doesn't error in this situation. +function createDummyTest() { + describe("no tests", () => { + it("no tests", () => { + // no tests + }) + }) +} + +export function datasourceDescribe(opts: DatasourceDescribeOpts) { + if (process.env.DATASOURCE === "none") { + createDummyTest() + } + + const { only, exclude } = opts + + if (only && exclude) { + throw new Error("you can only supply one of 'only' or 'exclude'") + } + + let databases = Object.values(DatabaseName) + if (only) { + databases = only + } else if (exclude) { + databases = databases.filter(db => !exclude.includes(db)) + } + + if (process.env.DATASOURCE) { + databases = databases.filter(db => db === process.env.DATASOURCE) + } + + if (databases.length === 0) { + createDummyTest() + } + + const config = new TestConfiguration() + return databases.map(dbName => ({ + dbName, + config, + dsProvider: () => createDatasources(config, dbName), + isInternal: dbName === DatabaseName.SQS, + isExternal: dbName !== DatabaseName.SQS, + isSql: [ + DatabaseName.MARIADB, + DatabaseName.MYSQL, + DatabaseName.POSTGRES, + DatabaseName.SQL_SERVER, + DatabaseName.ORACLE, + ].includes(dbName), + isMySQL: dbName === DatabaseName.MYSQL, + isPostgres: dbName === DatabaseName.POSTGRES, + isMongodb: dbName === DatabaseName.MONGODB, + isMSSQL: dbName === DatabaseName.SQL_SERVER, + isOracle: dbName === DatabaseName.ORACLE, + })) +} + +function getDatasource( sourceName: DatabaseName -): DatasourceProvider { - return providers[sourceName] -} - -export function getDatasource(sourceName: DatabaseName): Promise { +): Promise { return providers[sourceName]() } -export async function getDatasources( - ...sourceNames: DatabaseName[] -): Promise { - return Promise.all(sourceNames.map(sourceName => providers[sourceName]())) -} - export async function knexClient(ds: Datasource) { switch (ds.source) { case SourceName.POSTGRES: { diff --git a/packages/server/src/integrations/tests/utils/mariadb.ts b/packages/server/src/integrations/tests/utils/mariadb.ts index 529ac0b76b..8f797a5776 100644 --- a/packages/server/src/integrations/tests/utils/mariadb.ts +++ b/packages/server/src/integrations/tests/utils/mariadb.ts @@ -31,7 +31,7 @@ export async function getDatasource(): Promise { new GenericContainer(MARIADB_IMAGE) .withExposedPorts(3306) .withEnvironment({ MARIADB_ROOT_PASSWORD: "password" }) - .withWaitStrategy(new MariaDBWaitStrategy()) + .withWaitStrategy(new MariaDBWaitStrategy().withStartupTimeout(20000)) ) } diff --git a/packages/server/src/integrations/tests/utils/mongodb.ts b/packages/server/src/integrations/tests/utils/mongodb.ts index a62d895042..1622c831d6 100644 --- a/packages/server/src/integrations/tests/utils/mongodb.ts +++ b/packages/server/src/integrations/tests/utils/mongodb.ts @@ -18,7 +18,7 @@ export async function getDatasource(): Promise { .withWaitStrategy( Wait.forSuccessfulCommand( `mongosh --eval "db.version()"` - ).withStartupTimeout(10000) + ).withStartupTimeout(20000) ) ) } diff --git a/packages/server/src/integrations/tests/utils/mssql.ts b/packages/server/src/integrations/tests/utils/mssql.ts index 709ebb9439..28a7928f3b 100644 --- a/packages/server/src/integrations/tests/utils/mssql.ts +++ b/packages/server/src/integrations/tests/utils/mssql.ts @@ -24,7 +24,7 @@ export async function getDatasource(): Promise { .withWaitStrategy( Wait.forSuccessfulCommand( "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Password_123 -q 'SELECT 1'" - ) + ).withStartupTimeout(20000) ) ) } diff --git a/packages/server/src/integrations/tests/utils/mysql.ts b/packages/server/src/integrations/tests/utils/mysql.ts index 68e591837b..5fa4e4f46d 100644 --- a/packages/server/src/integrations/tests/utils/mysql.ts +++ b/packages/server/src/integrations/tests/utils/mysql.ts @@ -34,7 +34,7 @@ export async function getDatasource(): Promise { new GenericContainer(MYSQL_IMAGE) .withExposedPorts(3306) .withEnvironment({ MYSQL_ROOT_PASSWORD: "password" }) - .withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(10000)) + .withWaitStrategy(new MySQLWaitStrategy().withStartupTimeout(20000)) ) } diff --git a/packages/server/src/integrations/tests/utils/oracle.ts b/packages/server/src/integrations/tests/utils/oracle.ts index 5c788fd130..8e7fd6c900 100644 --- a/packages/server/src/integrations/tests/utils/oracle.ts +++ b/packages/server/src/integrations/tests/utils/oracle.ts @@ -23,7 +23,11 @@ export async function getDatasource(): Promise { .withEnvironment({ ORACLE_PASSWORD: password, }) - .withWaitStrategy(Wait.forLogMessage("DATABASE IS READY TO USE!")) + .withWaitStrategy( + Wait.forLogMessage("DATABASE IS READY TO USE!").withStartupTimeout( + 20000 + ) + ) ) } diff --git a/packages/server/src/integrations/tests/utils/postgres.ts b/packages/server/src/integrations/tests/utils/postgres.ts index bf8d76e39d..cc77226ff6 100644 --- a/packages/server/src/integrations/tests/utils/postgres.ts +++ b/packages/server/src/integrations/tests/utils/postgres.ts @@ -16,7 +16,7 @@ export async function getDatasource(): Promise { .withWaitStrategy( Wait.forSuccessfulCommand( "pg_isready -h localhost -p 5432" - ).withStartupTimeout(10000) + ).withStartupTimeout(20000) ) ) } diff --git a/packages/server/src/middleware/publicApi.ts b/packages/server/src/middleware/publicApi.ts index e3897d1a49..da10dd3539 100644 --- a/packages/server/src/middleware/publicApi.ts +++ b/packages/server/src/middleware/publicApi.ts @@ -1,8 +1,8 @@ import { constants, utils } from "@budibase/backend-core" -import { BBContext } from "@budibase/types" +import { Ctx } from "@budibase/types" export default function ({ requiresAppId }: { requiresAppId?: boolean } = {}) { - return async (ctx: BBContext, next: any) => { + return async (ctx: Ctx, next: any) => { const appId = await utils.getAppIdFromCtx(ctx) if (requiresAppId && !appId) { ctx.throw( diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index 567e7d5cc8..3a582a46ea 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -1,11 +1,8 @@ import { EmptyFilterOption, - FeatureFlag, LegacyFilter, - LogicalOperator, Row, RowSearchParams, - SearchFilterKey, SearchFilters, SearchResponse, SortOrder, @@ -19,7 +16,6 @@ import { ExportRowsParams, ExportRowsResult } from "./search/types" import { dataFilters } from "@budibase/shared-core" import sdk from "../../index" import { checkFilters, searchInputMapping } from "./search/utils" -import { db, features } from "@budibase/backend-core" import tracer from "dd-trace" import { getQueryableFields, removeInvalidFilters } from "./queryUtils" import { enrichSearchContext } from "../../../api/controllers/row/utils" @@ -104,44 +100,14 @@ export async function search( } viewQuery = checkFilters(table, viewQuery) - const sqsEnabled = await features.flags.isEnabled(FeatureFlag.SQS) - const supportsLogicalOperators = - isExternalTableID(view.tableId) || sqsEnabled - - if (!supportsLogicalOperators) { - // In the unlikely event that a Grouped Filter is in a non-SQS environment - // It needs to be ignored entirely - let queryFilters: LegacyFilter[] = Array.isArray(view.query) - ? view.query - : [] - - const { filters } = dataFilters.splitFiltersArray(queryFilters) - - // Extract existing fields - const existingFields = filters.map(filter => - db.removeKeyNumbering(filter.field) - ) - - // Carry over filters for unused fields - Object.keys(options.query).forEach(key => { - const operator = key as Exclude - Object.keys(options.query[operator] || {}).forEach(field => { - if (!existingFields.includes(db.removeKeyNumbering(field))) { - viewQuery[operator]![field] = options.query[operator]![field] - } - }) - }) - options.query = viewQuery - } else { - const conditions = viewQuery ? [viewQuery] : [] - options.query = { - $and: { - conditions: [...conditions, options.query], - }, - } - if (viewQuery.onEmptyFilter) { - options.query.onEmptyFilter = viewQuery.onEmptyFilter - } + const conditions = viewQuery ? [viewQuery] : [] + options.query = { + $and: { + conditions: [...conditions, options.query], + }, + } + if (viewQuery.onEmptyFilter) { + options.query.onEmptyFilter = viewQuery.onEmptyFilter } } @@ -170,12 +136,9 @@ export async function search( if (isExternalTable) { span?.addTags({ searchType: "external" }) result = await external.search(options, source) - } else if (await features.flags.isEnabled(FeatureFlag.SQS)) { + } else { span?.addTags({ searchType: "sqs" }) result = await internal.sqs.search(options, source) - } else { - span?.addTags({ searchType: "lucene" }) - result = await internal.lucene.search(options, source) } span.addTags({ diff --git a/packages/server/src/sdk/app/rows/search/internal/index.ts b/packages/server/src/sdk/app/rows/search/internal/index.ts index f3db9169f4..58d1bd9c96 100644 --- a/packages/server/src/sdk/app/rows/search/internal/index.ts +++ b/packages/server/src/sdk/app/rows/search/internal/index.ts @@ -1,3 +1,2 @@ export * as sqs from "./sqs" -export * as lucene from "./lucene" export * from "./internal" diff --git a/packages/server/src/sdk/app/rows/search/internal/lucene.ts b/packages/server/src/sdk/app/rows/search/internal/lucene.ts deleted file mode 100644 index 953fb90c1f..0000000000 --- a/packages/server/src/sdk/app/rows/search/internal/lucene.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { PROTECTED_INTERNAL_COLUMNS } from "@budibase/shared-core" -import { fullSearch, paginatedSearch } from "../utils" -import { InternalTables } from "../../../../../db/utils" -import { - Row, - RowSearchParams, - SearchResponse, - SortType, - Table, - User, - ViewV2, -} from "@budibase/types" -import { getGlobalUsersFromMetadata } from "../../../../../utilities/global" -import { outputProcessing } from "../../../../../utilities/rowProcessor" -import pick from "lodash/pick" -import sdk from "../../../../" - -export async function search( - options: RowSearchParams, - source: Table | ViewV2 -): Promise> { - let table: Table - if (sdk.views.isView(source)) { - table = await sdk.views.getTable(source.id) - } else { - table = source - } - - const { paginate, query } = options - - const params: RowSearchParams = { - tableId: options.tableId, - viewId: options.viewId, - sort: options.sort, - sortOrder: options.sortOrder, - sortType: options.sortType, - limit: options.limit, - bookmark: options.bookmark, - version: options.version, - disableEscaping: options.disableEscaping, - query: {}, - } - - if (params.sort && !params.sortType) { - const schema = table.schema - const sortField = schema[params.sort] - params.sortType = - sortField.type === "number" ? SortType.NUMBER : SortType.STRING - } - - let response - if (paginate) { - response = await paginatedSearch(query, params) - } else { - response = await fullSearch(query, params) - } - - // Enrich search results with relationships - if (response.rows && response.rows.length) { - // enrich with global users if from users table - if (table._id === InternalTables.USER_METADATA) { - response.rows = await getGlobalUsersFromMetadata(response.rows as User[]) - } - - const visibleFields = - options.fields || - Object.keys(source.schema || {}).filter( - key => source.schema?.[key].visible !== false - ) - const allowedFields = [...visibleFields, ...PROTECTED_INTERNAL_COLUMNS] - response.rows = response.rows.map((r: any) => pick(r, allowedFields)) - - response.rows = await outputProcessing(source, response.rows, { - squash: true, - }) - } - - return response -} diff --git a/packages/server/src/sdk/app/rows/search/tests/search.spec.ts b/packages/server/src/sdk/app/rows/search/tests/search.spec.ts index 4d8a6b6d69..b424c3707d 100644 --- a/packages/server/src/sdk/app/rows/search/tests/search.spec.ts +++ b/packages/server/src/sdk/app/rows/search/tests/search.spec.ts @@ -7,240 +7,218 @@ import { Table, } from "@budibase/types" -import TestConfiguration from "../../../../../tests/utilities/TestConfiguration" import { search } from "../../../../../sdk/app/rows/search" import { generator } from "@budibase/backend-core/tests" -import { features } from "@budibase/backend-core" + import { DatabaseName, - getDatasource, + datasourceDescribe, } from "../../../../../integrations/tests/utils" import { tableForDatasource } from "../../../../../tests/utilities/structures" // These test cases are only for things that cannot be tested through the API // (e.g. limiting searches to returning specific fields). If it's possible to // test through the API, it should be done there instead. -describe.each([ - ["lucene", undefined], - ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], -])("search sdk (%s)", (name, dsProvider) => { - const isSqs = name === "sqs" - const isLucene = name === "lucene" - const isInternal = isLucene || isSqs - const config = new TestConfiguration() +const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] }) - let envCleanup: (() => void) | undefined - let datasource: Datasource | undefined - let table: Table +if (descriptions.length) { + describe.each(descriptions)( + "search sdk ($dbName)", + ({ config, dsProvider, isInternal }) => { + let datasource: Datasource | undefined + let table: Table - beforeAll(async () => { - await features.testutils.withFeatureFlags("*", { SQS: isSqs }, () => - config.init() - ) - - envCleanup = features.testutils.setFeatureFlags("*", { - SQS: isSqs, - }) - - if (dsProvider) { - datasource = await config.createDatasource({ - datasource: await dsProvider, - }) - } - }) - - beforeEach(async () => { - const idFieldSchema: NumberFieldMetadata | AutoColumnFieldMetadata = - isInternal - ? { - name: "id", - type: FieldType.AUTO, - subtype: AutoFieldSubType.AUTO_ID, - autocolumn: true, - } - : { - name: "id", - type: FieldType.NUMBER, - autocolumn: true, - } - - table = await config.api.table.save( - tableForDatasource(datasource, { - primary: ["id"], - schema: { - id: idFieldSchema, - name: { - name: "name", - type: FieldType.STRING, - }, - surname: { - name: "surname", - type: FieldType.STRING, - }, - age: { - name: "age", - type: FieldType.NUMBER, - }, - address: { - name: "address", - type: FieldType.STRING, - }, - }, - }) - ) - - for (let i = 0; i < 10; i++) { - await config.api.row.save(table._id!, { - name: generator.first(), - surname: generator.last(), - age: generator.age(), - address: generator.address(), - }) - } - }) - - afterAll(async () => { - config.end() - if (envCleanup) { - envCleanup() - } - }) - - it("querying by fields will always return data attribute columns", async () => { - await config.doInContext(config.appId, async () => { - const { rows } = await search({ - tableId: table._id!, - query: {}, - fields: ["name", "age"], + beforeAll(async () => { + const ds = await dsProvider() + datasource = ds.datasource }) - expect(rows).toHaveLength(10) - for (const row of rows) { - const keys = Object.keys(row) - expect(keys).toContain("name") - expect(keys).toContain("age") - expect(keys).not.toContain("surname") - expect(keys).not.toContain("address") - } - }) - }) + beforeEach(async () => { + const idFieldSchema: NumberFieldMetadata | AutoColumnFieldMetadata = + isInternal + ? { + name: "id", + type: FieldType.AUTO, + subtype: AutoFieldSubType.AUTO_ID, + autocolumn: true, + } + : { + name: "id", + type: FieldType.NUMBER, + autocolumn: true, + } - !isInternal && - it("will decode _id in oneOf query", async () => { - await config.doInContext(config.appId, async () => { - const result = await search({ - tableId: table._id!, - query: { - oneOf: { - _id: ["%5B1%5D", "%5B4%5D", "%5B8%5D"], + table = await config.api.table.save( + tableForDatasource(datasource, { + primary: ["id"], + schema: { + id: idFieldSchema, + name: { + name: "name", + type: FieldType.STRING, + }, + surname: { + name: "surname", + type: FieldType.STRING, + }, + age: { + name: "age", + type: FieldType.NUMBER, + }, + address: { + name: "address", + type: FieldType.STRING, + }, }, - }, - }) - - expect(result.rows).toHaveLength(3) - expect(result.rows.map(row => row.id)).toEqual( - expect.arrayContaining([1, 4, 8]) + }) ) - }) - }) - it("does not allow accessing hidden fields", async () => { - await config.doInContext(config.appId, async () => { - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - name: { - ...table.schema.name, - visible: true, - }, - age: { - ...table.schema.age, - visible: false, - }, - }, + for (let i = 0; i < 10; i++) { + await config.api.row.save(table._id!, { + name: generator.first(), + surname: generator.last(), + age: generator.age(), + address: generator.address(), + }) + } }) - const result = await search({ - tableId: table._id!, - query: {}, - }) - expect(result.rows).toHaveLength(10) - for (const row of result.rows) { - const keys = Object.keys(row) - expect(keys).toContain("name") - expect(keys).toContain("surname") - expect(keys).toContain("address") - expect(keys).not.toContain("age") - } - }) - }) - it("does not allow accessing hidden fields even if requested", async () => { - await config.doInContext(config.appId, async () => { - await config.api.table.save({ - ...table, - schema: { - ...table.schema, - name: { - ...table.schema.name, - visible: true, - }, - age: { - ...table.schema.age, - visible: false, - }, - }, + afterAll(async () => { + config.end() }) - const result = await search({ - tableId: table._id!, - query: {}, - fields: ["name", "age"], - }) - expect(result.rows).toHaveLength(10) - for (const row of result.rows) { - const keys = Object.keys(row) - expect(keys).toContain("name") - expect(keys).not.toContain("age") - expect(keys).not.toContain("surname") - expect(keys).not.toContain("address") - } - }) - }) - !isLucene && - it.each([ - [["id", "name", "age"], 3], - [["name", "age"], 10], - ])( - "cannot query by non search fields (fields: %s)", - async (queryFields, expectedRows) => { + it("querying by fields will always return data attribute columns", async () => { await config.doInContext(config.appId, async () => { const { rows } = await search({ tableId: table._id!, - query: { - $or: { - conditions: [ - { - $and: { - conditions: [ - { range: { id: { low: 2, high: 4 } } }, - { range: { id: { low: 3, high: 5 } } }, - ], - }, - }, - { equal: { id: 7 } }, - ], - }, - }, - fields: queryFields, + query: {}, + fields: ["name", "age"], }) - expect(rows).toHaveLength(expectedRows) + expect(rows).toHaveLength(10) + for (const row of rows) { + const keys = Object.keys(row) + expect(keys).toContain("name") + expect(keys).toContain("age") + expect(keys).not.toContain("surname") + expect(keys).not.toContain("address") + } }) - } - ) -}) + }) + + !isInternal && + it("will decode _id in oneOf query", async () => { + await config.doInContext(config.appId, async () => { + const result = await search({ + tableId: table._id!, + query: { + oneOf: { + _id: ["%5B1%5D", "%5B4%5D", "%5B8%5D"], + }, + }, + }) + + expect(result.rows).toHaveLength(3) + expect(result.rows.map(row => row.id)).toEqual( + expect.arrayContaining([1, 4, 8]) + ) + }) + }) + + it("does not allow accessing hidden fields", async () => { + await config.doInContext(config.appId, async () => { + await config.api.table.save({ + ...table, + schema: { + ...table.schema, + name: { + ...table.schema.name, + visible: true, + }, + age: { + ...table.schema.age, + visible: false, + }, + }, + }) + const result = await search({ + tableId: table._id!, + query: {}, + }) + expect(result.rows).toHaveLength(10) + for (const row of result.rows) { + const keys = Object.keys(row) + expect(keys).toContain("name") + expect(keys).toContain("surname") + expect(keys).toContain("address") + expect(keys).not.toContain("age") + } + }) + }) + + it("does not allow accessing hidden fields even if requested", async () => { + await config.doInContext(config.appId, async () => { + await config.api.table.save({ + ...table, + schema: { + ...table.schema, + name: { + ...table.schema.name, + visible: true, + }, + age: { + ...table.schema.age, + visible: false, + }, + }, + }) + const result = await search({ + tableId: table._id!, + query: {}, + fields: ["name", "age"], + }) + expect(result.rows).toHaveLength(10) + for (const row of result.rows) { + const keys = Object.keys(row) + expect(keys).toContain("name") + expect(keys).not.toContain("age") + expect(keys).not.toContain("surname") + expect(keys).not.toContain("address") + } + }) + }) + + it.each([ + [["id", "name", "age"], 3], + [["name", "age"], 10], + ])( + "cannot query by non search fields (fields: %s)", + async (queryFields, expectedRows) => { + await config.doInContext(config.appId, async () => { + const { rows } = await search({ + tableId: table._id!, + query: { + $or: { + conditions: [ + { + $and: { + conditions: [ + { range: { id: { low: 2, high: 4 } } }, + { range: { id: { low: 3, high: 5 } } }, + ], + }, + }, + { equal: { id: 7 } }, + ], + }, + }, + fields: queryFields, + }) + + expect(rows).toHaveLength(expectedRows) + }) + } + ) + } + ) +} diff --git a/packages/server/src/sdk/app/tables/getters.ts b/packages/server/src/sdk/app/tables/getters.ts index a8ad606647..1ad82b8e42 100644 --- a/packages/server/src/sdk/app/tables/getters.ts +++ b/packages/server/src/sdk/app/tables/getters.ts @@ -1,4 +1,4 @@ -import { context, features } from "@budibase/backend-core" +import { context } from "@budibase/backend-core" import { getTableParams } from "../../../db/utils" import { breakExternalTableId, @@ -12,7 +12,6 @@ import { TableResponse, TableSourceType, TableViewsResponse, - FeatureFlag, } from "@budibase/types" import datasources from "../datasources" import sdk from "../../../sdk" @@ -49,10 +48,7 @@ export async function processTable(table: Table): Promise
{ type: "table", sourceId: table.sourceId || INTERNAL_TABLE_SOURCE_ID, sourceType: TableSourceType.INTERNAL, - } - const sqsEnabled = await features.flags.isEnabled(FeatureFlag.SQS) - if (sqsEnabled) { - processed.sql = true + sql: true, } return processed } @@ -82,8 +78,11 @@ export async function getAllInternalTables(db?: Database): Promise { } async function getAllExternalTables(): Promise { + // this is all datasources, we'll need to filter out internal const datasources = await sdk.datasources.fetch({ enriched: true }) - const allEntities = datasources.map(datasource => datasource.entities) + const allEntities = datasources + .filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID) + .map(datasource => datasource.entities) let final: Table[] = [] for (let entities of allEntities) { if (entities) { diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 9c111ff079..58537c96ad 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -26,6 +26,7 @@ import { isExternalTableID } from "../../../integrations/utils" import * as internal from "./internal" import * as external from "./external" import sdk from "../../../sdk" +import { ensureQueryUISet } from "./utils" function pickApi(tableId: any) { if (isExternalTableID(tableId)) { @@ -44,6 +45,24 @@ export async function getEnriched(viewId: string): Promise { return pickApi(tableId).getEnriched(viewId) } +export async function getAllEnriched(): Promise { + const tables = await sdk.tables.getAllTables() + let views: ViewV2Enriched[] = [] + for (let table of tables) { + if (!table.views || Object.keys(table.views).length === 0) { + continue + } + const v2Views = Object.values(table.views).filter(isV2) + const enrichedViews = await Promise.all( + v2Views.map(view => + enrichSchema(ensureQueryUISet(view), table.schema, tables) + ) + ) + views = views.concat(enrichedViews) + } + return views +} + export async function getTable(view: string | ViewV2): Promise
{ const viewId = typeof view === "string" ? view : view.id const cached = context.getTableForView(viewId) @@ -333,13 +352,19 @@ export function allowedFields( export async function enrichSchema( view: ViewV2, - tableSchema: TableSchema + tableSchema: TableSchema, + tables?: Table[] ): Promise { async function populateRelTableSchema( tableId: string, viewFields: Record ) { - const relTable = await sdk.tables.getTable(tableId) + let relTable = tables + ? tables?.find(t => t._id === tableId) + : await sdk.tables.getTable(tableId) + if (!relTable) { + throw new Error("Cannot enrich relationship, table not found") + } const result: Record = {} for (const relTableFieldName of Object.keys(relTable.schema)) { const relTableField = relTable.schema[relTableFieldName] diff --git a/packages/server/src/startup/index.ts b/packages/server/src/startup/index.ts index 53c4f884cc..edca64db7d 100644 --- a/packages/server/src/startup/index.ts +++ b/packages/server/src/startup/index.ts @@ -28,6 +28,7 @@ import Koa from "koa" import { Server } from "http" import { AddressInfo } from "net" import fs from "fs" +import bson from "bson" let STARTUP_RAN = false @@ -193,6 +194,10 @@ export async function startup( }) } + if (coreEnv.BSON_BUFFER_SIZE) { + bson.setInternalBufferSize(coreEnv.BSON_BUFFER_SIZE) + } + console.log("Initialising JS runner") jsRunner.init() } diff --git a/packages/server/src/tests/filters/datasource-tests.js b/packages/server/src/tests/filters/datasource-tests.js new file mode 100644 index 0000000000..c82de4a556 --- /dev/null +++ b/packages/server/src/tests/filters/datasource-tests.js @@ -0,0 +1,9 @@ +const { isDatasourceTest } = require(".") + +module.exports = paths => { + return { + filtered: paths + .filter(path => isDatasourceTest(path)) + .map(path => ({ test: path })), + } +} diff --git a/packages/server/src/tests/filters/index.js b/packages/server/src/tests/filters/index.js new file mode 100644 index 0000000000..03d3d44aa5 --- /dev/null +++ b/packages/server/src/tests/filters/index.js @@ -0,0 +1,10 @@ +const fs = require("fs") + +function isDatasourceTest(path) { + const content = fs.readFileSync(path, "utf8") + return content.includes("datasourceDescribe(") +} + +module.exports = { + isDatasourceTest, +} diff --git a/packages/server/src/tests/filters/non-datasource-tests.js b/packages/server/src/tests/filters/non-datasource-tests.js new file mode 100644 index 0000000000..31272f3ec8 --- /dev/null +++ b/packages/server/src/tests/filters/non-datasource-tests.js @@ -0,0 +1,9 @@ +const { isDatasourceTest } = require(".") + +module.exports = paths => { + return { + filtered: paths + .filter(path => !isDatasourceTest(path)) + .map(path => ({ test: path })), + } +} diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts index 9741240f27..7cc57673a0 100644 --- a/packages/server/src/tests/utilities/api/viewV2.ts +++ b/packages/server/src/tests/utilities/api/viewV2.ts @@ -5,6 +5,7 @@ import { SearchViewRowRequest, PaginatedSearchRowResponse, ViewResponseEnriched, + ViewFetchResponseEnriched, } from "@budibase/types" import { Expectations, TestAPI } from "./base" @@ -49,6 +50,12 @@ export class ViewV2API extends TestAPI { .data } + fetch = async (expectations?: Expectations) => { + return await this._get(`/api/v2/views`, { + expectations, + }) + } + search = async ( viewId: string, params?: SearchViewRowRequest, diff --git a/packages/server/src/threads/query.ts b/packages/server/src/threads/query.ts index c7e28d3bf4..facdd20642 100644 --- a/packages/server/src/threads/query.ts +++ b/packages/server/src/threads/query.ts @@ -136,21 +136,23 @@ class QueryRunner { pagination = output.pagination } - // transform as required - if (transformer) { + // We avoid invoking the transformer if it's trivial because there is a cost + // to passing data in and out of the isolate, especially for MongoDB where + // we have to bson serialise/deserialise the data. + const hasTransformer = + transformer != null && + transformer.length > 0 && + transformer.trim() !== "return data" && + transformer.trim() !== "return data;" + + if (transformer && hasTransformer) { transformer = iifeWrapper(transformer) let vm = new IsolatedVM() if (datasource.source === SourceName.MONGODB) { vm = vm.withParsingBson(rows) } - - const ctx = { - data: rows, - params: enrichedParameters, - } - if (transformer != null) { - rows = vm.withContext(ctx, () => vm.execute(transformer!)) - } + const ctx = { data: rows, params: enrichedParameters } + rows = vm.withContext(ctx, () => vm.execute(transformer!)) } // if the request fails we retry once, invalidating the cached value diff --git a/packages/server/src/utilities/fileSystem/processor.ts b/packages/server/src/utilities/fileSystem/processor.ts index a32a7568f4..03fbf4ad0a 100644 --- a/packages/server/src/utilities/fileSystem/processor.ts +++ b/packages/server/src/utilities/fileSystem/processor.ts @@ -1,4 +1,4 @@ -import jimp from "jimp" +import { Jimp } from "jimp" const FORMATS = { IMAGES: ["png", "jpg", "jpeg", "gif", "bmp", "tiff"], @@ -6,8 +6,8 @@ const FORMATS = { function processImage(file: { path: string }) { // this will overwrite the temp file - return jimp.read(file.path).then(img => { - return img.resize(300, jimp.AUTO).write(file.path) + return Jimp.read(file.path).then(img => { + return img.resize({ w: 256 }).write(file.path as `${string}.${string}`) }) } diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 910e9d220f..7d6d537302 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -3,7 +3,6 @@ import { fixAutoColumnSubType, processFormulas } from "./utils" import { cache, context, - features, HTTPError, objectStore, utils, @@ -19,7 +18,6 @@ import { Table, User, ViewV2, - FeatureFlag, } from "@budibase/types" import { cloneDeep } from "lodash/fp" import { @@ -163,33 +161,33 @@ async function processDefaultValues(table: Table, row: Row) { /** * This will coerce a value to the correct types based on the type transform map - * @param row The value to coerce + * @param value The value to coerce * @param type The type fo coerce to * @returns The coerced value */ -export function coerce(row: any, type: string) { +export function coerce(value: unknown, type: string) { // no coercion specified for type, skip it if (!TYPE_TRANSFORM_MAP[type]) { - return row + return value } // eslint-disable-next-line no-prototype-builtins - if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) { + if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(value)) { // @ts-ignore - return TYPE_TRANSFORM_MAP[type][row] + return TYPE_TRANSFORM_MAP[type][value] } else if (TYPE_TRANSFORM_MAP[type].parse) { // @ts-ignore - return TYPE_TRANSFORM_MAP[type].parse(row) + return TYPE_TRANSFORM_MAP[type].parse(value) } - return row + return value } /** * Given an input route this function will apply all the necessary pre-processing to it, such as coercion * of column values or adding auto-column values. - * @param user the user which is performing the input. + * @param userId the ID of the user which is performing the input. * @param row the row which is being created/updated. - * @param table the table which the row is being saved to. + * @param source the table/view which the row is being saved to. * @param opts some input processing options (like disabling auto-column relationships). * @returns the row which has been prepared to be written to the DB. */ @@ -423,45 +421,43 @@ export async function coreOutputProcessing( // remove null properties to match internal API const isExternal = isExternalTableID(table._id!) - if (isExternal || (await features.flags.isEnabled(FeatureFlag.SQS))) { - for (const row of rows) { - for (const key of Object.keys(row)) { - if (row[key] === null) { - delete row[key] - } else if (row[key] && table.schema[key]?.type === FieldType.LINK) { - for (const link of row[key] || []) { - for (const linkKey of Object.keys(link)) { - if (link[linkKey] === null) { - delete link[linkKey] - } + for (const row of rows) { + for (const key of Object.keys(row)) { + if (row[key] === null) { + delete row[key] + } else if (row[key] && table.schema[key]?.type === FieldType.LINK) { + for (const link of row[key] || []) { + for (const linkKey of Object.keys(link)) { + if (link[linkKey] === null) { + delete link[linkKey] } } } } } + } - if (sdk.views.isView(source)) { - // We ensure calculation fields are returned as numbers. During the - // testing of this feature it was discovered that the COUNT operation - // returns a string for MySQL, MariaDB, and Postgres. But given that all - // calculation fields (except ones operating on BIGINTs) should be - // numbers, we blanket make sure of that here. - for (const [name, field] of Object.entries( - helpers.views.calculationFields(source) - )) { - if ("field" in field) { - const targetSchema = table.schema[field.field] - // We don't convert BIGINT fields to floats because we could lose - // precision. - if (targetSchema.type === FieldType.BIGINT) { - continue - } + if (sdk.views.isView(source)) { + // We ensure calculation fields are returned as numbers. During the + // testing of this feature it was discovered that the COUNT operation + // returns a string for MySQL, MariaDB, and Postgres. But given that all + // calculation fields (except ones operating on BIGINTs) should be + // numbers, we blanket make sure of that here. + for (const [name, field] of Object.entries( + helpers.views.calculationFields(source) + )) { + if ("field" in field) { + const targetSchema = table.schema[field.field] + // We don't convert BIGINT fields to floats because we could lose + // precision. + if (targetSchema.type === FieldType.BIGINT) { + continue } + } - for (const row of rows) { - if (typeof row[name] === "string") { - row[name] = parseFloat(row[name]) - } + for (const row of rows) { + if (typeof row[name] === "string") { + row[name] = parseFloat(row[name]) } } } diff --git a/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts b/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts index 8cbe585d90..cd375ecb23 100644 --- a/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts +++ b/packages/server/src/utilities/rowProcessor/tests/outputProcessing.spec.ts @@ -8,7 +8,7 @@ import { } from "@budibase/types" import { outputProcessing } from ".." import { generator, structures } from "@budibase/backend-core/tests" -import { features } from "@budibase/backend-core" + import * as bbReferenceProcessor from "../bbReferenceProcessor" import TestConfiguration from "../../../tests/utilities/TestConfiguration" @@ -21,7 +21,6 @@ jest.mock("../bbReferenceProcessor", (): typeof bbReferenceProcessor => ({ describe("rowProcessor - outputProcessing", () => { const config = new TestConfiguration() - let cleanupFlags: () => void = () => {} beforeAll(async () => { await config.init() @@ -33,11 +32,6 @@ describe("rowProcessor - outputProcessing", () => { beforeEach(() => { jest.resetAllMocks() - cleanupFlags = features.testutils.setFeatureFlags("*", { SQS: true }) - }) - - afterEach(() => { - cleanupFlags() }) const processOutputBBReferenceMock = diff --git a/packages/server/src/utilities/rowProcessor/utils.ts b/packages/server/src/utilities/rowProcessor/utils.ts index 33aba5eb3a..9dbeb8ebb2 100644 --- a/packages/server/src/utilities/rowProcessor/utils.ts +++ b/packages/server/src/utilities/rowProcessor/utils.ts @@ -10,11 +10,13 @@ import { FieldType, OperationFieldTypeEnum, AIOperationEnum, + AIFieldMetadata, } from "@budibase/types" import { OperationFields } from "@budibase/shared-core" import tracer from "dd-trace" import { context } from "@budibase/backend-core" import * as pro from "@budibase/pro" +import { coerce } from "./index" interface FormulaOpts { dynamic?: boolean @@ -67,7 +69,18 @@ export async function processFormulas( continue } + const responseType = schema.responseType const isStatic = schema.formulaType === FormulaType.STATIC + const formula = schema.formula + + // coerce static values + if (isStatic) { + rows.forEach(row => { + if (row[column] && responseType) { + row[column] = coerce(row[column], responseType) + } + }) + } if ( schema.formula == null || @@ -80,12 +93,18 @@ export async function processFormulas( for (let i = 0; i < rows.length; i++) { let row = rows[i] let context = contextRows ? contextRows[i] : row - let formula = schema.formula rows[i] = { ...row, [column]: tracer.trace("processStringSync", {}, span => { span?.addTags({ table_id: table._id, column, static: isStatic }) - return processStringSync(formula, context) + const result = processStringSync(formula, context) + try { + return responseType ? coerce(result, responseType) : result + } catch (err: any) { + // if the coercion fails, we return empty row contents + span?.addTags({ coercionError: err.message }) + return undefined + } }), } } @@ -117,12 +136,13 @@ export async function processAIColumns( continue } + const operation = schema.operation + const aiSchema: AIFieldMetadata = schema const rowUpdates = rows.map((row, i) => { const contextRow = contextRows ? contextRows[i] : row // Check if the type is bindable and pass through HBS if so - const operationField = - OperationFields[schema.operation as AIOperationEnum] + const operationField = OperationFields[operation as AIOperationEnum] for (const key in schema) { const fieldType = operationField[key as keyof typeof operationField] if (fieldType === OperationFieldTypeEnum.BINDABLE_TEXT) { @@ -131,7 +151,10 @@ export async function processAIColumns( } } - const prompt = llm.buildPromptFromAIOperation({ schema, row }) + const prompt = llm.buildPromptFromAIOperation({ + schema: aiSchema, + row, + }) return tracer.trace("processAIColumn", {}, async span => { span?.addTags({ table_id: table._id, column }) diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index d9f84f6a5e..9d633c6455 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.build.json", "compilerOptions": { + "lib": ["es2020", "dom"], "composite": true, "baseUrl": "." }, diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index 15c30800a1..61950fd523 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -527,7 +527,12 @@ export function search>( ): SearchResponse { let result = runQuery(docs, query.query) if (query.sort) { - result = sort(result, query.sort, query.sortOrder || SortOrder.ASCENDING) + result = sort( + result, + query.sort, + query.sortOrder || SortOrder.ASCENDING, + query.sortType + ) } const totalRows = result.length if (query.limit) { diff --git a/packages/string-templates/.npmignore b/packages/string-templates/.npmignore new file mode 100644 index 0000000000..fb547825eb --- /dev/null +++ b/packages/string-templates/.npmignore @@ -0,0 +1,4 @@ +* +!dist/**/* +dist/tsconfig.build.tsbuildinfo +!package.json \ No newline at end of file diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 17f26604a3..7fe0c25679 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -4,7 +4,7 @@ "description": "Handlebars wrapper for Budibase templating.", "main": "dist/bundle.cjs", "module": "dist/bundle.mjs", - "types": "src/index.ts", + "types": "dist/index.d.ts", "license": "MPL-2.0", "exports": { ".": { @@ -12,12 +12,8 @@ "import": "./dist/bundle.mjs" }, "./package.json": "./package.json", - "./iife": "./src/iife.js" + "./iife": "./dist/iife.mjs" }, - "files": [ - "dist", - "src" - ], "scripts": { "build": "tsc --emitDeclarationOnly && rollup -c", "dev": "rollup -cw", diff --git a/packages/string-templates/rollup.config.js b/packages/string-templates/rollup.config.js index ee02c7a14a..b745e97d91 100644 --- a/packages/string-templates/rollup.config.js +++ b/packages/string-templates/rollup.config.js @@ -10,8 +10,8 @@ import inject from "@rollup/plugin-inject" const production = !process.env.ROLLUP_WATCH -const config = (format, outputFile) => ({ - input: "src/index.ts", +const config = (input, outputFile, format) => ({ + input, output: { sourcemap: !production, format, @@ -42,6 +42,7 @@ const config = (format, outputFile) => ({ }) export default [ - config("cjs", "./dist/bundle.cjs"), - config("esm", "./dist/bundle.mjs"), + config("src/index.ts", "./dist/bundle.cjs", "cjs"), + config("src/index.ts", "./dist/bundle.mjs", "esm"), + config("src/iife.ts", "./dist/iife.mjs", "esm"), ] diff --git a/packages/types/src/api/web/app/view.ts b/packages/types/src/api/web/app/view.ts index a6be5e2986..2560f7507f 100644 --- a/packages/types/src/api/web/app/view.ts +++ b/packages/types/src/api/web/app/view.ts @@ -9,6 +9,10 @@ export interface ViewResponseEnriched { data: ViewV2Enriched } +export interface ViewFetchResponseEnriched { + data: ViewV2Enriched[] +} + export interface CreateViewRequest extends Omit {} export interface UpdateViewRequest extends ViewV2 {} diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index b0c5267b37..6b6b38a5cf 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -134,6 +134,12 @@ export const JsonTypes = [ FieldType.ARRAY, ] +export type FormulaResponseType = + | FieldType.STRING + | FieldType.NUMBER + | FieldType.BOOLEAN + | FieldType.DATETIME + export const NumericTypes = [FieldType.NUMBER, FieldType.BIGINT] export function isNumeric(type: FieldType) { diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts index 7e79902a49..771192e2f5 100644 --- a/packages/types/src/documents/app/table/schema.ts +++ b/packages/types/src/documents/app/table/schema.ts @@ -1,6 +1,6 @@ // all added by grid/table when defining the // column size, position and whether it can be viewed -import { FieldType } from "../row" +import { FieldType, FormulaResponseType } from "../row" import { AutoFieldSubType, AutoReason, @@ -115,6 +115,7 @@ export interface FormulaFieldMetadata extends BaseFieldSchema { type: FieldType.FORMULA formula: string formulaType?: FormulaType + responseType?: FormulaResponseType } export interface AIFieldMetadata extends BaseFieldSchema { diff --git a/packages/types/src/documents/app/view.ts b/packages/types/src/documents/app/view.ts index 1212031f24..1170284b15 100644 --- a/packages/types/src/documents/app/view.ts +++ b/packages/types/src/documents/app/view.ts @@ -101,6 +101,10 @@ export interface ViewV2 { schema?: ViewV2Schema } +export interface PublicAPIView extends Omit { + query?: UISearchFilter +} + export type ViewV2Schema = Record export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema diff --git a/packages/types/src/documents/pouch.ts b/packages/types/src/documents/pouch.ts index 6ff851a515..c2ac1599ee 100644 --- a/packages/types/src/documents/pouch.ts +++ b/packages/types/src/documents/pouch.ts @@ -8,7 +8,7 @@ export interface RowValue { export interface RowResponse { id: string key: string - error: string + error?: string value: T doc?: T } diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index b679d6e182..9feecbdb2b 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -12,7 +12,6 @@ import type PouchDB from "pouchdb-find" export enum SearchIndex { ROWS = "rows", - AUDIT = "audit", USER = "user", } @@ -164,8 +163,8 @@ export interface Database { viewName: string, params: DatabaseQueryOpts ): Promise> - destroy(): Promise - compact(): Promise + destroy(): Promise + compact(): Promise // these are all PouchDB related functions that are rarely used - in future // should be replaced by better typed/non-pouch implemented methods dump(stream: Writable, opts?: DatabaseDumpOpts): Promise diff --git a/packages/types/src/sdk/featureFlag.ts b/packages/types/src/sdk/featureFlag.ts index 97d145db6c..64a7362e9f 100644 --- a/packages/types/src/sdk/featureFlag.ts +++ b/packages/types/src/sdk/featureFlag.ts @@ -2,10 +2,9 @@ export enum FeatureFlag { PER_CREATOR_PER_USER_PRICE = "PER_CREATOR_PER_USER_PRICE", PER_CREATOR_PER_USER_PRICE_ALERT = "PER_CREATOR_PER_USER_PRICE_ALERT", AUTOMATION_BRANCHING = "AUTOMATION_BRANCHING", - SQS = "SQS", AI_CUSTOM_CONFIGS = "AI_CUSTOM_CONFIGS", DEFAULT_VALUES = "DEFAULT_VALUES", - ENRICHED_RELATIONSHIPS = "ENRICHED_RELATIONSHIPS", + BUDIBASE_AI = "BUDIBASE_AI", } diff --git a/packages/worker/package.json b/packages/worker/package.json index a65b5ed90f..36c88a9a49 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -16,6 +16,7 @@ "build": "node ../../scripts/build.js", "postbuild": "copyfiles -f ../../yarn.lock ./dist/", "check:types": "tsc -p tsconfig.json --noEmit --paths null --target es2020", + "check:dependencies": "node ../../scripts/depcheck.js", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "run:docker": "node dist/index.js", "debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js", @@ -39,20 +40,24 @@ "dependencies": { "@budibase/backend-core": "0.0.0", "@budibase/pro": "0.0.0", + "@budibase/shared-core": "0.0.0", "@budibase/string-templates": "0.0.0", "@budibase/types": "0.0.0", - "@koa/router": "8.0.8", + "@koa/router": "13.1.0", "@techpass/passport-openidconnect": "0.3.3", "@types/global-agent": "2.1.1", - "aws-sdk": "2.1030.0", + "aws-sdk": "2.1692.0", "bcrypt": "5.1.0", "bcryptjs": "2.4.3", "bull": "4.10.1", - "dd-trace": "5.2.0", + "dd-trace": "5.26.0", "dotenv": "8.6.0", + "email-validator": "^2.0.4", "global-agent": "3.0.0", "ical-generator": "4.1.0", "joi": "17.6.0", + "jsonwebtoken": "9.0.2", + "knex": "2.4.2", "koa": "2.13.4", "koa-body": "4.2.0", "koa-compress": "4.0.1", @@ -69,16 +74,18 @@ "pouchdb": "7.3.0", "pouchdb-all-dbs": "1.1.1", "server-destroy": "1.0.1", - "knex": "2.4.2" + "uuid": "^8.3.2" }, "devDependencies": { + "@jest/types": "^29.6.3", "@swc/core": "1.3.71", "@swc/jest": "0.2.27", "@types/jest": "29.5.5", "@types/jsonwebtoken": "9.0.3", "@types/koa": "2.13.4", - "@types/koa__router": "8.0.8", + "@types/koa__router": "12.0.4", "@types/lodash": "4.14.200", + "@types/node": "^22.9.0", "@types/node-fetch": "2.6.4", "@types/server-destroy": "1.0.1", "@types/supertest": "2.0.14", @@ -87,6 +94,7 @@ "nock": "^13.5.4", "nodemon": "2.0.15", "rimraf": "3.0.2", + "superagent": "^10.1.1", "supertest": "6.3.3", "timekeeper": "2.2.0", "typescript": "5.5.2", diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index fa19948bf5..2479a50d9e 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -40,6 +40,8 @@ import { import { checkAnyUserExists } from "../../../utilities/users" import { isEmailConfigured } from "../../../utilities/email" import { BpmStatusKey, BpmStatusValue, utils } from "@budibase/shared-core" +import emailValidator from "email-validator" +import crypto from "crypto" const MAX_USERS_UPLOAD_LIMIT = 1000 @@ -299,6 +301,10 @@ export const find = async (ctx: any) => { export const tenantUserLookup = async (ctx: any) => { const id = ctx.params.id + // is email, check its valid + if (id.includes("@") && !emailValidator.validate(id)) { + ctx.throw(400, `${id} is not a valid email address to lookup.`) + } const user = await userSdk.core.getFirstPlatformUser(id) if (user) { ctx.body = user diff --git a/packages/worker/src/api/controllers/system/environment.ts b/packages/worker/src/api/controllers/system/environment.ts index 48ab2b586f..18ecc380db 100644 --- a/packages/worker/src/api/controllers/system/environment.ts +++ b/packages/worker/src/api/controllers/system/environment.ts @@ -1,6 +1,6 @@ -import { Ctx, MaintenanceType, FeatureFlag } from "@budibase/types" +import { Ctx, MaintenanceType } from "@budibase/types" import env from "../../../environment" -import { env as coreEnv, db as dbCore, features } from "@budibase/backend-core" +import { env as coreEnv, db as dbCore } from "@budibase/backend-core" import nodeFetch from "node-fetch" import { helpers } from "@budibase/shared-core" @@ -35,10 +35,7 @@ async function isSqsAvailable() { } async function isSqsMissing() { - return ( - (await features.flags.isEnabled(FeatureFlag.SQS)) && - !(await isSqsAvailable()) - ) + return !(await isSqsAvailable()) } export const fetch = async (ctx: Ctx) => { diff --git a/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts b/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts index b540836583..f901925016 100644 --- a/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts +++ b/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts @@ -1,5 +1,5 @@ import { mocks, structures } from "@budibase/backend-core/tests" -import { context, events, features } from "@budibase/backend-core" +import { context, events } from "@budibase/backend-core" import { Event, IdentityType } from "@budibase/types" import { TestConfiguration } from "../../../../tests" @@ -12,19 +12,14 @@ const BASE_IDENTITY = { const USER_AUDIT_LOG_COUNT = 3 const APP_ID = "app_1" -describe.each(["lucene", "sql"])("/api/global/auditlogs (%s)", method => { +describe("/api/global/auditlogs (%s)", () => { const config = new TestConfiguration() - let envCleanup: (() => void) | undefined beforeAll(async () => { - envCleanup = features.testutils.setFeatureFlags("*", { - SQS: method === "sql", - }) await config.beforeAll() }) afterAll(async () => { - envCleanup?.() await config.afterAll() }) diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 010aa4c1a0..0547afab38 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -99,6 +99,7 @@ export default server.listen(parseInt(env.PORT || "4002"), async () => { startupLog = `${startupLog} - environment: "${env.BUDIBASE_ENVIRONMENT}"` } console.log(startupLog) + await initPro() await redis.clients.init() features.init() diff --git a/scripts/depcheck.js b/scripts/depcheck.js new file mode 100755 index 0000000000..a60bd9e580 --- /dev/null +++ b/scripts/depcheck.js @@ -0,0 +1,29 @@ +#!/usr/bin/node + +const depcheck = require("depcheck") + +function filterResults(missing) { + if (missing.src) { + delete missing.src + } + return missing +} + +function printMissing(missing) { + for (let [key, value] of Object.entries(filterResults(missing))) { + console.log(`Package ${key} missing in: ${value.join(", ")}`) + } +} + +depcheck(process.cwd(), { + ignorePatterns: ["dist"], + skipMissing: false, +}).then(results => { + if (Object.values(filterResults(results.missing)).length > 0) { + printMissing(results.missing) + console.error("Missing packages found - stopping.") + process.exit(-1) + } else { + console.log("No missing dependencies.") + } +}) diff --git a/scripts/deploy-camunda.sh b/scripts/deploy-camunda.sh deleted file mode 100755 index 66c505a6f0..0000000000 --- a/scripts/deploy-camunda.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -yarn global add zbctl -export ZEEBE_ADDRESS='localhost:26500' - -cd ../budibase-bpm - -is_camunda_ready() { - if (zbctl --insecure status 2>/dev/null) | grep -q 'Healthy'; then - return 1 - else - return 0 - fi -} - -docker-compose up -d -echo "waiting for Camunda to be ready..." - -while is_camunda_ready -eq 0; do sleep 1; done - -echo "deploy processes..." -for file in src/main/resources/models/*; do - zbctl deploy resource $file --insecure -done - -cd ../budibase/packages/pro -yarn && yarn build - -cd ../account-portal/packages/server -yarn worker:run & cd ../../../.. && yarn dev:accountportal - - - diff --git a/scripts/localdomain.sh b/scripts/localdomain.sh index f13511723d..d32dbcc116 100755 --- a/scripts/localdomain.sh +++ b/scripts/localdomain.sh @@ -5,12 +5,12 @@ domain=$2 if [ "$enable" = "enable" ]; then lerna run env:localdomain:enable -- "$domain" - cd packages/account-portal + cd ../account-portal yarn env:localdomain:enable "$domain" cd - else lerna run env:localdomain:disable - cd packages/account-portal + cd ../account-portal yarn env:localdomain:disable cd - fi \ No newline at end of file diff --git a/scripts/run-affected.js b/scripts/run-affected.js deleted file mode 100755 index 97f79bb463..0000000000 --- a/scripts/run-affected.js +++ /dev/null @@ -1,34 +0,0 @@ -/*** - * Running lerna with since and scope is not working as expected. - * For example, running the command `yarn test --scope=@budibase/worker --since=master`, with changes only on `@budibase/backend-core` will not work as expected, as it does not analyse the dependencies properly. The actual `@budibase/worker` task will not be triggered. - * - * This script is using `lerna ls` to detect all the affected projects from a given commit, and if the scoped package is affected, the actual command will be executed. - * - * The current version of the script only supports a single project in the scope. - */ - -const { execSync } = require("child_process") - -const argv = require("yargs").demandOption(["task", "since", "scope"]).argv - -const { task, since, scope } = argv - -const affectedPackages = execSync( - `yarn --silent nx show projects --affected -t ${task} --base=${since} --json`, - { - encoding: "utf-8", - } -) - -const packages = JSON.parse(affectedPackages) - -const isAffected = packages.includes(scope) - -if (isAffected) { - console.log(`${scope} is affected. Running task "${task}"`) - execSync(`yarn ${task} --scope=${scope}`, { - stdio: "inherit", - }) -} else { - console.log(`${scope} is not affected. Skipping task "${task}"`) -} diff --git a/yarn.lock b/yarn.lock index 73a4e8fa4d..3960a0d5a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,7 +17,7 @@ resolved "https://registry.yarnpkg.com/@adobe/spectrum-css-workflow-icons/-/spectrum-css-workflow-icons-1.2.1.tgz#7e2cb3fcfb5c8b12d7275afafbb6ec44913551b4" integrity sha512-uVgekyBXnOVkxp+CUssjN/gefARtudZC8duEn1vm0lBQFwGRZFlDEzU1QC+aIRWCrD1Z8OgRpmBYlSZ7QS003w== -"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.2.1", "@ampproject/remapping@^2.3.0": +"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.2.1": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== @@ -82,568 +82,615 @@ call-me-maybe "^1.0.1" z-schema "^5.0.1" -"@aws-crypto/crc32@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa" - integrity "sha1-BzAOyiFECcM+P/dpzVaXtX/dOPo= sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==" +"@aws-crypto/crc32@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-5.2.0.tgz#cfcc22570949c98c6689cfcbd2d693d36cdae2e1" + integrity sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg== dependencies: - "@aws-crypto/util" "^3.0.0" + "@aws-crypto/util" "^5.2.0" "@aws-sdk/types" "^3.222.0" - tslib "^1.11.1" + tslib "^2.6.2" -"@aws-crypto/crc32c@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz#016c92da559ef638a84a245eecb75c3e97cb664f" - integrity "sha1-AWyS2lWe9jioSiRe7LdcPpfLZk8= sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==" +"@aws-crypto/crc32c@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz#4e34aab7f419307821509a98b9b08e84e0c1917e" + integrity sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag== dependencies: - "@aws-crypto/util" "^3.0.0" + "@aws-crypto/util" "^5.2.0" "@aws-sdk/types" "^3.222.0" - tslib "^1.11.1" + tslib "^2.6.2" -"@aws-crypto/ie11-detection@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz#640ae66b4ec3395cee6a8e94ebcd9f80c24cd688" - integrity "sha1-ZArma07DOVzuao6U682fgMJM1og= sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==" +"@aws-crypto/sha1-browser@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz#b0ee2d2821d3861f017e965ef3b4cb38e3b6a0f4" + integrity sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg== dependencies: - tslib "^1.11.1" - -"@aws-crypto/sha1-browser@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz#f9083c00782b24714f528b1a1fef2174002266a3" - integrity "sha1-+Qg8AHgrJHFPUosaH+8hdAAiZqM= sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==" - dependencies: - "@aws-crypto/ie11-detection" "^3.0.0" - "@aws-crypto/supports-web-crypto" "^3.0.0" - "@aws-crypto/util" "^3.0.0" + "@aws-crypto/supports-web-crypto" "^5.2.0" + "@aws-crypto/util" "^5.2.0" "@aws-sdk/types" "^3.222.0" "@aws-sdk/util-locate-window" "^3.0.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" -"@aws-crypto/sha256-browser@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz#05f160138ab893f1c6ba5be57cfd108f05827766" - integrity "sha1-BfFgE4q4k/HGulvlfP0QjwWCd2Y= sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==" +"@aws-crypto/sha256-browser@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz#153895ef1dba6f9fce38af550e0ef58988eb649e" + integrity sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw== dependencies: - "@aws-crypto/ie11-detection" "^3.0.0" - "@aws-crypto/sha256-js" "^3.0.0" - "@aws-crypto/supports-web-crypto" "^3.0.0" - "@aws-crypto/util" "^3.0.0" + "@aws-crypto/sha256-js" "^5.2.0" + "@aws-crypto/supports-web-crypto" "^5.2.0" + "@aws-crypto/util" "^5.2.0" "@aws-sdk/types" "^3.222.0" "@aws-sdk/util-locate-window" "^3.0.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" -"@aws-crypto/sha256-js@3.0.0", "@aws-crypto/sha256-js@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz#f06b84d550d25521e60d2a0e2a90139341e007c2" - integrity "sha1-8GuE1VDSVSHmDSoOKpATk0HgB8I= sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==" +"@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz#c4fdb773fdbed9a664fc1a95724e206cf3860042" + integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== dependencies: - "@aws-crypto/util" "^3.0.0" + "@aws-crypto/util" "^5.2.0" "@aws-sdk/types" "^3.222.0" - tslib "^1.11.1" + tslib "^2.6.2" -"@aws-crypto/supports-web-crypto@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz#5d1bf825afa8072af2717c3e455f35cda0103ec2" - integrity "sha1-XRv4Ja+oByrycXw+RV81zaAQPsI= sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==" +"@aws-crypto/supports-web-crypto@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz#a1e399af29269be08e695109aa15da0a07b5b5fb" + integrity sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg== dependencies: - tslib "^1.11.1" + tslib "^2.6.2" -"@aws-crypto/util@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-3.0.0.tgz#1c7ca90c29293f0883468ad48117937f0fe5bfb0" - integrity "sha1-HHypDCkpPwiDRorUgReTfw/lv7A= sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==" +"@aws-crypto/util@5.2.0", "@aws-crypto/util@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" + integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== dependencies: "@aws-sdk/types" "^3.222.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" + "@smithy/util-utf8" "^2.0.0" + tslib "^2.6.2" "@aws-sdk/client-s3@^3.388.0": - version "3.423.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.423.0.tgz#b15fc64db09f1698bf4ad19f6f8e3b57c15e5305" - integrity "sha1-sV/GTbCfFpi/StGfb447V8FeUwU= sha512-Sn/6fotTDGp+uUfPU0JrKszHT/cYwZonly6Ahi4R/uxASLQnOEAF7MwVSjms+/LGu72Qs0Tt7B7RKW76GI4OIA==" + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.693.0.tgz#188b621498ffaeb7b1ea5794f61e3e8d9a4bcac2" + integrity sha512-vgGI2e0Q6pzyhqfrSysi+sk/i+Nl+lMon67oqj/57RcCw9daL1/inpS+ADuwHpiPWkrg+U0bOXnmHjkLeTslJg== dependencies: - "@aws-crypto/sha1-browser" "3.0.0" - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.423.0" - "@aws-sdk/credential-provider-node" "3.423.0" - "@aws-sdk/middleware-bucket-endpoint" "3.418.0" - "@aws-sdk/middleware-expect-continue" "3.418.0" - "@aws-sdk/middleware-flexible-checksums" "3.418.0" - "@aws-sdk/middleware-host-header" "3.418.0" - "@aws-sdk/middleware-location-constraint" "3.418.0" - "@aws-sdk/middleware-logger" "3.418.0" - "@aws-sdk/middleware-recursion-detection" "3.418.0" - "@aws-sdk/middleware-sdk-s3" "3.418.0" - "@aws-sdk/middleware-signing" "3.418.0" - "@aws-sdk/middleware-ssec" "3.418.0" - "@aws-sdk/middleware-user-agent" "3.418.0" - "@aws-sdk/region-config-resolver" "3.418.0" - "@aws-sdk/signature-v4-multi-region" "3.418.0" - "@aws-sdk/types" "3.418.0" - "@aws-sdk/util-endpoints" "3.418.0" - "@aws-sdk/util-user-agent-browser" "3.418.0" - "@aws-sdk/util-user-agent-node" "3.418.0" - "@aws-sdk/xml-builder" "3.310.0" - "@smithy/config-resolver" "^2.0.10" - "@smithy/eventstream-serde-browser" "^2.0.9" - "@smithy/eventstream-serde-config-resolver" "^2.0.9" - "@smithy/eventstream-serde-node" "^2.0.9" - "@smithy/fetch-http-handler" "^2.1.5" - "@smithy/hash-blob-browser" "^2.0.9" - "@smithy/hash-node" "^2.0.9" - "@smithy/hash-stream-node" "^2.0.9" - "@smithy/invalid-dependency" "^2.0.9" - "@smithy/md5-js" "^2.0.9" - "@smithy/middleware-content-length" "^2.0.11" - "@smithy/middleware-endpoint" "^2.0.9" - "@smithy/middleware-retry" "^2.0.12" - "@smithy/middleware-serde" "^2.0.9" - "@smithy/middleware-stack" "^2.0.2" - "@smithy/node-config-provider" "^2.0.12" - "@smithy/node-http-handler" "^2.1.5" - "@smithy/protocol-http" "^3.0.5" - "@smithy/smithy-client" "^2.1.6" - "@smithy/types" "^2.3.3" - "@smithy/url-parser" "^2.0.9" - "@smithy/util-base64" "^2.0.0" - "@smithy/util-body-length-browser" "^2.0.0" - "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.10" - "@smithy/util-defaults-mode-node" "^2.0.12" - "@smithy/util-retry" "^2.0.2" - "@smithy/util-stream" "^2.0.12" - "@smithy/util-utf8" "^2.0.0" - "@smithy/util-waiter" "^2.0.9" - fast-xml-parser "4.2.5" + "@aws-crypto/sha1-browser" "5.2.0" + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/client-sso-oidc" "3.693.0" + "@aws-sdk/client-sts" "3.693.0" + "@aws-sdk/core" "3.693.0" + "@aws-sdk/credential-provider-node" "3.693.0" + "@aws-sdk/middleware-bucket-endpoint" "3.693.0" + "@aws-sdk/middleware-expect-continue" "3.693.0" + "@aws-sdk/middleware-flexible-checksums" "3.693.0" + "@aws-sdk/middleware-host-header" "3.693.0" + "@aws-sdk/middleware-location-constraint" "3.693.0" + "@aws-sdk/middleware-logger" "3.693.0" + "@aws-sdk/middleware-recursion-detection" "3.693.0" + "@aws-sdk/middleware-sdk-s3" "3.693.0" + "@aws-sdk/middleware-ssec" "3.693.0" + "@aws-sdk/middleware-user-agent" "3.693.0" + "@aws-sdk/region-config-resolver" "3.693.0" + "@aws-sdk/signature-v4-multi-region" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@aws-sdk/util-endpoints" "3.693.0" + "@aws-sdk/util-user-agent-browser" "3.693.0" + "@aws-sdk/util-user-agent-node" "3.693.0" + "@aws-sdk/xml-builder" "3.693.0" + "@smithy/config-resolver" "^3.0.11" + "@smithy/core" "^2.5.2" + "@smithy/eventstream-serde-browser" "^3.0.12" + "@smithy/eventstream-serde-config-resolver" "^3.0.9" + "@smithy/eventstream-serde-node" "^3.0.11" + "@smithy/fetch-http-handler" "^4.1.0" + "@smithy/hash-blob-browser" "^3.1.8" + "@smithy/hash-node" "^3.0.9" + "@smithy/hash-stream-node" "^3.1.8" + "@smithy/invalid-dependency" "^3.0.9" + "@smithy/md5-js" "^3.0.9" + "@smithy/middleware-content-length" "^3.0.11" + "@smithy/middleware-endpoint" "^3.2.2" + "@smithy/middleware-retry" "^3.0.26" + "@smithy/middleware-serde" "^3.0.9" + "@smithy/middleware-stack" "^3.0.9" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/node-http-handler" "^3.3.0" + "@smithy/protocol-http" "^4.1.6" + "@smithy/smithy-client" "^3.4.3" + "@smithy/types" "^3.7.0" + "@smithy/url-parser" "^3.0.9" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.26" + "@smithy/util-defaults-mode-node" "^3.0.26" + "@smithy/util-endpoints" "^2.1.5" + "@smithy/util-middleware" "^3.0.9" + "@smithy/util-retry" "^3.0.9" + "@smithy/util-stream" "^3.3.0" + "@smithy/util-utf8" "^3.0.0" + "@smithy/util-waiter" "^3.1.8" + tslib "^2.6.2" + +"@aws-sdk/client-sso-oidc@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz#2fd7f93bd81839f5cd08c5e6e9a578b80572d3c4" + integrity sha512-UEDbYlYtK/e86OOMyFR4zEPyenIxDzO2DRdz3fwVW7RzZ94wfmSwBh/8skzPTuY1G7sI064cjHW0b0QG01Sdtg== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.693.0" + "@aws-sdk/credential-provider-node" "3.693.0" + "@aws-sdk/middleware-host-header" "3.693.0" + "@aws-sdk/middleware-logger" "3.693.0" + "@aws-sdk/middleware-recursion-detection" "3.693.0" + "@aws-sdk/middleware-user-agent" "3.693.0" + "@aws-sdk/region-config-resolver" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@aws-sdk/util-endpoints" "3.693.0" + "@aws-sdk/util-user-agent-browser" "3.693.0" + "@aws-sdk/util-user-agent-node" "3.693.0" + "@smithy/config-resolver" "^3.0.11" + "@smithy/core" "^2.5.2" + "@smithy/fetch-http-handler" "^4.1.0" + "@smithy/hash-node" "^3.0.9" + "@smithy/invalid-dependency" "^3.0.9" + "@smithy/middleware-content-length" "^3.0.11" + "@smithy/middleware-endpoint" "^3.2.2" + "@smithy/middleware-retry" "^3.0.26" + "@smithy/middleware-serde" "^3.0.9" + "@smithy/middleware-stack" "^3.0.9" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/node-http-handler" "^3.3.0" + "@smithy/protocol-http" "^4.1.6" + "@smithy/smithy-client" "^3.4.3" + "@smithy/types" "^3.7.0" + "@smithy/url-parser" "^3.0.9" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.26" + "@smithy/util-defaults-mode-node" "^3.0.26" + "@smithy/util-endpoints" "^2.1.5" + "@smithy/util-middleware" "^3.0.9" + "@smithy/util-retry" "^3.0.9" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-sso@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.693.0.tgz#9cd5e07e57013b8c7980512810d775d7b6f67e36" + integrity sha512-QEynrBC26x6TG9ZMzApR/kZ3lmt4lEIs2D+cHuDxt6fDGzahBUsQFBwJqhizzsM97JJI5YvmJhmihoYjdSSaXA== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.693.0" + "@aws-sdk/middleware-host-header" "3.693.0" + "@aws-sdk/middleware-logger" "3.693.0" + "@aws-sdk/middleware-recursion-detection" "3.693.0" + "@aws-sdk/middleware-user-agent" "3.693.0" + "@aws-sdk/region-config-resolver" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@aws-sdk/util-endpoints" "3.693.0" + "@aws-sdk/util-user-agent-browser" "3.693.0" + "@aws-sdk/util-user-agent-node" "3.693.0" + "@smithy/config-resolver" "^3.0.11" + "@smithy/core" "^2.5.2" + "@smithy/fetch-http-handler" "^4.1.0" + "@smithy/hash-node" "^3.0.9" + "@smithy/invalid-dependency" "^3.0.9" + "@smithy/middleware-content-length" "^3.0.11" + "@smithy/middleware-endpoint" "^3.2.2" + "@smithy/middleware-retry" "^3.0.26" + "@smithy/middleware-serde" "^3.0.9" + "@smithy/middleware-stack" "^3.0.9" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/node-http-handler" "^3.3.0" + "@smithy/protocol-http" "^4.1.6" + "@smithy/smithy-client" "^3.4.3" + "@smithy/types" "^3.7.0" + "@smithy/url-parser" "^3.0.9" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.26" + "@smithy/util-defaults-mode-node" "^3.0.26" + "@smithy/util-endpoints" "^2.1.5" + "@smithy/util-middleware" "^3.0.9" + "@smithy/util-retry" "^3.0.9" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/client-sts@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.693.0.tgz#9e2c418f4850269635632bee4d1a31057c04bcc5" + integrity sha512-4S2y7VEtvdnjJX4JPl4kDQlslxXEZFnC50/UXVUYSt/AMc5A/GgspFNA5FVz4E3Gwpfobbf23hR2NBF8AGvYoQ== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/client-sso-oidc" "3.693.0" + "@aws-sdk/core" "3.693.0" + "@aws-sdk/credential-provider-node" "3.693.0" + "@aws-sdk/middleware-host-header" "3.693.0" + "@aws-sdk/middleware-logger" "3.693.0" + "@aws-sdk/middleware-recursion-detection" "3.693.0" + "@aws-sdk/middleware-user-agent" "3.693.0" + "@aws-sdk/region-config-resolver" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@aws-sdk/util-endpoints" "3.693.0" + "@aws-sdk/util-user-agent-browser" "3.693.0" + "@aws-sdk/util-user-agent-node" "3.693.0" + "@smithy/config-resolver" "^3.0.11" + "@smithy/core" "^2.5.2" + "@smithy/fetch-http-handler" "^4.1.0" + "@smithy/hash-node" "^3.0.9" + "@smithy/invalid-dependency" "^3.0.9" + "@smithy/middleware-content-length" "^3.0.11" + "@smithy/middleware-endpoint" "^3.2.2" + "@smithy/middleware-retry" "^3.0.26" + "@smithy/middleware-serde" "^3.0.9" + "@smithy/middleware-stack" "^3.0.9" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/node-http-handler" "^3.3.0" + "@smithy/protocol-http" "^4.1.6" + "@smithy/smithy-client" "^3.4.3" + "@smithy/types" "^3.7.0" + "@smithy/url-parser" "^3.0.9" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-body-length-node" "^3.0.0" + "@smithy/util-defaults-mode-browser" "^3.0.26" + "@smithy/util-defaults-mode-node" "^3.0.26" + "@smithy/util-endpoints" "^2.1.5" + "@smithy/util-middleware" "^3.0.9" + "@smithy/util-retry" "^3.0.9" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/core@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.693.0.tgz#437969dd740895a59863d737bad14646bc2e1725" + integrity sha512-v6Z/kWmLFqRLDPEwl9hJGhtTgIFHjZugSfF1Yqffdxf4n1AWgtHS7qSegakuMyN5pP4K2tvUD8qHJ+gGe2Bw2A== + dependencies: + "@aws-sdk/types" "3.692.0" + "@smithy/core" "^2.5.2" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/property-provider" "^3.1.9" + "@smithy/protocol-http" "^4.1.6" + "@smithy/signature-v4" "^4.2.2" + "@smithy/smithy-client" "^3.4.3" + "@smithy/types" "^3.7.0" + "@smithy/util-middleware" "^3.0.9" + fast-xml-parser "4.4.1" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-env@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.693.0.tgz#f97feed9809fe2800216943470015fdaaba47c4f" + integrity sha512-hMUZaRSF7+iBKZfBHNLihFs9zvpM1CB8MBOTnTp5NGCVkRYF3SB2LH+Kcippe0ats4qCyB1eEoyQX99rERp2iQ== + dependencies: + "@aws-sdk/core" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/property-provider" "^3.1.9" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-http@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.693.0.tgz#5caad0ac47eded1edeb63f907280580ccfaadba3" + integrity sha512-sL8MvwNJU7ZpD7/d2VVb3by1GknIJUxzTIgYtVkDVA/ojo+KRQSSHxcj0EWWXF5DTSh2Tm+LrEug3y1ZyKHsDA== + dependencies: + "@aws-sdk/core" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/fetch-http-handler" "^4.1.0" + "@smithy/node-http-handler" "^3.3.0" + "@smithy/property-provider" "^3.1.9" + "@smithy/protocol-http" "^4.1.6" + "@smithy/smithy-client" "^3.4.3" + "@smithy/types" "^3.7.0" + "@smithy/util-stream" "^3.3.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-ini@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.693.0.tgz#b4557ac1092657660a15c9bd55e17c27f79ec621" + integrity sha512-kvaa4mXhCCOuW7UQnBhYqYfgWmwy7WSBSDClutwSLPZvgrhYj2l16SD2lN4IfYdxARYMJJ1lFYp3/jJG/9Yk4Q== + dependencies: + "@aws-sdk/core" "3.693.0" + "@aws-sdk/credential-provider-env" "3.693.0" + "@aws-sdk/credential-provider-http" "3.693.0" + "@aws-sdk/credential-provider-process" "3.693.0" + "@aws-sdk/credential-provider-sso" "3.693.0" + "@aws-sdk/credential-provider-web-identity" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/credential-provider-imds" "^3.2.6" + "@smithy/property-provider" "^3.1.9" + "@smithy/shared-ini-file-loader" "^3.1.10" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-node@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.693.0.tgz#c5ceac64a69304d5b4db3fd68473480cafddb4a9" + integrity sha512-42WMsBjTNnjYxYuM3qD/Nq+8b7UdMopUq5OduMDxoM3mFTV6PXMMnfI4Z1TNnR4tYRvPXAnuNltF6xmjKbSJRA== + dependencies: + "@aws-sdk/credential-provider-env" "3.693.0" + "@aws-sdk/credential-provider-http" "3.693.0" + "@aws-sdk/credential-provider-ini" "3.693.0" + "@aws-sdk/credential-provider-process" "3.693.0" + "@aws-sdk/credential-provider-sso" "3.693.0" + "@aws-sdk/credential-provider-web-identity" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/credential-provider-imds" "^3.2.6" + "@smithy/property-provider" "^3.1.9" + "@smithy/shared-ini-file-loader" "^3.1.10" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-process@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.693.0.tgz#e84e945f1a148f06ff697608d5309e73347e5aa9" + integrity sha512-cvxQkrTWHHjeHrPlj7EWXPnFSq8x7vMx+Zn1oTsMpCY445N9KuzjfJTkmNGwU2GT6rSZI9/0MM02aQvl5bBBTQ== + dependencies: + "@aws-sdk/core" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/property-provider" "^3.1.9" + "@smithy/shared-ini-file-loader" "^3.1.10" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-sso@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.693.0.tgz#72767389f533d9d17a14af63daaafcc8368ab43a" + integrity sha512-479UlJxY+BFjj3pJFYUNC0DCMrykuG7wBAXfsvZqQxKUa83DnH5Q1ID/N2hZLkxjGd4ZW0AC3lTOMxFelGzzpQ== + dependencies: + "@aws-sdk/client-sso" "3.693.0" + "@aws-sdk/core" "3.693.0" + "@aws-sdk/token-providers" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/property-provider" "^3.1.9" + "@smithy/shared-ini-file-loader" "^3.1.10" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-web-identity@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.693.0.tgz#b6133b5ef9d3582e36e02e9c66766714ff672a11" + integrity sha512-8LB210Pr6VeCiSb2hIra+sAH4KUBLyGaN50axHtIgufVK8jbKIctTZcVY5TO9Se+1107TsruzeXS7VeqVdJfFA== + dependencies: + "@aws-sdk/core" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/property-provider" "^3.1.9" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-bucket-endpoint@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.693.0.tgz#e4823a40935d34f5e58a4fbc830d8ff92e44fc99" + integrity sha512-cPIa+lxMYiFRHtxKfNIVSFGO6LSgZCk42pu3d7KGwD6hu6vXRD5B2/DD3rPcEH1zgl2j0Kx1oGAV7SRXKHSFag== + dependencies: + "@aws-sdk/types" "3.692.0" + "@aws-sdk/util-arn-parser" "3.693.0" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/protocol-http" "^4.1.6" + "@smithy/types" "^3.7.0" + "@smithy/util-config-provider" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-expect-continue@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.693.0.tgz#d8696cee9ebea1d973d8daf872fd913b41d62cf0" + integrity sha512-MuK/gsJWpHz6Tv0CqTCS+QNOxLa2RfPh1biVCu/uO3l7kA0TjQ/C+tfgKvLXeH103tuDrOVINK+bt2ENmI3SWg== + dependencies: + "@aws-sdk/types" "3.692.0" + "@smithy/protocol-http" "^4.1.6" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-flexible-checksums@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.693.0.tgz#80f07802d98ff33a6899a09c59cf51aab426aaac" + integrity sha512-xkS6zjuE11ob93H9t65kHzphXcUMnN2SmIm2wycUPg+hi8Q6DJA6U2p//6oXkrr9oHy1QvwtllRd7SAd63sFKQ== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@aws-crypto/crc32c" "5.2.0" + "@aws-crypto/util" "5.2.0" + "@aws-sdk/core" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/is-array-buffer" "^3.0.0" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/protocol-http" "^4.1.6" + "@smithy/types" "^3.7.0" + "@smithy/util-middleware" "^3.0.9" + "@smithy/util-stream" "^3.3.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-host-header@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.693.0.tgz#69322909c0792df1e6be7c7fb5e2b6f76090a55c" + integrity sha512-BCki6sAZ5jYwIN/t3ElCiwerHad69ipHwPsDCxJQyeiOnJ8HG+lEpnVIfrnI8A0fLQNSF3Gtx6ahfBpKiv1Oug== + dependencies: + "@aws-sdk/types" "3.692.0" + "@smithy/protocol-http" "^4.1.6" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-location-constraint@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.693.0.tgz#1856eaaad64d41d1f8fa53ced58a6c7cf5eccc6e" + integrity sha512-eDAExTZ9uNIP7vs2JCVCOuWJauGueisBSn+Ovt7UvvuEUp6KOIJqn8oFxWmyUQu2GvbG4OcaTLgbqD95YHTB0Q== + dependencies: + "@aws-sdk/types" "3.692.0" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-logger@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.693.0.tgz#fc10294e6963f8e5d58ba1ededd891e999f544a9" + integrity sha512-dXnXDPr+wIiJ1TLADACI1g9pkSB21KkMIko2u4CJ2JCBoxi5IqeTnVoa6YcC8GdFNVRl+PorZ3Zqfmf1EOTC6w== + dependencies: + "@aws-sdk/types" "3.692.0" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-recursion-detection@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.693.0.tgz#88a8157293775e7116707da26501da4b5e042f51" + integrity sha512-0LDmM+VxXp0u3rG0xQRWD/q6Ubi7G8I44tBPahevD5CaiDZTkmNTrVUf0VEJgVe0iCKBppACMBDkLB0/ETqkFw== + dependencies: + "@aws-sdk/types" "3.692.0" + "@smithy/protocol-http" "^4.1.6" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-sdk-s3@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.693.0.tgz#e0850854d5079f372786b2ccfe85729caa7a49d8" + integrity sha512-5A++RBjJ3guyq5pbYs+Oq5hMlA8CK2OWaHx09cxVfhHWl/RoaY8DXrft4gnhoUEBrrubyMw7r9j7RIMLvS58kg== + dependencies: + "@aws-sdk/core" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@aws-sdk/util-arn-parser" "3.693.0" + "@smithy/core" "^2.5.2" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/protocol-http" "^4.1.6" + "@smithy/signature-v4" "^4.2.2" + "@smithy/smithy-client" "^3.4.3" + "@smithy/types" "^3.7.0" + "@smithy/util-config-provider" "^3.0.0" + "@smithy/util-middleware" "^3.0.9" + "@smithy/util-stream" "^3.3.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-ssec@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.693.0.tgz#2ff779147d188090b3a6cda3ed12ca4085220a73" + integrity sha512-Ro5vzI7SRgEeuoMk3fKqFjGv6mG4c7VsSCDwnkiasmafQFBTPvUIpgmu2FXMHqW/OthvoiOzpSrlJ9Bwlx2f8A== + dependencies: + "@aws-sdk/types" "3.692.0" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-user-agent@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.693.0.tgz#4b55cfab3fc7e671b08e1ea63a98e45a1e13e6a5" + integrity sha512-/KUq/KEpFFbQmNmpp7SpAtFAdViquDfD2W0QcG07zYBfz9MwE2ig48ALynXm5sMpRmnG7sJXjdvPtTsSVPfkiw== + dependencies: + "@aws-sdk/core" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@aws-sdk/util-endpoints" "3.693.0" + "@smithy/core" "^2.5.2" + "@smithy/protocol-http" "^4.1.6" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" + +"@aws-sdk/node-http-handler@^3.374.0": + version "3.374.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.374.0.tgz#8cd58b4d9814713e26034c12eabc119c113a5bc4" + integrity sha512-v1Z6m0wwkf65/tKuhwrtPRqVoOtNkDTRn2MBMtxCwEw+8V8Q+YRFqVgGN+J1n53ktE0G5OYVBux/NHiAjJHReQ== + dependencies: + "@smithy/node-http-handler" "^1.0.2" tslib "^2.5.0" -"@aws-sdk/client-sso@3.423.0": - version "3.423.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.423.0.tgz#99db1f73419443cef544892337a1344aba10abc2" - integrity "sha1-mdsfc0GUQ871RIkjN6E0SroQq8I= sha512-znIufHkwhCIePgaYciIs3x/+BpzR57CZzbCKHR9+oOvGyufEPPpUT5bFLvbwTgfiVkTjuk6sG/ES3U5Bc+xtrA==" +"@aws-sdk/region-config-resolver@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.693.0.tgz#9cde5e99f654c788540acfb2a4218d444e8621c2" + integrity sha512-YLUkMsUY0GLW/nfwlZ69cy1u07EZRmsv8Z9m0qW317/EZaVx59hcvmcvb+W4bFqj5E8YImTjoGfE4cZ0F9mkyw== dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/middleware-host-header" "3.418.0" - "@aws-sdk/middleware-logger" "3.418.0" - "@aws-sdk/middleware-recursion-detection" "3.418.0" - "@aws-sdk/middleware-user-agent" "3.418.0" - "@aws-sdk/region-config-resolver" "3.418.0" - "@aws-sdk/types" "3.418.0" - "@aws-sdk/util-endpoints" "3.418.0" - "@aws-sdk/util-user-agent-browser" "3.418.0" - "@aws-sdk/util-user-agent-node" "3.418.0" - "@smithy/config-resolver" "^2.0.10" - "@smithy/fetch-http-handler" "^2.1.5" - "@smithy/hash-node" "^2.0.9" - "@smithy/invalid-dependency" "^2.0.9" - "@smithy/middleware-content-length" "^2.0.11" - "@smithy/middleware-endpoint" "^2.0.9" - "@smithy/middleware-retry" "^2.0.12" - "@smithy/middleware-serde" "^2.0.9" - "@smithy/middleware-stack" "^2.0.2" - "@smithy/node-config-provider" "^2.0.12" - "@smithy/node-http-handler" "^2.1.5" - "@smithy/protocol-http" "^3.0.5" - "@smithy/smithy-client" "^2.1.6" - "@smithy/types" "^2.3.3" - "@smithy/url-parser" "^2.0.9" - "@smithy/util-base64" "^2.0.0" - "@smithy/util-body-length-browser" "^2.0.0" - "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.10" - "@smithy/util-defaults-mode-node" "^2.0.12" - "@smithy/util-retry" "^2.0.2" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" + "@aws-sdk/types" "3.692.0" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/types" "^3.7.0" + "@smithy/util-config-provider" "^3.0.0" + "@smithy/util-middleware" "^3.0.9" + tslib "^2.6.2" -"@aws-sdk/client-sts@3.423.0": - version "3.423.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.423.0.tgz#530a9cd58baef40cc6bbc6321c6ed93175e0e5b2" - integrity "sha1-Uwqc1Yuu9AzGu8YyHG7ZMXXg5bI= sha512-EcpkKu02QZbRX6dQE0u7a8RgWrn/5riz1qAlKd7rM8FZJpr/D6GGX8ZzWxjgp7pRUgfNvinTmIudDnyQY3v9Mg==" +"@aws-sdk/signature-v4-multi-region@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.693.0.tgz#85bd90bb78be1a98d5a5ca41033cb0703146c2c4" + integrity sha512-s7zbbsoVIriTR4ZGaateKuTqz6ddpazAyHvjk7I9kd+NvGNPiuAI18UdbuiiRI6K5HuYKf1ah6mKWFGPG15/kQ== dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/credential-provider-node" "3.423.0" - "@aws-sdk/middleware-host-header" "3.418.0" - "@aws-sdk/middleware-logger" "3.418.0" - "@aws-sdk/middleware-recursion-detection" "3.418.0" - "@aws-sdk/middleware-sdk-sts" "3.418.0" - "@aws-sdk/middleware-signing" "3.418.0" - "@aws-sdk/middleware-user-agent" "3.418.0" - "@aws-sdk/region-config-resolver" "3.418.0" - "@aws-sdk/types" "3.418.0" - "@aws-sdk/util-endpoints" "3.418.0" - "@aws-sdk/util-user-agent-browser" "3.418.0" - "@aws-sdk/util-user-agent-node" "3.418.0" - "@smithy/config-resolver" "^2.0.10" - "@smithy/fetch-http-handler" "^2.1.5" - "@smithy/hash-node" "^2.0.9" - "@smithy/invalid-dependency" "^2.0.9" - "@smithy/middleware-content-length" "^2.0.11" - "@smithy/middleware-endpoint" "^2.0.9" - "@smithy/middleware-retry" "^2.0.12" - "@smithy/middleware-serde" "^2.0.9" - "@smithy/middleware-stack" "^2.0.2" - "@smithy/node-config-provider" "^2.0.12" - "@smithy/node-http-handler" "^2.1.5" - "@smithy/protocol-http" "^3.0.5" - "@smithy/smithy-client" "^2.1.6" - "@smithy/types" "^2.3.3" - "@smithy/url-parser" "^2.0.9" - "@smithy/util-base64" "^2.0.0" - "@smithy/util-body-length-browser" "^2.0.0" - "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.10" - "@smithy/util-defaults-mode-node" "^2.0.12" - "@smithy/util-retry" "^2.0.2" - "@smithy/util-utf8" "^2.0.0" - fast-xml-parser "4.2.5" - tslib "^2.5.0" + "@aws-sdk/middleware-sdk-s3" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/protocol-http" "^4.1.6" + "@smithy/signature-v4" "^4.2.2" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" -"@aws-sdk/credential-provider-env@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.418.0.tgz#7b14169350d9c14c9f656da06edf46f40a224ed2" - integrity "sha1-exQWk1DZwUyfZW2gbt9G9AoiTtI= sha512-e74sS+x63EZUBO+HaI8zor886YdtmULzwKdctsZp5/37Xho1CVUNtEC+fYa69nigBD9afoiH33I4JggaHgrekQ==" +"@aws-sdk/token-providers@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.693.0.tgz#5ce7d6aa7a3437d4abdc0dca1be47f5158d15c85" + integrity sha512-nDBTJMk1l/YmFULGfRbToOA2wjf+FkQT4dMgYCv+V9uSYsMzQj8A7Tha2dz9yv4vnQgYaEiErQ8d7HVyXcVEoA== dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" + "@aws-sdk/types" "3.692.0" + "@smithy/property-provider" "^3.1.9" + "@smithy/shared-ini-file-loader" "^3.1.10" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" -"@aws-sdk/credential-provider-ini@3.423.0": - version "3.423.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.423.0.tgz#62690a3c49b0223c3d239c8a3d2f2708e967a767" - integrity "sha1-YmkKPEmwIjw9I5yKPS8nCOlnp2c= sha512-7CsFWz8g7dQmblp57XzzxMirO4ClowGZIOwAheBkmk6q1XHbllcHFnbh2kdPyQQ0+JmjDg6waztIc7dY7Ycfvw==" +"@aws-sdk/types@3.692.0", "@aws-sdk/types@^3.222.0": + version "3.692.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.692.0.tgz#c8f6c75b6ad659865b72759796d4d92c1b72069b" + integrity sha512-RpNvzD7zMEhiKgmlxGzyXaEcg2khvM7wd5sSHVapOcrde1awQSOMGI4zKBQ+wy5TnDfrm170ROz/ERLYtrjPZA== dependencies: - "@aws-sdk/credential-provider-env" "3.418.0" - "@aws-sdk/credential-provider-process" "3.418.0" - "@aws-sdk/credential-provider-sso" "3.423.0" - "@aws-sdk/credential-provider-web-identity" "3.418.0" - "@aws-sdk/types" "3.418.0" - "@smithy/credential-provider-imds" "^2.0.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" -"@aws-sdk/credential-provider-node@3.423.0": - version "3.423.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.423.0.tgz#80d05ea89b1a4f245786171ae516c331aa315908" - integrity "sha1-gNBeqJsaTyRXhhca5RbDMaoxWQg= sha512-lygbGJJUnDpgo8OEqdoYd51BKkyBVQ1Catiua/m0aHvL+SCmVrHiYPQPawWYGxpH8X3DXdXa0nd0LkEaevrHRg==" +"@aws-sdk/util-arn-parser@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.693.0.tgz#8dae27eb822ab4f88be28bb3c0fc11f1f13d3948" + integrity sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ== dependencies: - "@aws-sdk/credential-provider-env" "3.418.0" - "@aws-sdk/credential-provider-ini" "3.423.0" - "@aws-sdk/credential-provider-process" "3.418.0" - "@aws-sdk/credential-provider-sso" "3.423.0" - "@aws-sdk/credential-provider-web-identity" "3.418.0" - "@aws-sdk/types" "3.418.0" - "@smithy/credential-provider-imds" "^2.0.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" + tslib "^2.6.2" -"@aws-sdk/credential-provider-process@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.418.0.tgz#1cb6d816bd471db3f9724715b007035ef18b5b2b" - integrity "sha1-HLbYFr1HHbP5ckcVsAcDXvGLWys= sha512-xPbdm2WKz1oH6pTkrJoUmr3OLuqvvcPYTQX0IIlc31tmDwDWPQjXGGFD/vwZGIZIkKaFpFxVMgAzfFScxox7dw==" +"@aws-sdk/util-endpoints@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.693.0.tgz#99f56f83fc25bdc3321f5871d6354abd56768891" + integrity sha512-eo4F6DRQ/kxS3gxJpLRv+aDNy76DxQJL5B3DPzpr9Vkq0ygVoi4GT5oIZLVaAVIJmi6k5qq9dLsYZfWLUxJJSg== dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/credential-provider-sso@3.423.0": - version "3.423.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.423.0.tgz#a04f1715e5d9c75370d17ceac645379ca57cbb0b" - integrity "sha1-oE8XFeXZx1Nw0XzqxkU3nKV8uws= sha512-zAH68IjRMmW22USbsCVQ5Q6AHqhmWABwLbZAMocSGMasddTGv/nkA/nUiVCJ/B4LI3P81FoPQVrG5JxNmkNH0w==" - dependencies: - "@aws-sdk/client-sso" "3.423.0" - "@aws-sdk/token-providers" "3.418.0" - "@aws-sdk/types" "3.418.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/credential-provider-web-identity@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.418.0.tgz#c2aed2a79bf193c1fef2b98391aaa9de7336aaaf" - integrity "sha1-wq7Sp5vxk8H+8rmDkaqp3nM2qq8= sha512-do7ang565n9p3dS1JdsQY01rUfRx8vkxQqz5M8OlcEHBNiCdi2PvSjNwcBdrv/FKkyIxZb0TImOfBSt40hVdxQ==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-bucket-endpoint@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.418.0.tgz#1c330fb4dd583454872db7eba3b6e06c0699d59d" - integrity "sha1-HDMPtN1YNFSHLbfro7bgbAaZ1Z0= sha512-gj/mj1UfbKkGbQ1N4YUvjTTp8BVs5fO1QAL2AjFJ+jfJOToLReX72aNEkm7sPGbHML0TqOY4cQbJuWYy+zdD5g==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@aws-sdk/util-arn-parser" "3.310.0" - "@smithy/node-config-provider" "^2.0.12" - "@smithy/protocol-http" "^3.0.5" - "@smithy/types" "^2.3.3" - "@smithy/util-config-provider" "^2.0.0" - tslib "^2.5.0" - -"@aws-sdk/middleware-expect-continue@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.418.0.tgz#b621c6a8bc281f23bfd3791eaab25f687946d4a7" - integrity "sha1-tiHGqLwoHyO/03keqrJfaHlG1Kc= sha512-6x4rcIj685EmqDLQkbWoCur3Dg5DRClHMen6nHXmD3CR5Xyt3z1Gk/+jmZICxyJo9c6M4AeZht8o95BopkmYAQ==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-flexible-checksums@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.418.0.tgz#a79f44739ec918d8947294d0acc52eb7eb358773" - integrity "sha1-p59Ec57JGNiUcpTQrMUut+s1h3M= sha512-3O203dqS2JU5P1TAAbo7p1qplXQh59pevw9nqzPVb3EG8B+mSucVf2kKmF7kGHqKSk+nK/mB/4XGSsZBzGt6Wg==" - dependencies: - "@aws-crypto/crc32" "3.0.0" - "@aws-crypto/crc32c" "3.0.0" - "@aws-sdk/types" "3.418.0" - "@smithy/is-array-buffer" "^2.0.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/types" "^2.3.3" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - -"@aws-sdk/middleware-host-header@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.418.0.tgz#35d682e14f36c9d9d7464c7c1dd582bf6611436d" - integrity "sha1-NdaC4U82ydnXRkx8HdWCv2YRQ20= sha512-LrMTdzalkPw/1ujLCKPLwCGvPMCmT4P+vOZQRbSEVZPnlZk+Aj++aL/RaHou0jL4kJH3zl8iQepriBt4a7UvXQ==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-location-constraint@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.418.0.tgz#e62e213a72ce583ba6135db51dcc60d07825b8ee" - integrity "sha1-5i4hOnLOWDumE121Hcxg0HgluO4= sha512-cc8M3VEaESHJhDsDV8tTpt2QYUprDWhvAVVSlcL43cTdZ54Quc0W+toDiaVOUlwrAZz2Y7g5NDj22ibJGFbOvw==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-logger@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.418.0.tgz#08d7419f4220c36032a070a7dbb8bbf7e744a9ce" - integrity "sha1-CNdBn0Igw2AyoHCn27i79+dEqc4= sha512-StKGmyPVfoO/wdNTtKemYwoJsqIl4l7oqarQY7VSf2Mp3mqaa+njLViHsQbirYpyqpgUEusOnuTlH5utxJ1NsQ==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-recursion-detection@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.418.0.tgz#2bb80d084f946846ad4907f3d6e0b451787d62b1" - integrity "sha1-K7gNCE+UaEatSQfz1uC0UXh9YrE= sha512-kKFrIQglBLUFPbHSDy1+bbe3Na2Kd70JSUC3QLMbUHmqipXN8KeXRfAj7vTv97zXl0WzG0buV++WcNwOm1rFjg==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-sdk-s3@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.418.0.tgz#b1de52d54e0cbc8d46ce0bc4c6c54b527f409aaf" - integrity "sha1-sd5S1U4MvI1GzgvExsVLUn9Amq8= sha512-rei32LF45SyqL3NlWDjEOfMwAca9A5F4QgUyXJqvASc43oWC1tJnLIhiCxNh8qkWAiRyRzFpcanTeqyaRSsZpA==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@aws-sdk/util-arn-parser" "3.310.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/smithy-client" "^2.1.6" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-sdk-sts@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.418.0.tgz#f167f16050e055282ddd60226a2216c84873d464" - integrity "sha1-8WfxYFDgVSgt3WAiaiIWyEhz1GQ= sha512-cW8ijrCTP+mgihvcq4+TbhAcE/we5lFl4ydRqvTdtcSnYQAVQADg47rnTScQiFsPFEB3NKq7BGeyTJF9MKolPA==" - dependencies: - "@aws-sdk/middleware-signing" "3.418.0" - "@aws-sdk/types" "3.418.0" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-signing@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.418.0.tgz#c7242b84069067bb671cb4191d412b59713a375e" - integrity "sha1-xyQrhAaQZ7tnHLQZHUErWXE6N14= sha512-onvs5KoYQE8OlOE740RxWBGtsUyVIgAo0CzRKOQO63ZEYqpL1Os+MS1CGzdNhvQnJgJruE1WW+Ix8fjN30zKPA==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/property-provider" "^2.0.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/signature-v4" "^2.0.0" - "@smithy/types" "^2.3.3" - "@smithy/util-middleware" "^2.0.2" - tslib "^2.5.0" - -"@aws-sdk/middleware-ssec@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.418.0.tgz#67b554c4acad81c7aa93421c8fcba8a18e138294" - integrity "sha1-Z7VUxKytgceqk0Icj8uooY4TgpQ= sha512-J7K+5h6aP7IYMlu/NwHEIjb0+WDu1eFvO8TCPo6j1H9xYRi8B/6h+6pa9Rk9IgRUzFnrdlDu9FazG8Tp0KKLyg==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/middleware-user-agent@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.418.0.tgz#37426cf801332165fb170b1fd62dea8bb967a1ef" - integrity "sha1-N0Js+AEzIWX7Fwsf1i3qi7lnoe8= sha512-Jdcztg9Tal9SEAL0dKRrnpKrm6LFlWmAhvuwv0dQ7bNTJxIxyEFbpqdgy7mpQHsLVZgq1Aad/7gT/72c9igyZw==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@aws-sdk/util-endpoints" "3.418.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/region-config-resolver@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.418.0.tgz#53b99e4bd92f3369f51e9a76534b7d884db67526" - integrity "sha1-U7meS9kvM2n1Hpp2U0t9iE22dSY= sha512-lJRZ/9TjZU6yLz+mAwxJkcJZ6BmyYoIJVo1p5+BN//EFdEmC8/c0c9gXMRzfISV/mqWSttdtccpAyN4/goHTYA==" - dependencies: - "@smithy/node-config-provider" "^2.0.12" - "@smithy/types" "^2.3.3" - "@smithy/util-config-provider" "^2.0.0" - "@smithy/util-middleware" "^2.0.2" - tslib "^2.5.0" - -"@aws-sdk/signature-v4-multi-region@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.418.0.tgz#984c8fc948c61a7ad02f1ccc6c2ddecf43a265b1" - integrity "sha1-mEyPyUjGGnrQLxzMbC3ez0OiZbE= sha512-LeVYMZeUQUURFqDf4yZxTEv016g64hi0LqYBjU0mjwd8aPc0k6hckwvshezc80jCNbuLyjNfQclvlg3iFliItQ==" - dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/signature-v4" "^2.0.0" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/token-providers@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.418.0.tgz#cbfac922df397e72daf6dbdd8c1e9a140df0aa0e" - integrity "sha1-y/rJIt85fnLa9tvdjB6aFA3wqg4= sha512-9P7Q0VN0hEzTngy3Sz5eya2qEOEf0Q8qf1vB3um0gE6ID6EVAdz/nc/DztfN32MFxk8FeVBrCP5vWdoOzmd72g==" - dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/middleware-host-header" "3.418.0" - "@aws-sdk/middleware-logger" "3.418.0" - "@aws-sdk/middleware-recursion-detection" "3.418.0" - "@aws-sdk/middleware-user-agent" "3.418.0" - "@aws-sdk/types" "3.418.0" - "@aws-sdk/util-endpoints" "3.418.0" - "@aws-sdk/util-user-agent-browser" "3.418.0" - "@aws-sdk/util-user-agent-node" "3.418.0" - "@smithy/config-resolver" "^2.0.10" - "@smithy/fetch-http-handler" "^2.1.5" - "@smithy/hash-node" "^2.0.9" - "@smithy/invalid-dependency" "^2.0.9" - "@smithy/middleware-content-length" "^2.0.11" - "@smithy/middleware-endpoint" "^2.0.9" - "@smithy/middleware-retry" "^2.0.12" - "@smithy/middleware-serde" "^2.0.9" - "@smithy/middleware-stack" "^2.0.2" - "@smithy/node-config-provider" "^2.0.12" - "@smithy/node-http-handler" "^2.1.5" - "@smithy/property-provider" "^2.0.0" - "@smithy/protocol-http" "^3.0.5" - "@smithy/shared-ini-file-loader" "^2.0.6" - "@smithy/smithy-client" "^2.1.6" - "@smithy/types" "^2.3.3" - "@smithy/url-parser" "^2.0.9" - "@smithy/util-base64" "^2.0.0" - "@smithy/util-body-length-browser" "^2.0.0" - "@smithy/util-body-length-node" "^2.1.0" - "@smithy/util-defaults-mode-browser" "^2.0.10" - "@smithy/util-defaults-mode-node" "^2.0.12" - "@smithy/util-retry" "^2.0.2" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - -"@aws-sdk/types@3.418.0", "@aws-sdk/types@^3.222.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.418.0.tgz#c23213110b0c313d5546c810da032a441682f49a" - integrity "sha1-wjITEQsMMT1VRsgQ2gMqRBaC9Jo= sha512-y4PQSH+ulfFLY0+FYkaK4qbIaQI9IJNMO2xsxukW6/aNoApNymN1D2FSi2la8Qbp/iPjNDKsG8suNPm9NtsWXQ==" - dependencies: - "@smithy/types" "^2.3.3" - tslib "^2.5.0" - -"@aws-sdk/util-arn-parser@3.310.0": - version "3.310.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz#861ff8810851be52a320ec9e4786f15b5fc74fba" - integrity "sha1-hh/4gQhRvlKjIOyeR4bxW1/HT7o= sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==" - dependencies: - tslib "^2.5.0" - -"@aws-sdk/util-endpoints@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.418.0.tgz#462c976f054fe260562d4d2844152a04dd883fd7" - integrity "sha1-RiyXbwVP4mBWLU0oRBUqBN2IP9c= sha512-sYSDwRTl7yE7LhHkPzemGzmIXFVHSsi3AQ1KeNEk84eBqxMHHcCc2kqklaBk2roXWe50QDgRMy1ikZUxvtzNHQ==" - dependencies: - "@aws-sdk/types" "3.418.0" - tslib "^2.5.0" + "@aws-sdk/types" "3.692.0" + "@smithy/types" "^3.7.0" + "@smithy/util-endpoints" "^2.1.5" + tslib "^2.6.2" "@aws-sdk/util-locate-window@^3.0.0": - version "3.310.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz#b071baf050301adee89051032bd4139bba32cc40" - integrity "sha1-sHG68FAwGt7okFEDK9QTm7oyzEA= sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==" + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.693.0.tgz#1160f6d055cf074ca198eb8ecf89b6311537ad6c" + integrity sha512-ttrag6haJLWABhLqtg1Uf+4LgHWIMOVSYL+VYZmAp2v4PUGOwWmWQH0Zk8RM7YuQcLfH/EoR72/Yxz6A4FKcuw== dependencies: - tslib "^2.5.0" + tslib "^2.6.2" -"@aws-sdk/util-user-agent-browser@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.418.0.tgz#dc76b8e7e5cae3f827d68cd4a3ee30c0d475a39c" - integrity "sha1-3Ha45+XK4/gn1ozUo+4wwNR1o5w= sha512-c4p4mc0VV/jIeNH0lsXzhJ1MpWRLuboGtNEpqE4s1Vl9ck2amv9VdUUZUmHbg+bVxlMgRQ4nmiovA4qIrqGuyg==" +"@aws-sdk/util-user-agent-browser@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.693.0.tgz#c6969be97e7cd0190b3b72a82a642b29ff4659c4" + integrity sha512-6EUfuKOujtddy18OLJUaXfKBgs+UcbZ6N/3QV4iOkubCUdeM1maIqs++B9bhCbWeaeF5ORizJw5FTwnyNjE/mw== dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/types" "^2.3.3" + "@aws-sdk/types" "3.692.0" + "@smithy/types" "^3.7.0" bowser "^2.11.0" - tslib "^2.5.0" + tslib "^2.6.2" -"@aws-sdk/util-user-agent-node@3.418.0": - version "3.418.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.418.0.tgz#7d5a1c82ce3265ff0f70b13d58d08593113ab99a" - integrity "sha1-fVocgs4yZf8PcLE9WNCFkxE6uZo= sha512-BXMskXFtg+dmzSCgmnWOffokxIbPr1lFqa1D9kvM3l3IFRiFGx2IyDg+8MAhq11aPDLvoa/BDuQ0Yqma5izOhg==" +"@aws-sdk/util-user-agent-node@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.693.0.tgz#b26c806faa2001d4fa1d515b146eeff411513dd9" + integrity sha512-td0OVX8m5ZKiXtecIDuzY3Y3UZIzvxEr57Hp21NOwieqKCG2UeyQWWeGPv0FQaU7dpTkvFmVNI+tx9iB8V/Nhg== dependencies: - "@aws-sdk/types" "3.418.0" - "@smithy/node-config-provider" "^2.0.12" - "@smithy/types" "^2.3.3" - tslib "^2.5.0" + "@aws-sdk/middleware-user-agent" "3.693.0" + "@aws-sdk/types" "3.692.0" + "@smithy/node-config-provider" "^3.1.10" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" -"@aws-sdk/util-utf8-browser@^3.0.0": - version "3.259.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff" - integrity "sha1-MnWm9eszT5bKdmNblh08UCWf2f8= sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==" +"@aws-sdk/xml-builder@3.693.0": + version "3.693.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.693.0.tgz#709a46a3335b71144d9f7917a7cb3033b5a04e82" + integrity sha512-C/rPwJcqnV8VDr2/VtcQnymSpcfEEgH1Jm6V0VmfXNZFv4Qzf1eCS8nsec0gipYgZB+cBBjfXw5dAk6pJ8ubpw== dependencies: - tslib "^2.3.1" - -"@aws-sdk/xml-builder@3.310.0": - version "3.310.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.310.0.tgz#f0236f2103b438d16117e0939a6305ad69b7ff76" - integrity "sha1-8CNvIQO0ONFhF+CTmmMFrWm3/3Y= sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==" - dependencies: - tslib "^2.5.0" - -"@aws/dynamodb-auto-marshaller@^0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@aws/dynamodb-auto-marshaller/-/dynamodb-auto-marshaller-0.7.1.tgz#70676c056e4ecb798c08ec2e398a3d93e703858d" - integrity sha512-LeURlf6/avrfFo9+4Yht9J3CUTJ72yoBpm1FOUmlexuHNW4Ka61tG30w3ZDCXXXmCO2rG0k3ywAgNJEo3WPbyw== - dependencies: - tslib "^1.8.1" + "@smithy/types" "^3.7.0" + tslib "^2.6.2" "@azure/abort-controller@^1.0.0", "@azure/abort-controller@^1.0.4": version "1.1.0" @@ -659,7 +706,7 @@ dependencies: tslib "^2.6.2" -"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0", "@azure/core-auth@^1.5.0": +"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.5.0.tgz#a41848c5c31cb3b7c84c409885267d55a2c92e44" integrity sha512-udzoBuYG1VBoHVohDTrvKjyzel34zt77Bhp7dQntVGGD0ehVq48owENbBG8fIgkHRNUBQH5k1r0hpoMu5L8+kw== @@ -668,6 +715,15 @@ "@azure/core-util" "^1.1.0" tslib "^2.2.0" +"@azure/core-auth@^1.5.0", "@azure/core-auth@^1.7.2", "@azure/core-auth@^1.8.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.9.0.tgz#ac725b03fabe3c892371065ee9e2041bee0fd1ac" + integrity sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-util" "^1.11.0" + tslib "^2.6.2" + "@azure/core-client@^1.3.0", "@azure/core-client@^1.4.0", "@azure/core-client@^1.5.0": version "1.9.2" resolved "https://registry.yarnpkg.com/@azure/core-client/-/core-client-1.9.2.tgz#6fc69cee2816883ab6c5cdd653ee4f2ff9774f74" @@ -691,9 +747,9 @@ "@azure/core-rest-pipeline" "^1.3.0" "@azure/core-http@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@azure/core-http/-/core-http-3.0.0.tgz#345845f9ba479a5ee41efc3fd7a13e82d2a0ec47" - integrity sha512-BxI2SlGFPPz6J1XyZNIVUf0QZLBKFX+ViFjKOkzqD18J1zOINIQ8JSBKKr+i+v8+MB6LacL6Nn/sP/TE13+s2Q== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@azure/core-http/-/core-http-3.0.4.tgz#024b2909bbc0f2fce08c74f97a21312c4f42e922" + integrity sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ== dependencies: "@azure/abort-controller" "^1.0.0" "@azure/core-auth" "^1.3.0" @@ -708,7 +764,7 @@ tslib "^2.2.0" tunnel "^0.0.6" uuid "^8.3.0" - xml2js "^0.4.19" + xml2js "^0.5.0" "@azure/core-lro@^2.2.0": version "2.5.1" @@ -726,7 +782,21 @@ dependencies: tslib "^2.2.0" -"@azure/core-rest-pipeline@^1.1.0", "@azure/core-rest-pipeline@^1.3.0", "@azure/core-rest-pipeline@^1.8.0", "@azure/core-rest-pipeline@^1.9.1": +"@azure/core-rest-pipeline@^1.1.0": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz#165f1cd9bb1060be3b6895742db3d1f1106271d3" + integrity sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q== + dependencies: + "@azure/abort-controller" "^2.0.0" + "@azure/core-auth" "^1.8.0" + "@azure/core-tracing" "^1.0.1" + "@azure/core-util" "^1.11.0" + "@azure/logger" "^1.0.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.0" + tslib "^2.6.2" + +"@azure/core-rest-pipeline@^1.3.0", "@azure/core-rest-pipeline@^1.8.0", "@azure/core-rest-pipeline@^1.9.1": version "1.11.0" resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.11.0.tgz#fc0e8f56caac08a9d4ac91c07a6c5a360ea31c82" integrity sha512-nB4KXl6qAyJmBVLWA7SakT4tzpYZTCk4pvRBeI+Ye0WYSOrlTqlMhc4MSS/8atD3ufeYWdkN380LLoXlUUzThw== @@ -756,7 +826,7 @@ dependencies: tslib "^2.2.0" -"@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.1.1", "@azure/core-util@^1.3.0", "@azure/core-util@^1.6.1": +"@azure/core-util@^1.0.0", "@azure/core-util@^1.1.0", "@azure/core-util@^1.3.0", "@azure/core-util@^1.6.1": version "1.6.1" resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.6.1.tgz#fea221c4fa43c26543bccf799beb30c1c7878f5a" integrity sha512-h5taHeySlsV9qxuK64KZxy4iln1BtMYlNt5jbuEFN3UFSAd1EwKg/Gjl5a6tZ/W8t6li3xPnutOx7zbDyXnPmQ== @@ -764,7 +834,15 @@ "@azure/abort-controller" "^1.0.0" tslib "^2.2.0" -"@azure/identity@4.2.1", "@azure/identity@^3.4.1": +"@azure/core-util@^1.1.1", "@azure/core-util@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@azure/core-util/-/core-util-1.11.0.tgz#f530fc67e738aea872fbdd1cc8416e70219fada7" + integrity sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g== + dependencies: + "@azure/abort-controller" "^2.0.0" + tslib "^2.6.2" + +"@azure/identity@4.2.1", "@azure/identity@^4.2.1": version "4.2.1" resolved "https://registry.yarnpkg.com/@azure/identity/-/identity-4.2.1.tgz#22b366201e989b7b41c0e1690e103bd579c31e4c" integrity sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q== @@ -809,30 +887,30 @@ tslib "^2.2.0" "@azure/msal-browser@^3.11.1": - version "3.24.0" - resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.24.0.tgz#3208047672d0b0c943b0bef5f995d510d6582ae4" - integrity sha512-JGNV9hTYAa7lsum9IMIibn2kKczAojNihGo1hi7pG0kNrcKej530Fl6jxwM05A44/6I079CSn6WxYxbVhKUmWg== + version "3.27.0" + resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.27.0.tgz#b6f02f73c8e102d3f115009b4677539fb173fe2b" + integrity sha512-+b4ZKSD8+vslCtVRVetkegEhOFMLP3rxDWJY212ct+2r6jVg6OSQKc1Qz3kCoXo0FgwaXkb+76TMZfpHp8QtgA== dependencies: - "@azure/msal-common" "14.15.0" + "@azure/msal-common" "14.16.0" -"@azure/msal-common@14.15.0": - version "14.15.0" - resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.15.0.tgz#0e27ac0bb88fe100f4f8d1605b64d5c268636a55" - integrity sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ== +"@azure/msal-common@14.16.0": + version "14.16.0" + resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.16.0.tgz#f3470fcaec788dbe50859952cd499340bda23d7a" + integrity sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA== -"@azure/msal-node@^2.9.2": - version "2.14.0" - resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.14.0.tgz#7881895d41b03d8b9b38a29550ba3bbb15f73b3c" - integrity sha512-rrfzIpG3Q1rHjVYZmHAEDidWAZZ2cgkxlIcMQ8dHebRISaZ2KCV33Q8Vs+uaV6lxweROabNxKFlR2lIKagZqYg== +"@azure/msal-node@^2.5.1", "@azure/msal-node@^2.9.2": + version "2.16.2" + resolved "https://registry.yarnpkg.com/@azure/msal-node/-/msal-node-2.16.2.tgz#3eb768d36883ea6f9a939c0b5b467b518e78fffc" + integrity sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ== dependencies: - "@azure/msal-common" "14.15.0" + "@azure/msal-common" "14.16.0" jsonwebtoken "^9.0.0" uuid "^8.3.0" -"@azure/storage-blob@^12.11.0": - version "12.13.0" - resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.13.0.tgz#9209cbb5c2cd463fb967a0f2ae144ace20879160" - integrity sha512-t3Q2lvBMJucgTjQcP5+hvEJMAsJSk0qmAnjDLie2td017IiduZbbC9BOcFfmwzR6y6cJdZOuewLCNFmEx9IrXA== +"@azure/storage-blob@12.18.x": + version "12.18.0" + resolved "https://registry.yarnpkg.com/@azure/storage-blob/-/storage-blob-12.18.0.tgz#9dd001c9aa5e972216f5af15131009086cfeb59e" + integrity sha512-BzBZJobMoDyjJsPRMLNHvqHycTGrT8R/dtcTx9qUFcqwSRfGVK9A/cZ7Nx38UQydT9usZGbaDCN75QRNjezSAA== dependencies: "@azure/abort-controller" "^1.0.0" "@azure/core-http" "^3.0.0" @@ -851,6 +929,15 @@ "@babel/highlight" "^7.23.4" chalk "^2.4.2" +"@babel/code-frame@^7.25.9": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.5.tgz#b1f6c86a02d85d2dd3368a2b67c09add8cd0c255" @@ -896,6 +983,17 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.25.9": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" + integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== + dependencies: + "@babel/parser" "^7.26.2" + "@babel/types" "^7.26.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" @@ -1077,11 +1175,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.16.7", "@babel/helper-validator-option@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" @@ -1120,6 +1228,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== +"@babel/parser@^7.23.0", "@babel/parser@^7.25.3", "@babel/parser@^7.25.9", "@babel/parser@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" + integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== + dependencies: + "@babel/types" "^7.26.0" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" @@ -2000,7 +2115,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.25.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== @@ -2016,6 +2131,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/traverse@^7.22.5": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.6.tgz#b53526a2367a0dd6edc423637f3d2d0f2521abc5" @@ -2032,6 +2156,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.23.2": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" + integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/template" "^7.25.9" + "@babel/types" "^7.25.9" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.16.8", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" @@ -2041,6 +2178,14 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.25.9", "@babel/types@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" + integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@balena/dockerignore@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" @@ -2051,21 +2196,24 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.33.2": +"@budibase/backend-core@3.2.11": version "0.0.0" dependencies: "@budibase/nano" "10.1.5" "@budibase/pouchdb-replication-stream" "1.2.11" "@budibase/shared-core" "0.0.0" "@budibase/types" "0.0.0" + "@techpass/passport-openidconnect" "0.3.3" aws-cloudfront-sign "3.0.2" - aws-sdk "2.1030.0" + aws-sdk "2.1692.0" bcrypt "5.1.0" bcryptjs "2.4.3" bull "4.10.1" correlation-id "4.0.0" - dd-trace "5.2.0" + dd-trace "5.26.0" dotenv "16.0.1" + google-auth-library "^8.0.1" + google-spreadsheet "npm:@budibase/google-spreadsheet@4.1.5" ioredis "5.3.2" joi "17.6.0" jsonwebtoken "9.0.2" @@ -2080,8 +2228,8 @@ pino "8.11.0" pino-http "8.3.3" posthog-node "4.0.1" - pouchdb "7.3.0" - pouchdb-find "7.2.2" + pouchdb "9.0.0" + pouchdb-find "9.0.0" redlock "4.2.0" rotating-file-stream "3.1.0" sanitize-s3-objectkey "0.0.1" @@ -2132,18 +2280,18 @@ through2 "^2.0.0" "@budibase/pro@npm:@budibase/pro@latest": - version "2.33.2" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.33.2.tgz#5c2012f7b2bf0fd871cda1ad37ad7a0442c84658" - integrity sha512-lBB6Wfp6OIOHRlGq82WS9KxvEXRs/P2QlwJT0Aj9PhmkQFsnXm2r8d18f0xTGvcflD+iR7XGP/k56JlCanmhQg== + version "3.2.11" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.11.tgz#40d4929b3958dacca3f4c207718a4647c08a6100" + integrity sha512-xE1tx/C2cnbyR4s/6XkkweoQw6CW5fsQt++gzrrML8abgsODru+tA7M2NbWlKsEnVWHDQRvUVcXm4wqDeNNZ9g== dependencies: "@anthropic-ai/sdk" "^0.27.3" - "@budibase/backend-core" "2.33.2" - "@budibase/shared-core" "2.33.2" - "@budibase/string-templates" "2.33.2" - "@budibase/types" "2.33.2" - "@koa/router" "8.0.8" + "@budibase/backend-core" "3.2.11" + "@budibase/shared-core" "3.2.11" + "@budibase/string-templates" "3.2.11" + "@budibase/types" "3.2.11" + "@koa/router" "13.1.0" bull "4.10.1" - dd-trace "5.2.0" + dd-trace "5.23.0" joi "17.6.0" jsonwebtoken "9.0.2" lru-cache "^7.14.1" @@ -2153,13 +2301,13 @@ scim-patch "^0.8.1" scim2-parse-filter "^0.2.8" -"@budibase/shared-core@2.33.2": +"@budibase/shared-core@3.2.11": version "0.0.0" dependencies: "@budibase/types" "0.0.0" cron-validate "1.4.5" -"@budibase/string-templates@2.33.2": +"@budibase/string-templates@3.2.11": version "0.0.0" dependencies: "@budibase/handlebars-helpers" "^0.13.2" @@ -2167,7 +2315,7 @@ handlebars "^4.7.8" lodash.clonedeep "^4.5.0" -"@budibase/types@2.33.2": +"@budibase/types@3.2.11": version "0.0.0" dependencies: scim-patch "^0.8.1" @@ -2200,32 +2348,6 @@ dependencies: "@bull-board/api" "5.10.2" -"@camunda8/sdk@^8.5.3": - version "8.6.12" - resolved "https://registry.yarnpkg.com/@camunda8/sdk/-/sdk-8.6.12.tgz#8a210359cd9873b9e1750dcde45e62045409cc17" - integrity sha512-dQNw9rMCrL0hJezjAeCAJWMyhuV/ouizP21UzgG9Edqnj/od9ko9XQjEd/AuSj9VMEEQ+bt40mBMnZszbISONg== - dependencies: - "@grpc/grpc-js" "1.10.9" - "@grpc/proto-loader" "0.7.13" - chalk "^2.4.2" - console-stamp "^3.0.2" - dayjs "^1.8.15" - debug "^4.3.4" - fast-xml-parser "^4.1.3" - got "^11.8.6" - jwt-decode "^4.0.0" - lodash.mergewith "^4.6.2" - long "^4.0.0" - lossless-json "^4.0.1" - neon-env "^0.1.3" - promise-retry "^1.1.1" - reflect-metadata "^0.2.1" - stack-trace "0.0.10" - typed-duration "^1.0.12" - uuid "^7.0.3" - optionalDependencies: - win-ca "3.5.1" - "@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.7.1": version "6.7.1" resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.7.1.tgz#3364799b78dff70fb8f81615536c52ea53ce40b2" @@ -2304,10 +2426,10 @@ style-mod "^4.0.0" w3c-keyname "^2.2.4" -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== +"@colors/colors@1.6.0", "@colors/colors@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" + integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -2355,25 +2477,52 @@ resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.4.tgz#d77bfa9ff49e2307c0c6e6b8b26b5dd3c05816c4" integrity sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw== -"@datadog/native-appsec@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-7.0.0.tgz#a380174dd49aef2d9bb613a0ec8ead6dc7822095" - integrity sha512-bywstWFW2hWxzPuS0+mFMVHHL0geulx5yQFtsjfszaH2LTAgk2D+Rt40MKbAoZ8q3tRw2dy6aYQ7svO3ca8jpA== +"@datadog/libdatadog@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.2.2.tgz#ac02c76ac9a38250dca740727c7cdf00244ce3d3" + integrity sha512-rTWo96mEPTY5UbtGoFj8/wY0uKSViJhsPg/Z6aoFWBFXQ8b45Ix2e/yvf92AAwrhG+gPLTxEqTXh3kef2dP8Ow== + +"@datadog/native-appsec@8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.1.1.tgz#76aa34697e6ecbd3d9ef7e6938d3cdcfa689b1f3" + integrity sha512-mf+Ym/AzET4FeUTXOs8hz0uLOSsVIUnavZPUx8YoKWK5lKgR2L+CLfEzOpjBwgFpDgbV8I1/vyoGelgGpsMKHA== dependencies: node-gyp-build "^3.9.0" -"@datadog/native-iast-rewriter@2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.2.2.tgz#3f7feaf6be1af4c83ad063065b8ed509bbaf11cb" - integrity sha512-13ZBhJpjZ/tiV6rYfyAf/ITye9cyd3x12M/2NKhD4Ivev4N4uKBREAjpArOtzKtPXZ5b6oXwVV4ofT1SHoYyzA== +"@datadog/native-appsec@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.3.0.tgz#91afd89d18d386be4da8a1b0e04500f2f8b5eb66" + integrity sha512-RYHbSJ/MwJcJaLzaCaZvUyNLUKFbMshayIiv4ckpFpQJDiq1T8t9iM2k7008s75g1vRuXfsRNX7MaLn4aoFuWA== + dependencies: + node-gyp-build "^3.9.0" + +"@datadog/native-iast-rewriter@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.4.1.tgz#e8211f78c818906513fb96a549374da0382c7623" + integrity sha512-j3auTmyyn63e2y+SL28CGNy/l+jXQyh+pxqoGTacWaY5FW/dvo5nGQepAismgJ3qJ8VhQfVWRdxBSiT7wu9clw== dependencies: lru-cache "^7.14.0" node-gyp-build "^4.5.0" -"@datadog/native-iast-taint-tracking@1.6.4": - version "1.6.4" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-1.6.4.tgz#16c21ad7c36a53420c0d3c5a3720731809cc7e98" - integrity sha512-Owxk7hQ4Dxwv4zJAoMjRga0IvE6lhvxnNc8pJCHsemCWBXchjr/9bqg05Zy5JnMbKUWn4XuZeJD6RFZpRa8bfw== +"@datadog/native-iast-rewriter@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.5.0.tgz#b613defe86e78168f750d1f1662d4ffb3cf002e6" + integrity sha512-WRu34A3Wwp6oafX8KWNAbedtDaaJO+nzfYQht7pcJKjyC2ggfPeF7SoP+eDo9wTn4/nQwEOscSR4hkJqTRlpXQ== + dependencies: + lru-cache "^7.14.0" + node-gyp-build "^4.5.0" + +"@datadog/native-iast-taint-tracking@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.1.0.tgz#7b2ed7f8fad212d65e5ab03bcdea8b42a3051b2e" + integrity sha512-rw6qSjmxmu1yFHVvZLXFt/rVq2tUZXocNogPLB8n7MPpA0jijNGb109WokWw5ITImiW91GcGDuBW6elJDVKouQ== + dependencies: + node-gyp-build "^3.9.0" + +"@datadog/native-iast-taint-tracking@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.2.0.tgz#9fb6823d82f934e12c06ea1baa7399ca80deb2ec" + integrity sha512-Mc6FzCoyvU5yXLMsMS9yKnEqJMWoImAukJXolNWCTm+JQYCMf2yMsJ8pBAm7KyZKliamM9rCn7h7Tr2H3lXwjA== dependencies: node-gyp-build "^3.9.0" @@ -2385,15 +2534,34 @@ node-addon-api "^6.1.0" node-gyp-build "^3.9.0" -"@datadog/pprof@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.0.0.tgz#0c0aaf06def6d2bc4b2d353ec7b264dadbfbefab" - integrity sha512-vhNan4SBuNWLpexunDJQ+hNbRAgWdk2qy5Iyh7Nn94uSSHXigAJMAvu4jwMKKQKFfchtobOkWT8GQUWW3tgpFg== +"@datadog/native-metrics@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.0.1.tgz#dc276c93785c0377a048e316f23b7c8ff3acfa84" + integrity sha512-0GuMyYyXf+Qpb/F+Fcekz58f2mO37lit9U3jMbWY/m8kac44gCPABzL5q3gWbdH+hWgqYfQoEYsdNDGSrKfwoQ== + dependencies: + node-addon-api "^6.1.0" + node-gyp-build "^3.9.0" + +"@datadog/pprof@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.3.0.tgz#c2f58d328ecced7f99887f1a559d7fe3aecb9219" + integrity sha512-53z2Q3K92T6Pf4vz4Ezh8kfkVEvLzbnVqacZGgcbkP//q0joFzO8q00Etw1S6NdnCX0XmX08ULaF4rUI5r14mw== dependencies: delay "^5.0.0" node-gyp-build "<4.0" p-limit "^3.1.0" - pprof-format "^2.0.7" + pprof-format "^2.1.0" + source-map "^0.7.4" + +"@datadog/pprof@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.4.1.tgz#08c9bcf5d8efb2eeafdfc9f5bb5402f79fb41266" + integrity sha512-IvpL96e/cuh8ugP5O8Czdup7XQOLHeIDgM5pac5W7Lc1YzGe5zTtebKFpitvb1CPw1YY+1qFx0pWGgKP2kOfHg== + dependencies: + delay "^5.0.0" + node-gyp-build "<4.0" + p-limit "^3.1.0" + pprof-format "^2.1.0" source-map "^0.7.4" "@datadog/sketches-js@^2.1.0": @@ -2421,231 +2589,116 @@ find-up "^5.0.0" strip-json-comments "^3.1.1" -"@esbuild/aix-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" - integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== - "@esbuild/android-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== -"@esbuild/android-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" - integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== - "@esbuild/android-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== -"@esbuild/android-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" - integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== - "@esbuild/android-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== -"@esbuild/android-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" - integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== - "@esbuild/darwin-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== -"@esbuild/darwin-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" - integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== - "@esbuild/darwin-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== -"@esbuild/darwin-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" - integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== - "@esbuild/freebsd-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== -"@esbuild/freebsd-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" - integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== - "@esbuild/freebsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== -"@esbuild/freebsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" - integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== - "@esbuild/linux-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== -"@esbuild/linux-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" - integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== - "@esbuild/linux-arm@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== -"@esbuild/linux-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" - integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== - "@esbuild/linux-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== -"@esbuild/linux-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" - integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== - "@esbuild/linux-loong64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== -"@esbuild/linux-loong64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" - integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== - "@esbuild/linux-mips64el@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== -"@esbuild/linux-mips64el@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" - integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== - "@esbuild/linux-ppc64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== -"@esbuild/linux-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" - integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== - "@esbuild/linux-riscv64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== -"@esbuild/linux-riscv64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" - integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== - "@esbuild/linux-s390x@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== -"@esbuild/linux-s390x@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" - integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== - "@esbuild/linux-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== -"@esbuild/linux-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" - integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== - "@esbuild/netbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== -"@esbuild/netbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" - integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== - "@esbuild/openbsd-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== -"@esbuild/openbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" - integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== - "@esbuild/sunos-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== -"@esbuild/sunos-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" - integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== - "@esbuild/win32-arm64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== -"@esbuild/win32-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" - integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== - "@esbuild/win32-ia32@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== -"@esbuild/win32-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" - integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== - "@esbuild/win32-x64@0.18.20": version "0.18.20" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== -"@esbuild/win32-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" - integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== - "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -2653,20 +2706,11 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.11.0", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": version "4.11.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== -"@eslint/config-array@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.18.0.tgz#37d8fe656e0d5e3dbaea7758ea56540867fd074d" - integrity sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw== - dependencies: - "@eslint/object-schema" "^2.1.4" - debug "^4.3.1" - minimatch "^3.1.2" - "@eslint/eslintrc@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" @@ -2682,36 +2726,11 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/eslintrc@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" - integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^10.0.1" - globals "^14.0.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - "@eslint/js@8.57.0": version "8.57.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@eslint/js@9.9.1", "@eslint/js@^9.7.0": - version "9.9.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06" - integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ== - -"@eslint/object-schema@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" - integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== - "@fontsource/source-sans-pro@^5.0.3": version "5.0.3" resolved "https://registry.yarnpkg.com/@fontsource/source-sans-pro/-/source-sans-pro-5.0.3.tgz#7d6e84a8169ba12fa5e6ce70757aa2ca7e74d855" @@ -2743,7 +2762,7 @@ dependencies: "@fortawesome/fontawesome-common-types" "6.4.2" -"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": +"@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== @@ -2758,56 +2777,45 @@ google-gax "^4.3.3" protobufjs "^7.2.6" -"@google-cloud/paginator@^3.0.7": - version "3.0.7" - resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" - integrity "sha1-+2+OJOyEH5ne+uv2LHXC50TdQZs= sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==" +"@google-cloud/paginator@^5.0.0": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-5.0.2.tgz#86ad773266ce9f3b82955a8f75e22cd012ccc889" + integrity sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg== dependencies: arrify "^2.0.0" extend "^3.0.2" -"@google-cloud/projectify@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-3.0.0.tgz#302b25f55f674854dce65c2532d98919b118a408" - integrity "sha1-MCsl9V9nSFTc5lwlMtmJGbEYpAg= sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==" +"@google-cloud/projectify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-4.0.0.tgz#d600e0433daf51b88c1fa95ac7f02e38e80a07be" + integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA== -"@google-cloud/promisify@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-3.0.1.tgz#8d724fb280f47d1ff99953aee0c1669b25238c2e" - integrity "sha1-jXJPsoD0fR/5mVOu4MFmmyUjjC4= sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==" +"@google-cloud/promisify@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-4.0.0.tgz#a906e533ebdd0f754dca2509933334ce58b8c8b1" + integrity sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g== -"@google-cloud/storage@^6.9.3": - version "6.12.0" - resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-6.12.0.tgz#a5d3093cc075252dca5bd19a3cfda406ad3a9de1" - integrity "sha1-pdMJPMB1JS3KW9GaPP2kBq06neE= sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==" +"@google-cloud/storage@^7.7.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.14.0.tgz#eda9715f68507949214af804c906eba6d168a214" + integrity sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ== dependencies: - "@google-cloud/paginator" "^3.0.7" - "@google-cloud/projectify" "^3.0.0" - "@google-cloud/promisify" "^3.0.0" + "@google-cloud/paginator" "^5.0.0" + "@google-cloud/projectify" "^4.0.0" + "@google-cloud/promisify" "^4.0.0" abort-controller "^3.0.0" async-retry "^1.3.3" - compressible "^2.0.12" - duplexify "^4.0.0" - ent "^2.2.0" - extend "^3.0.2" - fast-xml-parser "^4.2.2" - gaxios "^5.0.0" - google-auth-library "^8.0.1" + duplexify "^4.1.3" + fast-xml-parser "^4.4.1" + gaxios "^6.0.2" + google-auth-library "^9.6.3" + html-entities "^2.5.2" mime "^3.0.0" - mime-types "^2.0.8" p-limit "^3.0.1" - retry-request "^5.0.0" - teeny-request "^8.0.0" + retry-request "^7.0.0" + teeny-request "^9.0.0" uuid "^8.0.0" -"@grpc/grpc-js@1.10.9": - version "1.10.9" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.10.9.tgz#468cc1549a3fe37b760a16745fb7685d91f4f10c" - integrity sha512-5tcgUctCG0qoNyfChZifz2tJqbRbXVO9J7X6duFcOjY3HUNCxg5D0ZCK7EP9vIcZ0zRpLU9bWkyCqVCLZ46IbQ== - dependencies: - "@grpc/proto-loader" "^0.7.13" - "@js-sdsl/ordered-map" "^4.4.2" - "@grpc/grpc-js@^1.10.9": version "1.10.10" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.10.10.tgz#476d315feeb9dbb0f2d6560008c92688c30f13e0" @@ -2816,7 +2824,7 @@ "@grpc/proto-loader" "^0.7.13" "@js-sdsl/ordered-map" "^4.4.2" -"@grpc/proto-loader@0.7.13", "@grpc/proto-loader@^0.7.13": +"@grpc/proto-loader@^0.7.13": version "0.7.13" resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== @@ -2826,32 +2834,18 @@ protobufjs "^7.2.5" yargs "^17.7.2" -"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": +"@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== -"@hapi/topo@^5.0.0", "@hapi/topo@^5.1.0": +"@hapi/topo@^5.0.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== dependencies: "@hapi/hoek" "^9.0.0" -"@hubspot/api-client@7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@hubspot/api-client/-/api-client-7.1.2.tgz#a405b0a18b8caa27f129fd510b2555e5a5cc2708" - integrity sha512-JVQqh0fdHf97ePk0Hg/7BJsiXNlS9HQRPiM/CLgvVWt5CIviSLQ/kHLZXREmZqTWu7BisjCgHxnSx/d7gRdr2g== - dependencies: - bluebird "^3.7.2" - bottleneck "^2.19.5" - btoa "^1.2.1" - es6-promise "^4.2.4" - form-data "^2.5.0" - lodash "^4.17.21" - node-fetch "^2.6.0" - url-parse "^1.4.3" - "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -2871,11 +2865,6 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@humanwhocodes/retry@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570" - integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== - "@hutson/parse-repository-url@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" @@ -2903,6 +2892,11 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" +"@isaacs/ttlcache@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" + integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -3140,262 +3134,277 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jimp/bmp@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.22.12.tgz#0316044dc7b1a90274aef266d50349347fb864d4" - integrity sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g== +"@jimp/core@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/core/-/core-1.1.4.tgz#54f0c0877bb015361f2cf7d1e1de6fed07e026a9" + integrity sha512-Pokt0rq2qT9oTbQkYVd4z8nIA0eHu2yI3Gd5SmkKQjQa/lRVWRFazqAJMpPkIQt32gSf2rRUVopp7O7wkjjV8w== dependencies: - "@jimp/utils" "^0.22.12" - bmp-js "^0.1.0" - -"@jimp/core@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/core/-/core-0.22.12.tgz#70785ea7d10b138fb65bcfe9f712826f00a10e1d" - integrity sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA== - dependencies: - "@jimp/utils" "^0.22.12" - any-base "^1.1.0" - buffer "^5.2.0" + "@jimp/file-ops" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + await-to-js "^3.0.0" exif-parser "^0.1.12" - file-type "^16.5.4" - isomorphic-fetch "^3.0.0" - pixelmatch "^4.0.2" - tinycolor2 "^1.6.0" + file-type "^16.0.0" + mime "3" -"@jimp/custom@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/custom/-/custom-0.22.12.tgz#236f2a3f016b533c50869ff22ad1ac00dd0c36be" - integrity sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q== +"@jimp/diff@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/diff/-/diff-1.1.4.tgz#505b6f9f738f9a6495f36960662738937fea529b" + integrity sha512-Xc/g1SfphHT9+aeghCxQou8cCmzIArLot31PNXYhx/Bip0Px1wtZHW22sFgCPjGJS6pE/74qRjM0V8VJQYup3w== dependencies: - "@jimp/core" "^0.22.12" + "@jimp/plugin-resize" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + pixelmatch "^5.3.0" -"@jimp/gif@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/gif/-/gif-0.22.12.tgz#6caccb45df497fb971b7a88690345596e22163c0" - integrity sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg== +"@jimp/file-ops@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/file-ops/-/file-ops-1.1.4.tgz#3a1670c1ffdd72a848c10a80187b53556fd05131" + integrity sha512-vJqidRRZlQfaOS/DE9FnkFDmu6Fyx5ZtqTRfBDRr8fAPPDC+N6Fh4//0YQ2CO1xstI35WoPPkJDu6Geq+f1b5Q== + +"@jimp/js-bmp@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/js-bmp/-/js-bmp-1.1.4.tgz#c8c777e2100db8fa69583e7c2ca4cf1d145b7e51" + integrity sha512-fO8dhqfDF08Zw4SXdXD2GqLakR4KInUY6dWkNyOLH+fADsi2jmx/UgcdNiZMGm/iaQSdTdUovgpmLJrr5kQ3Kg== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + bmp-ts "^1.0.9" + +"@jimp/js-gif@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/js-gif/-/js-gif-1.1.4.tgz#5f6d18b250ca3d241de0fd1a0421cff679956ee3" + integrity sha512-/+W2hCPljZg4xEC82W4Zl/gy3ZzQVD05jYovviuHx+T3d/8y/GZWElDp6dHkBefnZ1P3ZEC+sBtLUIyuAz7c4A== + dependencies: + "@jimp/core" "1.1.4" + "@jimp/types" "1.1.4" gifwrap "^0.10.1" - omggif "^1.0.9" + omggif "^1.0.10" -"@jimp/jpeg@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/jpeg/-/jpeg-0.22.12.tgz#b5c74a5aac9826245311370dda8c71a1fcca05ed" - integrity sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q== +"@jimp/js-jpeg@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/js-jpeg/-/js-jpeg-1.1.4.tgz#b84065aca4f5631497321883f09e841fae7ffc1a" + integrity sha512-Qt7U2MLuLd7fpA9m7LEUvf4oEjYofJtxi7a4XApkHOtRC7+l2KBEpiw2EGwCd1AQ8dnryaO5ehFqALhiIjcv+w== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/types" "1.1.4" jpeg-js "^0.4.4" -"@jimp/plugin-blit@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-blit/-/plugin-blit-0.22.12.tgz#0fa8320767fda77434b4408798655ff7c7e415d4" - integrity sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ== +"@jimp/js-png@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/js-png/-/js-png-1.1.4.tgz#128670c6f3de2d7291bb53cbac7fdce95838c2c4" + integrity sha512-F+8d0cHlS5MJnvle5TbQRhe7UIyqbZJlrqYemrfARTeoyhUQo5NYfeOmnnyABl1Jiwvhe7cWzKnlXRkhJZzS6g== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/types" "1.1.4" + pngjs "^7.0.0" -"@jimp/plugin-blur@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-blur/-/plugin-blur-0.22.12.tgz#0c37b2ff4e588b45f4307b4f13d3d0eef813920d" - integrity sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw== +"@jimp/js-tiff@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/js-tiff/-/js-tiff-1.1.4.tgz#e85a7e228e91098c01f84045f9f0e63bfab1d121" + integrity sha512-kopUh2c2vxNjeAljniP8jQnWGWdhlFUfP6RySAnRpRDbp9LhTrpYGngKf/fOxv8MMEXOifGNvQlvzgOrnmF4sQ== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/types" "1.1.4" + utif2 "^4.1.0" -"@jimp/plugin-circle@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-circle/-/plugin-circle-0.22.12.tgz#9fffda83d3fc5bad8c1e1492b15b1433cb42e16e" - integrity sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg== +"@jimp/plugin-blit@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-blit/-/plugin-blit-1.1.4.tgz#07a0ec5c5890697cf4905b5eaf58403920c6848a" + integrity sha512-mwiZp7tSId/2LyFzct456rMulbi+J9Mm9jQ1jhWt7TPM4qjobFXHem5glyU1aNf9CpHcsOP83RUj5me7DavvEg== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-color@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-color/-/plugin-color-0.22.12.tgz#1e49f2e7387186507e917b0686599767c15be336" - integrity sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA== +"@jimp/plugin-blur@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-blur/-/plugin-blur-1.1.4.tgz#e55fba4af64f095d51036b2dcbdd2d9f93d3c81a" + integrity sha512-XH+NGrKOQbs5Q0WF4HToWSUz5ts4xRABFcAIDgs9O34iYdTL3K9lPMHAOH+LrB+2uWMzguQQncdEJrPKgNXC4Q== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/utils" "1.1.4" + +"@jimp/plugin-circle@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-circle/-/plugin-circle-1.1.4.tgz#bad7981483219554587ded91e63ec4aa69eb0884" + integrity sha512-zOemNyA5VIgWnC+NQys7FCqpFt6jN7Hvp//G9pL+oD9sXDLQbkR65ZHdpI7iglVtxsq3yc7hFe2ojCRCu3AbSg== + dependencies: + "@jimp/types" "1.1.4" + zod "^3.23.8" + +"@jimp/plugin-color@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-color/-/plugin-color-1.1.4.tgz#bf142840d7570e7964a136f278de6f8134686417" + integrity sha512-j7xJqO9Cr45sLw+UYwCRtoeWl8/mZsBmZEAGdpx4ny2vHD0IMD3S56NcTuSLJ9zFtuyIEJkQFUNFUDaxVxVjag== + dependencies: + "@jimp/core" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" tinycolor2 "^1.6.0" + zod "^3.23.8" -"@jimp/plugin-contain@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-contain/-/plugin-contain-0.22.12.tgz#ed5ed9af3d4afd02a7568ff8d60603cff340e3f3" - integrity sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ== +"@jimp/plugin-contain@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-contain/-/plugin-contain-1.1.4.tgz#185b9b3fdb85d61b38c8ea9ac060bb05df4e9e68" + integrity sha512-XIMURmXFDdZYyKsETyopBloqndJKk7ohtE6ujO/o//O5/Op9A15deh8yais39A5j8uSyHEsvBwdGtGm4co7rnw== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/plugin-blit" "1.1.4" + "@jimp/plugin-resize" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-cover@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-cover/-/plugin-cover-0.22.12.tgz#4abbfabe4c78c71d8d46e707c35a65dc55f08afd" - integrity sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA== +"@jimp/plugin-cover@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-cover/-/plugin-cover-1.1.4.tgz#3b270c4526e24652f772f8a1aa940dc88118e24a" + integrity sha512-VxaQhcCYeJRQcNXrLbOUcn/KAVmVgTNexLucjUvm8uSWCyDfO+HJ6okL/qyux2h05asyCcFXz7zeNswEqjePSg== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/plugin-crop" "1.1.4" + "@jimp/plugin-resize" "1.1.4" + "@jimp/types" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-crop@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-crop/-/plugin-crop-0.22.12.tgz#e28329a9f285071442998560b040048d2ef5c32e" - integrity sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw== +"@jimp/plugin-crop@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-crop/-/plugin-crop-1.1.4.tgz#4d6729532f9229b6e54ccf45a72c147872ab3f9f" + integrity sha512-RejGsKWoG0ji2YwvlKKIEnCxZFGHZ7dwcmFIHiWOZs+fhT+HoHbDy9QEIT+MmgWeeIVm0B3MrA/oBMLEwaJbzg== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-displace@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-displace/-/plugin-displace-0.22.12.tgz#2e4b2b989a23da6687c49f2f628e1e6d686ec9b6" - integrity sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA== +"@jimp/plugin-displace@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-displace/-/plugin-displace-1.1.4.tgz#63b536a9f550cb03d233970478640fa5973cfae3" + integrity sha512-X+yMdj4DZu/p5YZ9Go7k3HfkC2XTw/5am/p9Fn2xoOJwGa+LIDCAAJ/xuVw+qJMuyvhjIa5rck39yePvBumLyg== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-dither@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-dither/-/plugin-dither-0.22.12.tgz#3cc5f3a58dbf85653c4e532d31a756a4fc8cabf7" - integrity sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw== +"@jimp/plugin-dither@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-dither/-/plugin-dither-1.1.4.tgz#96ae3a59b66f5f9ee9b7dc0a4e7d572a6c38e2a0" + integrity sha512-GvyRicPVpxlyol304C4v3T/OpJiuER4ibIhMTAKPx393ByvRgoo1r69nVRfEbmYoKAlwxEA+DISoDgBWYFq4yw== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/types" "1.1.4" -"@jimp/plugin-fisheye@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-fisheye/-/plugin-fisheye-0.22.12.tgz#77aef2f3ec59c0bafbd2dbc94b89eab60ce05a3e" - integrity sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q== +"@jimp/plugin-fisheye@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-fisheye/-/plugin-fisheye-1.1.4.tgz#1f2b611e939c9546c5d9d9abe416d1e10407d88a" + integrity sha512-mX2yUzndi9esrcEIv9wQIChTLhehZ0SNjRY81BMS9vxa3poxrLyNDq2GHSVmWcehSpkMmATVYKQy6AcLcfynSA== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-flip@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-flip/-/plugin-flip-0.22.12.tgz#7e2154592da01afcf165a3f9d1d25032aa8d8c57" - integrity sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q== +"@jimp/plugin-flip@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-flip/-/plugin-flip-1.1.4.tgz#ef8c6734a16b4385cb70bd8b84a2dbd14941f295" + integrity sha512-dhhM1tY21QqnaSvgh9Evpq09+IgAfeZJwLTTJnoWN5j+suE/+K9fIlMSx8XKbv3hBvOBVPHUrq8xoiTLHryr5w== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/types" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-gaussian@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-gaussian/-/plugin-gaussian-0.22.12.tgz#49a40950cedbbea6c84b3a6bccc45365fe78d6b7" - integrity sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg== +"@jimp/plugin-hash@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-hash/-/plugin-hash-1.1.4.tgz#3af4a37bf31fbfb15a3263245efd07da10b7981f" + integrity sha512-nmjnQwxcNVTq7qlkUuX5OzdwO/F+mnE2QT+TZ6VEAphPT8Iu7ZaJfYd3wxAbon9UrbxZULFnhiEOs6yE2dWYaw== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/js-bmp" "1.1.4" + "@jimp/js-jpeg" "1.1.4" + "@jimp/js-png" "1.1.4" + "@jimp/js-tiff" "1.1.4" + "@jimp/plugin-color" "1.1.4" + "@jimp/plugin-resize" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + any-base "^1.1.0" -"@jimp/plugin-invert@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-invert/-/plugin-invert-0.22.12.tgz#c569e85c1f59911a9a33ef36a51c9cf26065078e" - integrity sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ== +"@jimp/plugin-mask@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-mask/-/plugin-mask-1.1.4.tgz#6ad63a0f8c3c5b99439ba79e2a48bb00bfbc7e36" + integrity sha512-86Duc7r9kdv26oaApwHtFULMHxLCBoBdeAA/PyH1RRsZy2eu+M8hGFYf99vJlLBgJslyToGjxIgDE1nsuB1uHA== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/types" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-mask@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-mask/-/plugin-mask-0.22.12.tgz#0ac0d9c282f403255b126556521f90fb8e2997f0" - integrity sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA== +"@jimp/plugin-print@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-print/-/plugin-print-1.1.4.tgz#dc3c4c9130cda0c3571b3c352546f0d69aae4046" + integrity sha512-EMkakkwi1qrcmQ4nexD2w5ZEUxgesFd7lcYR7DBCXKBYabvC9RDHXaWTxzeFa0VWy3/ZnpoJJ3Qq8I8WqHvjmw== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/js-jpeg" "1.1.4" + "@jimp/js-png" "1.1.4" + "@jimp/plugin-blit" "1.1.4" + "@jimp/types" "1.1.4" + parse-bmfont-ascii "^1.0.6" + parse-bmfont-binary "^1.0.6" + parse-bmfont-xml "^1.1.6" + zod "^3.23.8" -"@jimp/plugin-normalize@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-normalize/-/plugin-normalize-0.22.12.tgz#6c44d216f2489cf9b0e0f1e03aa5dfb97f198c53" - integrity sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA== +"@jimp/plugin-quantize@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-quantize/-/plugin-quantize-1.1.4.tgz#a883eeef1f6144354bb868921895a89ca40d557f" + integrity sha512-+DuC7ZXjNGFoZtsYU2MxXz06E48AIBNg1G/2sq2bXu+PJEU0xvQctEvJEdl+xhRo4sQNQpwOzCZtBvY49VqfNA== dependencies: - "@jimp/utils" "^0.22.12" + image-q "^4.0.0" + zod "^3.23.8" -"@jimp/plugin-print@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-print/-/plugin-print-0.22.12.tgz#6a49020947a9bf21a5a28324425670a25587ca65" - integrity sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ== +"@jimp/plugin-resize@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-resize/-/plugin-resize-1.1.4.tgz#2107bda637dfa05e01aab341b2ba96cbb8b78835" + integrity sha512-+KY0A5agiOpV60cfs28DZCl3t/8QRVO9kyzrdDqCLkhc/7g2YYrdyhkJZYUq5GBJirkpGGzXZQR2t+g7Sc9dEQ== dependencies: - "@jimp/utils" "^0.22.12" - load-bmfont "^1.4.1" + "@jimp/core" "1.1.4" + "@jimp/types" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-resize@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-resize/-/plugin-resize-0.22.12.tgz#f92acbf73beb97dd1fe93b166ef367a323b81e81" - integrity sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg== +"@jimp/plugin-rotate@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-rotate/-/plugin-rotate-1.1.4.tgz#2c1c963ba5e522a81514fe3870e1c6c823113a47" + integrity sha512-9yRcL5cFcA88kVDt9nco1BUipAjw6uto6AOJi2Bp3FFfdJ84F3rU6Jvcbl4aDyywoJ+J93gKXRo/GAEPk8xpvg== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/plugin-crop" "1.1.4" + "@jimp/plugin-resize" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-rotate@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-rotate/-/plugin-rotate-0.22.12.tgz#2235d45aeb4914ff70d99e95750a6d9de45a0d9f" - integrity sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA== +"@jimp/plugin-threshold@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/plugin-threshold/-/plugin-threshold-1.1.4.tgz#6625758b6392124d284dbed07156c63fb155b874" + integrity sha512-d2uTz8iNuW3ogjH/OVEmPtiSzIpr99Dk5mTOXmnojSqT/5Ufs2ngJf3JQ2wIwOyb6pXph+xRqseQjhf1EUXasA== dependencies: - "@jimp/utils" "^0.22.12" + "@jimp/core" "1.1.4" + "@jimp/plugin-color" "1.1.4" + "@jimp/plugin-hash" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" + zod "^3.23.8" -"@jimp/plugin-scale@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-scale/-/plugin-scale-0.22.12.tgz#91f1ec3d114ff44092b946a16e66b14d918e32ed" - integrity sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw== +"@jimp/types@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/types/-/types-1.1.4.tgz#b46881102fc6d353451e18ccf06a3dab4dbfb0d5" + integrity sha512-Ck7ShGOeRjN1E2NH9YQs1UDD8Sh54XzSjLhbNq3gtbXrDgSAUH2e47K1VLoUHVBdq7COTDlDCBPOFb/kgQz0zQ== dependencies: - "@jimp/utils" "^0.22.12" + zod "^3.23.8" -"@jimp/plugin-shadow@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-shadow/-/plugin-shadow-0.22.12.tgz#52e3a1d55f61ddfcfb3265544f8d23b887a667b8" - integrity sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg== +"@jimp/utils@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@jimp/utils/-/utils-1.1.4.tgz#5af44854cc74be08253e6ed7dee82a9058765406" + integrity sha512-mkfoOtC3/vVibCQz3MQkbt8FMtuJI56ekcoDBJqcY9Pjyyd7nbOhfWhiLiLYIfcrslJX8pLEq4ewR2PTNRXTfA== dependencies: - "@jimp/utils" "^0.22.12" - -"@jimp/plugin-threshold@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugin-threshold/-/plugin-threshold-0.22.12.tgz#1efe20e154bf3a1fc4a5cc016092dbacaa60c958" - integrity sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw== - dependencies: - "@jimp/utils" "^0.22.12" - -"@jimp/plugins@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/plugins/-/plugins-0.22.12.tgz#45a3b96d2d24cec21d4f8b79d1cfcec6fcb2f1d4" - integrity sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww== - dependencies: - "@jimp/plugin-blit" "^0.22.12" - "@jimp/plugin-blur" "^0.22.12" - "@jimp/plugin-circle" "^0.22.12" - "@jimp/plugin-color" "^0.22.12" - "@jimp/plugin-contain" "^0.22.12" - "@jimp/plugin-cover" "^0.22.12" - "@jimp/plugin-crop" "^0.22.12" - "@jimp/plugin-displace" "^0.22.12" - "@jimp/plugin-dither" "^0.22.12" - "@jimp/plugin-fisheye" "^0.22.12" - "@jimp/plugin-flip" "^0.22.12" - "@jimp/plugin-gaussian" "^0.22.12" - "@jimp/plugin-invert" "^0.22.12" - "@jimp/plugin-mask" "^0.22.12" - "@jimp/plugin-normalize" "^0.22.12" - "@jimp/plugin-print" "^0.22.12" - "@jimp/plugin-resize" "^0.22.12" - "@jimp/plugin-rotate" "^0.22.12" - "@jimp/plugin-scale" "^0.22.12" - "@jimp/plugin-shadow" "^0.22.12" - "@jimp/plugin-threshold" "^0.22.12" - timm "^1.6.1" - -"@jimp/png@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/png/-/png-0.22.12.tgz#e033586caf38d9c9d33808e92eb87c4d7f0aa1eb" - integrity sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg== - dependencies: - "@jimp/utils" "^0.22.12" - pngjs "^6.0.0" - -"@jimp/tiff@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/tiff/-/tiff-0.22.12.tgz#67cac3f2ded6fde3ef631fbf74bea0fa53800123" - integrity sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg== - dependencies: - utif2 "^4.0.1" - -"@jimp/types@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/types/-/types-0.22.12.tgz#6f83761ba171cb8cd5998fa66a5cbfb0b22d3d8c" - integrity sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA== - dependencies: - "@jimp/bmp" "^0.22.12" - "@jimp/gif" "^0.22.12" - "@jimp/jpeg" "^0.22.12" - "@jimp/png" "^0.22.12" - "@jimp/tiff" "^0.22.12" - timm "^1.6.1" - -"@jimp/utils@^0.22.12": - version "0.22.12" - resolved "https://registry.yarnpkg.com/@jimp/utils/-/utils-0.22.12.tgz#8ffaed8f2dc2962539ccaf14727ac60793c7a537" - integrity sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q== - dependencies: - regenerator-runtime "^0.13.3" + "@jimp/types" "1.1.4" + tinycolor2 "^1.6.0" "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" @@ -3445,7 +3454,7 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@js-joda/core@^5.5.3": +"@js-joda/core@^5.6.1": version "5.6.3" resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-5.6.3.tgz#41ae1c07de1ebe0f6dde1abcbc9700a09b9c6056" integrity sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA== @@ -3460,24 +3469,31 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== -"@koa/cors@^5.0.0": +"@jsep-plugin/assignment@^1.2.1": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@jsep-plugin/assignment/-/assignment-1.3.0.tgz#fcfc5417a04933f7ceee786e8ab498aa3ce2b242" + integrity sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ== + +"@jsep-plugin/regex@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@jsep-plugin/regex/-/regex-1.0.4.tgz#cb2fc423220fa71c609323b9ba7f7d344a755fcc" + integrity sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg== + +"@koa/cors@5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-5.0.0.tgz#0029b5f057fa0d0ae0e37dd2c89ece315a0daffd" integrity sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw== dependencies: vary "^1.1.2" -"@koa/router@8.0.8": - version "8.0.8" - resolved "https://registry.yarnpkg.com/@koa/router/-/router-8.0.8.tgz#95f32d11373d03d89dcb63fabe9ac6f471095236" - integrity sha512-FnT93N4NUehnXr+juupDmG2yfi0JnWdCmNEuIXpCG4TtG+9xvtrLambBH3RclycopVUOEYAim2lydiNBI7IRVg== +"@koa/router@13.1.0": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@koa/router/-/router-13.1.0.tgz#43f4c554444ea4f4a148a5735a9525c6d16fd1b5" + integrity sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw== dependencies: - debug "^4.1.1" - http-errors "^1.7.3" + http-errors "^2.0.0" koa-compose "^4.1.0" - methods "^1.1.2" - path-to-regexp "1.x" - urijs "^1.19.2" + path-to-regexp "^6.3.0" "@lerna/child-process@7.4.2": version "7.4.2" @@ -3666,14 +3682,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@npmcli/fs@^1.0.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" - integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== - dependencies: - "@gar/promisify" "^1.0.1" - semver "^7.3.5" - "@npmcli/fs@^2.1.0": version "2.1.2" resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" @@ -3711,14 +3719,6 @@ npm-bundled "^3.0.0" npm-normalize-package-bin "^3.0.0" -"@npmcli/move-file@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" - integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - "@npmcli/move-file@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" @@ -3953,10 +3953,15 @@ dependencies: "@octokit/openapi-types" "^18.0.0" -"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.0.1": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.7.0.tgz#b139c81999c23e3c8d3c0a7234480e945920fc40" - integrity sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw== +"@opentelemetry/api@>=1.0.0 <1.9.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.8.0.tgz#5aa7abb48f23f693068ed2999ae627d2f7d902ec" + integrity sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w== + +"@opentelemetry/api@^1.0.1": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== "@opentelemetry/core@^1.14.0": version "1.19.0" @@ -4163,14 +4168,6 @@ is-module "^1.0.0" resolve "^1.22.1" -"@rollup/plugin-replace@^2.4.2": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" - integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== - dependencies: - "@rollup/pluginutils" "^3.1.0" - magic-string "^0.25.7" - "@rollup/plugin-replace@^5.0.2", "@rollup/plugin-replace@^5.0.3": version "5.0.7" resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz#150c9ee9db8031d9e4580a61a0edeaaed3d37687" @@ -4302,23 +4299,6 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz#e4291e3c1bc637083f87936c333cdbcad22af63b" integrity sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA== -"@roxi/routify@2.18.0": - version "2.18.0" - resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.18.0.tgz#8f88bedd936312d0dbe44cbc11ab179b1f938ec2" - integrity sha512-MVB50HN+VQWLzfjLplcBjsSBvwOiExKOmht2DuWR3WQ60JxQi9pSejkB06tFVkFKNXz2X5iYtKDqKBTdae/gRg== - dependencies: - "@roxi/ssr" "^0.2.1" - "@types/node" ">=4.2.0 < 13" - chalk "^4.0.0" - cheap-watch "^1.0.2" - commander "^7.1.0" - configent "^2.1.4" - esm "^3.2.25" - fs-extra "^9.0.1" - log-symbols "^3.0.0" - picomatch "^2.2.2" - rollup-pluginutils "^2.8.2" - "@roxi/routify@2.18.12": version "2.18.12" resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.18.12.tgz#901ca95b96f274ddddaefbf18424557ee1ae3fae" @@ -4354,22 +4334,14 @@ koa "^2.13.4" node-mocks-http "^1.11.0" -"@shopify/jest-koa-mocks@5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@shopify/jest-koa-mocks/-/jest-koa-mocks-5.3.0.tgz#0b9b6ce01a7ef945df7272aa08cda559c11b39f5" - integrity sha512-Yy9uS298/aWhT/bjfhUAcFRaq7pq7j0ybaI6U/vWQ5IcdWD3E73iqptkw+FNC+Z9cdV9QT8osFd0r0frFNb9Nw== - dependencies: - koa "^2.13.4" - node-mocks-http "^1.11.0" - -"@sideway/address@^4.1.3", "@sideway/address@^4.1.5": +"@sideway/address@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== dependencies: "@hapi/hoek" "^9.0.0" -"@sideway/formula@^3.0.0", "@sideway/formula@^3.0.1": +"@sideway/formula@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== @@ -4408,11 +4380,6 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== -"@sindresorhus/is@^4.0.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" - integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== - "@sinonjs/commons@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" @@ -4427,441 +4394,545 @@ dependencies: "@sinonjs/commons" "^2.0.0" -"@smithy/abort-controller@^2.0.10": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-2.0.10.tgz#a6d0d24973ac35b59cc450c34decd68485fbe2c0" - integrity "sha1-ptDSSXOsNbWcxFDDTezWhIX74sA= sha512-xn7PnFD3m4rQIG00h1lPuDVnC2QMtTFhzRLX3y56KkgFaCysS7vpNevNBgmNUtmJ4eVFc+66Zucwo2KDLdicOg==" +"@smithy/abort-controller@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-1.1.0.tgz#2da0d73c504b93ca8bb83bdc8d6b8208d73f418b" + integrity sha512-5imgGUlZL4dW4YWdMYAKLmal9ny/tlenM81QZY7xYyb76z9Z/QOg7oM5Ak9HQl8QfFTlGVWwcMXl+54jroRgEQ== dependencies: - "@smithy/types" "^2.3.4" + "@smithy/types" "^1.2.0" tslib "^2.5.0" -"@smithy/chunked-blob-reader-native@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-2.0.0.tgz#f6d0eeeb5481026b68b054f45540d924c194d558" - integrity "sha1-9tDu61SBAmtosFT0VUDZJMGU1Vg= sha512-HM8V2Rp1y8+1343tkZUKZllFhEQPNmpNdgFAncbTsxkZ18/gqjk23XXv3qGyXWp412f3o43ZZ1UZHVcHrpRnCQ==" +"@smithy/abort-controller@^3.1.8": + version "3.1.8" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-3.1.8.tgz#ce0c10ddb2b39107d70b06bbb8e4f6e368bc551d" + integrity sha512-+3DOBcUn5/rVjlxGvUPKc416SExarAQ+Qe0bqk30YSUjbepwpS7QN0cyKUSifvLJhdMZ0WPzPP5ymut0oonrpQ== dependencies: - "@smithy/util-base64" "^2.0.0" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/chunked-blob-reader-native@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.1.tgz#39045ed278ee1b6f4c12715c7565678557274c29" + integrity sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ== + dependencies: + "@smithy/util-base64" "^3.0.0" + tslib "^2.6.2" + +"@smithy/chunked-blob-reader@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader/-/chunked-blob-reader-4.0.0.tgz#754099909957fb1986c16eb88afad75919d7129d" + integrity sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ== + dependencies: + tslib "^2.6.2" + +"@smithy/config-resolver@^3.0.11", "@smithy/config-resolver@^3.0.12": + version "3.0.12" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-3.0.12.tgz#f355f95fcb5ee932a90871a488a4f2128e8ad3ac" + integrity sha512-YAJP9UJFZRZ8N+UruTeq78zkdjUHmzsY62J4qKWZ4SXB4QXJ/+680EfXXgkYA2xj77ooMqtUY9m406zGNqwivQ== + dependencies: + "@smithy/node-config-provider" "^3.1.11" + "@smithy/types" "^3.7.1" + "@smithy/util-config-provider" "^3.0.0" + "@smithy/util-middleware" "^3.0.10" + tslib "^2.6.2" + +"@smithy/core@^2.5.2", "@smithy/core@^2.5.3": + version "2.5.3" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-2.5.3.tgz#1d5723f676b0d6ec08c515272f0ac03aa59fac72" + integrity sha512-96uW8maifUSmehaeW7uydWn7wBc98NEeNI3zN8vqakGpyCQgzyJaA64Z4FCOUmAdCJkhppd/7SZ798Fo4Xx37g== + dependencies: + "@smithy/middleware-serde" "^3.0.10" + "@smithy/protocol-http" "^4.1.7" + "@smithy/types" "^3.7.1" + "@smithy/util-body-length-browser" "^3.0.0" + "@smithy/util-middleware" "^3.0.10" + "@smithy/util-stream" "^3.3.1" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/credential-provider-imds@^3.2.6", "@smithy/credential-provider-imds@^3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.7.tgz#6eedf87ba0238723ec46d8ce0f18e276685a702d" + integrity sha512-cEfbau+rrWF8ylkmmVAObOmjbTIzKyUC5TkBL58SbLywD0RCBC4JAUKbmtSm2w5KUJNRPGgpGFMvE2FKnuNlWQ== + dependencies: + "@smithy/node-config-provider" "^3.1.11" + "@smithy/property-provider" "^3.1.10" + "@smithy/types" "^3.7.1" + "@smithy/url-parser" "^3.0.10" + tslib "^2.6.2" + +"@smithy/eventstream-codec@^3.1.9": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-3.1.9.tgz#4271354e75e57d30771fca307da403896c657430" + integrity sha512-F574nX0hhlNOjBnP+noLtsPFqXnWh2L0+nZKCwcu7P7J8k+k+rdIDs+RMnrMwrzhUE4mwMgyN0cYnEn0G8yrnQ== + dependencies: + "@aws-crypto/crc32" "5.2.0" + "@smithy/types" "^3.7.1" + "@smithy/util-hex-encoding" "^3.0.0" + tslib "^2.6.2" + +"@smithy/eventstream-serde-browser@^3.0.12": + version "3.0.13" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.13.tgz#191dcf9181e7ab0914ec43d51518d471b9d466ae" + integrity sha512-Nee9m+97o9Qj6/XeLz2g2vANS2SZgAxV4rDBMKGHvFJHU/xz88x2RwCkwsvEwYjSX4BV1NG1JXmxEaDUzZTAtw== + dependencies: + "@smithy/eventstream-serde-universal" "^3.0.12" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/eventstream-serde-config-resolver@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.10.tgz#5c0b2ae0bb8e11cfa77851098e46f7350047ec8d" + integrity sha512-K1M0x7P7qbBUKB0UWIL5KOcyi6zqV5mPJoL0/o01HPJr0CSq3A9FYuJC6e11EX6hR8QTIR++DBiGrYveOu6trw== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/eventstream-serde-node@^3.0.11": + version "3.0.12" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.12.tgz#7312383e821b5807abf2fe12316c2a8967d022f0" + integrity sha512-kiZymxXvZ4tnuYsPSMUHe+MMfc4FTeFWJIc0Q5wygJoUQM4rVHNghvd48y7ppuulNMbuYt95ah71pYc2+o4JOA== + dependencies: + "@smithy/eventstream-serde-universal" "^3.0.12" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/eventstream-serde-universal@^3.0.12": + version "3.0.12" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.12.tgz#803d7beb29a3de4a64e91af97331a4654741c35f" + integrity sha512-1i8ifhLJrOZ+pEifTlF0EfZzMLUGQggYQ6WmZ4d5g77zEKf7oZ0kvh1yKWHPjofvOwqrkwRDVuxuYC8wVd662A== + dependencies: + "@smithy/eventstream-codec" "^3.1.9" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/fetch-http-handler@^4.1.0", "@smithy/fetch-http-handler@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.1.tgz#cead80762af4cdea11e7eeb627ea1c4835265dfa" + integrity sha512-bH7QW0+JdX0bPBadXt8GwMof/jz0H28I84hU1Uet9ISpzUqXqRQ3fEZJ+ANPOhzSEczYvANNl3uDQDYArSFDtA== + dependencies: + "@smithy/protocol-http" "^4.1.7" + "@smithy/querystring-builder" "^3.0.10" + "@smithy/types" "^3.7.1" + "@smithy/util-base64" "^3.0.0" + tslib "^2.6.2" + +"@smithy/hash-blob-browser@^3.1.8": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.9.tgz#1f2c3ef6afbb0ce3e58a0129753850bb9267aae8" + integrity sha512-wOu78omaUuW5DE+PVWXiRKWRZLecARyP3xcq5SmkXUw9+utgN8HnSnBfrjL2B/4ZxgqPjaAJQkC/+JHf1ITVaQ== + dependencies: + "@smithy/chunked-blob-reader" "^4.0.0" + "@smithy/chunked-blob-reader-native" "^3.0.1" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/hash-node@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-3.0.10.tgz#93c857b4bff3a48884886440fd9772924887e592" + integrity sha512-3zWGWCHI+FlJ5WJwx73Mw2llYR8aflVyZN5JhoqLxbdPZi6UyKSdCeXAWJw9ja22m6S6Tzz1KZ+kAaSwvydi0g== + dependencies: + "@smithy/types" "^3.7.1" + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/hash-stream-node@^3.1.8": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-3.1.9.tgz#97eb416811b7e7b9d036f0271588151b619759e9" + integrity sha512-3XfHBjSP3oDWxLmlxnt+F+FqXpL3WlXs+XXaB6bV9Wo8BBu87fK1dSEsyH7Z4ZHRmwZ4g9lFMdf08m9hoX1iRA== + dependencies: + "@smithy/types" "^3.7.1" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/invalid-dependency@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-3.0.10.tgz#8616dee555916c24dec3e33b1e046c525efbfee3" + integrity sha512-Lp2L65vFi+cj0vFMu2obpPW69DU+6O5g3086lmI4XcnRCG8PxvpWC7XyaVwJCxsZFzueHjXnrOH/E0pl0zikfA== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/is-array-buffer@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz#f84f0d9f9a36601a9ca9381688bd1b726fd39111" + integrity sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA== + dependencies: + tslib "^2.6.2" + +"@smithy/is-array-buffer@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz#9a95c2d46b8768946a9eec7f935feaddcffa5e7a" + integrity sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ== + dependencies: + tslib "^2.6.2" + +"@smithy/md5-js@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-3.0.10.tgz#52ab927cf03cd1d24fed82d8ba936faf5632436e" + integrity sha512-m3bv6dApflt3fS2Y1PyWPUtRP7iuBlvikEOGwu0HsCZ0vE7zcIX+dBoh3e+31/rddagw8nj92j0kJg2TfV+SJA== + dependencies: + "@smithy/types" "^3.7.1" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/middleware-content-length@^3.0.11": + version "3.0.12" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-3.0.12.tgz#3b248ed1e8f1e0ae67171abb8eae9da7ab7ca613" + integrity sha512-1mDEXqzM20yywaMDuf5o9ue8OkJ373lSPbaSjyEvkWdqELhFMyNNgKGWL/rCSf4KME8B+HlHKuR8u9kRj8HzEQ== + dependencies: + "@smithy/protocol-http" "^4.1.7" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/middleware-endpoint@^3.2.2", "@smithy/middleware-endpoint@^3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.3.tgz#7dd3df0052fc55891522631a7751e613b6efd68a" + integrity sha512-Hdl9296i/EMptaX7agrSzJZDiz5Y8XPUeBbctTmMtnCguGpqfU3jVsTUan0VLaOhsnquqWLL8Bl5HrlbVGT1og== + dependencies: + "@smithy/core" "^2.5.3" + "@smithy/middleware-serde" "^3.0.10" + "@smithy/node-config-provider" "^3.1.11" + "@smithy/shared-ini-file-loader" "^3.1.11" + "@smithy/types" "^3.7.1" + "@smithy/url-parser" "^3.0.10" + "@smithy/util-middleware" "^3.0.10" + tslib "^2.6.2" + +"@smithy/middleware-retry@^3.0.26": + version "3.0.27" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-3.0.27.tgz#2e4dda420178835cd2d416479505d313b601ba21" + integrity sha512-H3J/PjJpLL7Tt+fxDKiOD25sMc94YetlQhCnYeNmina2LZscAdu0ZEZPas/kwePHABaEtqp7hqa5S4UJgMs1Tg== + dependencies: + "@smithy/node-config-provider" "^3.1.11" + "@smithy/protocol-http" "^4.1.7" + "@smithy/service-error-classification" "^3.0.10" + "@smithy/smithy-client" "^3.4.4" + "@smithy/types" "^3.7.1" + "@smithy/util-middleware" "^3.0.10" + "@smithy/util-retry" "^3.0.10" + tslib "^2.6.2" + uuid "^9.0.1" + +"@smithy/middleware-serde@^3.0.10", "@smithy/middleware-serde@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-3.0.10.tgz#5f6c0b57b10089a21d355bd95e9b7d40378454d7" + integrity sha512-MnAuhh+dD14F428ubSJuRnmRsfOpxSzvRhaGVTvd/lrUDE3kxzCCmH8lnVTvoNQnV2BbJ4c15QwZ3UdQBtFNZA== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/middleware-stack@^3.0.10", "@smithy/middleware-stack@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-3.0.10.tgz#73e2fde5d151440844161773a17ee13375502baf" + integrity sha512-grCHyoiARDBBGPyw2BeicpjgpsDFWZZxptbVKb3CRd/ZA15F/T6rZjCCuBUjJwdck1nwUuIxYtsS4H9DDpbP5w== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/node-config-provider@^3.1.10", "@smithy/node-config-provider@^3.1.11": + version "3.1.11" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-3.1.11.tgz#95feba85a5cb3de3fe9adfff1060b35fd556d023" + integrity sha512-URq3gT3RpDikh/8MBJUB+QGZzfS7Bm6TQTqoh4CqE8NBuyPkWa5eUXj0XFcFfeZVgg3WMh1u19iaXn8FvvXxZw== + dependencies: + "@smithy/property-provider" "^3.1.10" + "@smithy/shared-ini-file-loader" "^3.1.11" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/node-http-handler@^1.0.2": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-1.1.0.tgz#887cee930b520e08043c9f41e463f8d8f5dae127" + integrity sha512-d3kRriEgaIiGXLziAM8bjnaLn1fthCJeTLZIwEIpzQqe6yPX0a+yQoLCTyjb2fvdLwkMoG4p7THIIB5cj5lkbg== + dependencies: + "@smithy/abort-controller" "^1.1.0" + "@smithy/protocol-http" "^1.2.0" + "@smithy/querystring-builder" "^1.1.0" + "@smithy/types" "^1.2.0" tslib "^2.5.0" -"@smithy/chunked-blob-reader@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader/-/chunked-blob-reader-2.0.0.tgz#c44fe2c780eaf77f9e5381d982ac99a880cce51b" - integrity "sha1-xE/ix4Dq93+eU4HZgqyZqIDM5Rs= sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==" +"@smithy/node-http-handler@^3.3.0", "@smithy/node-http-handler@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-3.3.1.tgz#788fc1c22c21a0cf982f4025ccf9f64217f3164f" + integrity sha512-fr+UAOMGWh6bn4YSEezBCpJn9Ukp9oR4D32sCjCo7U81evE11YePOQ58ogzyfgmjIO79YeOdfXXqr0jyhPQeMg== + dependencies: + "@smithy/abort-controller" "^3.1.8" + "@smithy/protocol-http" "^4.1.7" + "@smithy/querystring-builder" "^3.0.10" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/property-provider@^3.1.10", "@smithy/property-provider@^3.1.9": + version "3.1.10" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-3.1.10.tgz#ae00447c1060c194c3e1b9475f7c8548a70f8486" + integrity sha512-n1MJZGTorTH2DvyTVj+3wXnd4CzjJxyXeOgnTlgNVFxaaMeT4OteEp4QrzF8p9ee2yg42nvyVK6R/awLCakjeQ== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/protocol-http@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-1.2.0.tgz#a554e4dabb14508f0bc2cdef9c3710e2b294be04" + integrity sha512-GfGfruksi3nXdFok5RhgtOnWe5f6BndzYfmEXISD+5gAGdayFGpjWu5pIqIweTudMtse20bGbc+7MFZXT1Tb8Q== + dependencies: + "@smithy/types" "^1.2.0" + tslib "^2.5.0" + +"@smithy/protocol-http@^4.1.6", "@smithy/protocol-http@^4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-4.1.7.tgz#5c67e62beb5deacdb94f2127f9a344bdf1b2ed6e" + integrity sha512-FP2LepWD0eJeOTm0SjssPcgqAlDFzOmRXqXmGhfIM52G7Lrox/pcpQf6RP4F21k0+O12zaqQt5fCDOeBtqY6Cg== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/querystring-builder@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-1.1.0.tgz#de6306104640ade34e59be33949db6cc64aa9d7f" + integrity sha512-gDEi4LxIGLbdfjrjiY45QNbuDmpkwh9DX4xzrR2AzjjXpxwGyfSpbJaYhXARw9p17VH0h9UewnNQXNwaQyYMDA== + dependencies: + "@smithy/types" "^1.2.0" + "@smithy/util-uri-escape" "^1.1.0" + tslib "^2.5.0" + +"@smithy/querystring-builder@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-3.0.10.tgz#db8773af85ee3977c82b8e35a5cdd178c621306d" + integrity sha512-nT9CQF3EIJtIUepXQuBFb8dxJi3WVZS3XfuDksxSCSn+/CzZowRLdhDn+2acbBv8R6eaJqPupoI/aRFIImNVPQ== + dependencies: + "@smithy/types" "^3.7.1" + "@smithy/util-uri-escape" "^3.0.0" + tslib "^2.6.2" + +"@smithy/querystring-parser@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-3.0.10.tgz#62db744a1ed2cf90f4c08d2c73d365e033b4a11c" + integrity sha512-Oa0XDcpo9SmjhiDD9ua2UyM3uU01ZTuIrNdZvzwUTykW1PM8o2yJvMh1Do1rY5sUQg4NDV70dMi0JhDx4GyxuQ== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/service-error-classification@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-3.0.10.tgz#941c549daf0e9abb84d3def1d9e1e3f0f74f5ba6" + integrity sha512-zHe642KCqDxXLuhs6xmHVgRwy078RfqxP2wRDpIyiF8EmsWXptMwnMwbVa50lw+WOGNrYm9zbaEg0oDe3PTtvQ== + dependencies: + "@smithy/types" "^3.7.1" + +"@smithy/shared-ini-file-loader@^3.1.10", "@smithy/shared-ini-file-loader@^3.1.11": + version "3.1.11" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.11.tgz#0b4f98c4a66480956fbbefc4627c5dc09d891aea" + integrity sha512-AUdrIZHFtUgmfSN4Gq9nHu3IkHMa1YDcN+s061Nfm+6pQ0mJy85YQDB0tZBCmls0Vuj22pLwDPmL92+Hvfwwlg== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/signature-v4@^4.2.2": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-4.2.3.tgz#abbca5e5fe9158422b3125b2956791a325a27f22" + integrity sha512-pPSQQ2v2vu9vc8iew7sszLd0O09I5TRc5zhY71KA+Ao0xYazIG+uLeHbTJfIWGO3BGVLiXjUr3EEeCcEQLjpWQ== + dependencies: + "@smithy/is-array-buffer" "^3.0.0" + "@smithy/protocol-http" "^4.1.7" + "@smithy/types" "^3.7.1" + "@smithy/util-hex-encoding" "^3.0.0" + "@smithy/util-middleware" "^3.0.10" + "@smithy/util-uri-escape" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/smithy-client@^3.4.3", "@smithy/smithy-client@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-3.4.4.tgz#460870dc97d945fa2f390890359cf09d01131e0f" + integrity sha512-dPGoJuSZqvirBq+yROapBcHHvFjChoAQT8YPWJ820aPHHiowBlB3RL1Q4kPT1hx0qKgJuf+HhyzKi5Gbof4fNA== + dependencies: + "@smithy/core" "^2.5.3" + "@smithy/middleware-endpoint" "^3.2.3" + "@smithy/middleware-stack" "^3.0.10" + "@smithy/protocol-http" "^4.1.7" + "@smithy/types" "^3.7.1" + "@smithy/util-stream" "^3.3.1" + tslib "^2.6.2" + +"@smithy/types@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-1.2.0.tgz#9dc65767b0ee3d6681704fcc67665d6fc9b6a34e" + integrity sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA== dependencies: tslib "^2.5.0" -"@smithy/config-resolver@^2.0.10", "@smithy/config-resolver@^2.0.11": - version "2.0.11" - resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-2.0.11.tgz#20c4711b4e80f94527ee9e4e092cf024471bb09d" - integrity "sha1-IMRxG06A+UUn7p5OCSzwJEcbsJ0= sha512-q97FnlUmbai1c4JlQJgLVBsvSxgV/7Nvg/JK76E1nRq/U5UM56Eqo3dn2fY7JibqgJLg4LPsGdwtIyqyOk35CQ==" +"@smithy/types@^3.7.0", "@smithy/types@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.7.1.tgz#4af54c4e28351e9101996785a33f2fdbf93debe7" + integrity sha512-XKLcLXZY7sUQgvvWyeaL/qwNPp6V3dWcUjqrQKjSb+tzYiCy340R/c64LV5j+Tnb2GhmunEX0eou+L+m2hJNYA== dependencies: - "@smithy/node-config-provider" "^2.0.13" - "@smithy/types" "^2.3.4" - "@smithy/util-config-provider" "^2.0.0" - "@smithy/util-middleware" "^2.0.3" - tslib "^2.5.0" + tslib "^2.6.2" -"@smithy/credential-provider-imds@^2.0.0", "@smithy/credential-provider-imds@^2.0.13": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-2.0.13.tgz#9904912bc236d25d870add10b6eb138570bf5732" - integrity "sha1-mQSRK8I20l2HCt0QtusThXC/VzI= sha512-/xe3wNoC4j+BeTemH9t2gSKLBfyZmk8LXB2pQm/TOEYi+QhBgT+PSolNDfNAhrR68eggNE17uOimsrnwSkCt4w==" +"@smithy/url-parser@^3.0.10", "@smithy/url-parser@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-3.0.10.tgz#f389985a79766cff4a99af14979f01a17ce318da" + integrity sha512-j90NUalTSBR2NaZTuruEgavSdh8MLirf58LoGSk4AtQfyIymogIhgnGUU2Mga2bkMkpSoC9gxb74xBXL5afKAQ== dependencies: - "@smithy/node-config-provider" "^2.0.13" - "@smithy/property-provider" "^2.0.11" - "@smithy/types" "^2.3.4" - "@smithy/url-parser" "^2.0.10" - tslib "^2.5.0" + "@smithy/querystring-parser" "^3.0.10" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" -"@smithy/eventstream-codec@^2.0.10": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-2.0.10.tgz#dbd46d0ed13abc61b1f08ab249f3097602752933" - integrity "sha1-29RtDtE6vGGx8IqySfMJdgJ1KTM= sha512-3SSDgX2nIsFwif6m+I4+ar4KDcZX463Noes8ekBgQHitULiWvaDZX8XqPaRQSQ4bl1vbeVXHklJfv66MnVO+lw==" +"@smithy/util-base64@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-3.0.0.tgz#f7a9a82adf34e27a72d0719395713edf0e493017" + integrity sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ== dependencies: - "@aws-crypto/crc32" "3.0.0" - "@smithy/types" "^2.3.4" - "@smithy/util-hex-encoding" "^2.0.0" - tslib "^2.5.0" + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" -"@smithy/eventstream-serde-browser@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-2.0.10.tgz#93054f85194655d7eba27125f4935d247bdc2a8f" - integrity "sha1-kwVPhRlGVdfronEl9JNdJHvcKo8= sha512-/NSUNrWedO9Se80jo/2WcPvqobqCM/0drZ03Kqn1GZpGwVTsdqNj7frVTCUJs/W/JEzOShdMv8ewoKIR7RWPmA==" +"@smithy/util-body-length-browser@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz#86ec2f6256310b4845a2f064e2f571c1ca164ded" + integrity sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ== dependencies: - "@smithy/eventstream-serde-universal" "^2.0.10" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" + tslib "^2.6.2" -"@smithy/eventstream-serde-config-resolver@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-2.0.10.tgz#ea2f6675a4270fc3eccbb9fda4086f611887b510" - integrity "sha1-6i9mdaQnD8Psy7n9pAhvYRiHtRA= sha512-ag1U0vsC5rhRm7okFzsS6YsvyTRe62jIgJ82+Wr4qoOASx7eCDWdjoqLnrdDY0S4UToF9hZAyo4Du/xrSSSk4g==" +"@smithy/util-body-length-node@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz#99a291bae40d8932166907fe981d6a1f54298a6d" + integrity sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA== dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" + tslib "^2.6.2" -"@smithy/eventstream-serde-node@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.0.10.tgz#54af54b9719aa8f74fae5885a72e69b33d5661cf" - integrity "sha1-VK9UuXGaqPdPrliFpy5psz1WYc8= sha512-3+VeofxoVCa+dvqcuzEpnFve8EQJKaYR7UslDFpj6UTZfa7Hxr8o1/cbFkTftFo71PxzYVsR+bsD56EbAO432A==" +"@smithy/util-buffer-from@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz#6fc88585165ec73f8681d426d96de5d402021e4b" + integrity sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA== dependencies: - "@smithy/eventstream-serde-universal" "^2.0.10" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" + "@smithy/is-array-buffer" "^2.2.0" + tslib "^2.6.2" -"@smithy/eventstream-serde-universal@^2.0.10": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.0.10.tgz#575a6160a12508341c9c345bf3da7422a590aaae" - integrity "sha1-V1phYKElCDQcnDRb89p0IqWQqq4= sha512-JhJJU1ULLsn5kxKfFe8zOF2tibjxlPIvIB71Kn20aa/OFs+lvXBR0hBGswpovyYyckXH3qU8VxuIOEuS+2G+3A==" +"@smithy/util-buffer-from@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz#559fc1c86138a89b2edaefc1e6677780c24594e3" + integrity sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA== dependencies: - "@smithy/eventstream-codec" "^2.0.10" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" + "@smithy/is-array-buffer" "^3.0.0" + tslib "^2.6.2" -"@smithy/fetch-http-handler@^2.1.5", "@smithy/fetch-http-handler@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-2.2.1.tgz#a8abbd339c2c3d76456f4d16e65cf934727fc7ad" - integrity "sha1-qKu9M5wsPXZFb00W5lz5NHJ/x60= sha512-bXyM8PBAIKxVV++2ZSNBEposTDjFQ31XWOdHED+2hWMNvJHUoQqFbECg/uhcVOa6vHie2/UnzIZfXBSTpDBnEw==" +"@smithy/util-config-provider@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz#62c6b73b22a430e84888a8f8da4b6029dd5b8efe" + integrity sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ== dependencies: - "@smithy/protocol-http" "^3.0.6" - "@smithy/querystring-builder" "^2.0.10" - "@smithy/types" "^2.3.4" - "@smithy/util-base64" "^2.0.0" - tslib "^2.5.0" + tslib "^2.6.2" -"@smithy/hash-blob-browser@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-2.0.10.tgz#fa761e02c9a21b9c4bf827139d65376d50356c69" - integrity "sha1-+nYeAsmiG5xL+CcTnWU3bVA1bGk= sha512-U2+wIWWloOZ9DaRuz2sk9f7A6STRTlwdcv+q6abXDvS0TRDk8KGgUmfV5lCZy8yxFxZIA0hvHDNqcd25r4Hrew==" +"@smithy/util-defaults-mode-browser@^3.0.26": + version "3.0.27" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.27.tgz#d5df39faee8ad4bb5a6920b208469caa9dda2ccb" + integrity sha512-GV8NvPy1vAGp7u5iD/xNKUxCorE4nQzlyl057qRac+KwpH5zq8wVq6rE3lPPeuFLyQXofPN6JwxL1N9ojGapiQ== dependencies: - "@smithy/chunked-blob-reader" "^2.0.0" - "@smithy/chunked-blob-reader-native" "^2.0.0" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/hash-node@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-2.0.10.tgz#af13889a008880bdc30278b148e0e0b2a6e2d243" - integrity "sha1-rxOImgCIgL3DAnixSODgsqbi0kM= sha512-jSTf6uzPk/Vf+8aQ7tVXeHfjxe9wRXSCqIZcBymSDTf7/YrVxniBdpyN74iI8ZUOx/Pyagc81OK5FROLaEjbXQ==" - dependencies: - "@smithy/types" "^2.3.4" - "@smithy/util-buffer-from" "^2.0.0" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - -"@smithy/hash-stream-node@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-2.0.10.tgz#6e693b4362fbb031b8fc60e105220874d044ec8d" - integrity "sha1-bmk7Q2L7sDG4/GDhBSIIdNBE7I0= sha512-L58XEGrownZZSpF7Lp0gc0hy+eYKXuPgNz3pQgP5lPFGwBzHdldx2X6o3c6swD6RkcPvTRh0wTUVVGwUotbgnQ==" - dependencies: - "@smithy/types" "^2.3.4" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - -"@smithy/invalid-dependency@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-2.0.10.tgz#b708e7cfc35214ce664db6aa67465567b97ffd36" - integrity "sha1-twjnz8NSFM5mTbaqZ0ZVZ7l//TY= sha512-zw9p/zsmJ2cFcW4KMz3CJoznlbRvEA6HG2mvEaX5eAca5dq4VGI2MwPDTfmteC/GsnURS4ogoMQ0p6aHM2SDVQ==" - dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/is-array-buffer@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz#8fa9b8040651e7ba0b2f6106e636a91354ff7d34" - integrity "sha1-j6m4BAZR57oLL2EG5japE1T/fTQ= sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==" - dependencies: - tslib "^2.5.0" - -"@smithy/md5-js@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-2.0.10.tgz#8480de1b42abc581cf515e2b8e35542e9248f520" - integrity "sha1-hIDeG0KrxYHPUV4rjjVULpJI9SA= sha512-eA/Ova4/UdQUbMlrbBmnewmukH0zWU6C67HFFR/719vkFNepbnliGjmGksQ9vylz9eD4nfGkZZ5NKZMAcUuzjQ==" - dependencies: - "@smithy/types" "^2.3.4" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - -"@smithy/middleware-content-length@^2.0.11": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-2.0.12.tgz#e6f874f5eef880561f774a4376b73f04b97efc53" - integrity "sha1-5vh09e74gFYfd0pDdrc/BLl+/FM= sha512-QRhJTo5TjG7oF7np6yY4ZO9GDKFVzU/GtcqUqyEa96bLHE3yZHgNmsolOQ97pfxPHmFhH4vDP//PdpAIN3uI1Q==" - dependencies: - "@smithy/protocol-http" "^3.0.6" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/middleware-endpoint@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-2.0.10.tgz#c11d9f75549116453eea0e812e17ec7917ce5bb1" - integrity "sha1-wR2fdVSRFkU+6g6BLhfseRfOW7E= sha512-O6m4puZc16xfenotZUHL4bRlMrwf4gTp+0I5l954M5KNd3dOK18P+FA/IIUgnXF/dX6hlCUcJkBp7nAzwrePKA==" - dependencies: - "@smithy/middleware-serde" "^2.0.10" - "@smithy/types" "^2.3.4" - "@smithy/url-parser" "^2.0.10" - "@smithy/util-middleware" "^2.0.3" - tslib "^2.5.0" - -"@smithy/middleware-retry@^2.0.12": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-2.0.13.tgz#ef33b1511a4b01a77e54567165b78e6d0c266e88" - integrity "sha1-7zOxURpLAad+VFZxZbeObQwmbog= sha512-zuOva8xgWC7KYG8rEXyWIcZv2GWszO83DCTU6IKcf/FKu6OBmSE+EYv3EUcCGY+GfiwCX0EyJExC9Lpq9b0w5Q==" - dependencies: - "@smithy/node-config-provider" "^2.0.13" - "@smithy/protocol-http" "^3.0.6" - "@smithy/service-error-classification" "^2.0.3" - "@smithy/types" "^2.3.4" - "@smithy/util-middleware" "^2.0.3" - "@smithy/util-retry" "^2.0.3" - tslib "^2.5.0" - uuid "^8.3.2" - -"@smithy/middleware-serde@^2.0.10", "@smithy/middleware-serde@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-2.0.10.tgz#4b0e5f838c7d7621cabf7cfdd6cec4c7f4d52a3f" - integrity "sha1-Sw5fg4x9diHKv3z91s7Ex/TVKj8= sha512-+A0AFqs768256H/BhVEsBF6HijFbVyAwYRVXY/izJFkTalVWJOp4JA0YdY0dpXQd+AlW0tzs+nMQCE1Ew+DcgQ==" - dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/middleware-stack@^2.0.2", "@smithy/middleware-stack@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-2.0.4.tgz#cf199dd4d6eb3a3562e6757804faa91165693395" - integrity "sha1-zxmd1NbrOjVi5nV4BPqpEWVpM5U= sha512-MW0KNKfh8ZGLagMZnxcLJWPNXoKqW6XV/st5NnCBmmA2e2JhrUjU0AJ5Ca/yjTyNEKs3xH7AQDwp1YmmpEpmQQ==" - dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/node-config-provider@^2.0.12", "@smithy/node-config-provider@^2.0.13": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-2.0.13.tgz#26c95cebbb8bf9ef5dd703ab4e00ff80de34e15f" - integrity "sha1-Jslc67uL+e9d1wOrTgD/gN404V8= sha512-pPpLqYuJcOq1sj1EGu+DoZK47DUS4gepqSTNgRezmrjnzNlSU2/Dcc9Ebzs+WZ0Z5vXKazuE+k+NksFLo07/AA==" - dependencies: - "@smithy/property-provider" "^2.0.11" - "@smithy/shared-ini-file-loader" "^2.0.12" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/node-http-handler@^2.1.5", "@smithy/node-http-handler@^2.1.6": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-2.1.6.tgz#c2913363bbf28f315461bd54ef9a5394f1686776" - integrity "sha1-wpEzY7vyjzFUYb1U75pTlPFoZ3Y= sha512-NspvD3aCwiUNtoSTcVHz0RZz1tQ/SaRIe1KPF+r0mAdCZ9eWuhIeJT8ZNPYa1ITn7/Lgg64IyFjqPynZ8KnYQw==" - dependencies: - "@smithy/abort-controller" "^2.0.10" - "@smithy/protocol-http" "^3.0.6" - "@smithy/querystring-builder" "^2.0.10" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/property-provider@^2.0.0", "@smithy/property-provider@^2.0.11": - version "2.0.11" - resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-2.0.11.tgz#c6e03e4f6f886851339c3dfaf8cd8ae3b2878fa3" - integrity "sha1-xuA+T2+IaFEznD36+M2K47KHj6M= sha512-kzuOadu6XvrnlF1iXofpKXYmo4oe19st9/DE8f5gHNaFepb4eTkR8gD8BSdTnNnv7lxfv6uOwZPg4VS6hemX1w==" - dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/protocol-http@^3.0.5", "@smithy/protocol-http@^3.0.6": - version "3.0.6" - resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-3.0.6.tgz#c33c128cc0f7096bf4fcdcc6d14d156ba5cd5b7c" - integrity "sha1-wzwSjMD3CWv0/NzG0U0Va6XNW3w= sha512-F0jAZzwznMmHaggiZgc7YoS08eGpmLvhVktY/Taz6+OAOHfyIqWSDNgFqYR+WHW9z5fp2XvY4mEUrQgYMQ71jw==" - dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/querystring-builder@^2.0.10": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-2.0.10.tgz#b06aa958b6ec1c56254d8cc41a19882625fd1c05" - integrity "sha1-sGqpWLbsHFYlTYzEGhmIJiX9HAU= sha512-uujJGp8jzrrU1UHme8sUKEbawQTcTmUWsh8rbGXYD/lMwNLQ+9jQ9dMDWbbH9Hpoa9RER1BeL/38WzGrbpob2w==" - dependencies: - "@smithy/types" "^2.3.4" - "@smithy/util-uri-escape" "^2.0.0" - tslib "^2.5.0" - -"@smithy/querystring-parser@^2.0.10": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-2.0.10.tgz#074d770a37feafb0d550094dd8463bdff58515f5" - integrity "sha1-B013Cjf+r7DVUAlN2EY73/WFFfU= sha512-WSD4EU60Q8scacT5PIpx4Bahn6nWpt+MiYLcBkFt6fOj7AssrNeaNIU2Z0g40ftVmrwLcEOIKGX92ynbVDb3ZA==" - dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/service-error-classification@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-2.0.3.tgz#4c7de61d06db5f72437557d429bd74c74988b19e" - integrity "sha1-TH3mHQbbX3JDdVfUKb10x0mIsZ4= sha512-b+m4QCHXb7oKAkM/jHwHrl5gpqhFoMTHF643L0/vAEkegrcUWyh1UjyoHttuHcP5FnHVVy4EtpPtLkEYD+xMFw==" - dependencies: - "@smithy/types" "^2.3.4" - -"@smithy/shared-ini-file-loader@^2.0.12", "@smithy/shared-ini-file-loader@^2.0.6": - version "2.0.12" - resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.0.12.tgz#30c8a7a36f49734fde2f052bfaeaaf40c1980b55" - integrity "sha1-MMino29Jc0/eLwUr+uqvQMGYC1U= sha512-umi0wc4UBGYullAgYNUVfGLgVpxQyES47cnomTqzCKeKO5oudO4hyDNj+wzrOjqDFwK2nWYGVgS8Y0JgGietrw==" - dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/signature-v4@^2.0.0": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-2.0.10.tgz#89161b3f59071b77713cdf06f98b2e6780580742" - integrity "sha1-iRYbP1kHG3dxPN8G+YsuZ4BYB0I= sha512-S6gcP4IXfO/VMswovrhxPpqvQvMal7ZRjM4NvblHSPpE5aNBYx67UkHFF3kg0hR3tJKqNpBGbxwq0gzpdHKLRA==" - dependencies: - "@smithy/eventstream-codec" "^2.0.10" - "@smithy/is-array-buffer" "^2.0.0" - "@smithy/types" "^2.3.4" - "@smithy/util-hex-encoding" "^2.0.0" - "@smithy/util-middleware" "^2.0.3" - "@smithy/util-uri-escape" "^2.0.0" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - -"@smithy/smithy-client@^2.1.6", "@smithy/smithy-client@^2.1.9": - version "2.1.9" - resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-2.1.9.tgz#5a0a185947ae4e66d12d2a6135628dd2fc36924c" - integrity "sha1-WgoYWUeuTmbRLSphNWKN0vw2kkw= sha512-HTicQSn/lOcXKJT+DKJ4YMu51S6PzbWsO8Z6Pwueo30mSoFKXg5P0BDkg2VCDqCVR0mtddM/F6hKhjW6YAV/yg==" - dependencies: - "@smithy/middleware-stack" "^2.0.4" - "@smithy/types" "^2.3.4" - "@smithy/util-stream" "^2.0.14" - tslib "^2.5.0" - -"@smithy/types@^2.3.3", "@smithy/types@^2.3.4": - version "2.3.4" - resolved "https://registry.yarnpkg.com/@smithy/types/-/types-2.3.4.tgz#3b9bc15000af0a0b1f4fda741f78c1580ba15e92" - integrity "sha1-O5vBUACvCgsfT9p0H3jBWAuhXpI= sha512-D7xlM9FOMFyFw7YnMXn9dK2KuN6+JhnrZwVt1fWaIu8hCk5CigysweeIT/H/nCo4YV+s8/oqUdLfexbkPZtvqw==" - dependencies: - tslib "^2.5.0" - -"@smithy/url-parser@^2.0.10", "@smithy/url-parser@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-2.0.10.tgz#3261a463b87901d7686f66a9f26efb9f57d8d555" - integrity "sha1-MmGkY7h5Addob2ap8m77n1fY1VU= sha512-4TXQFGjHcqru8aH5VRB4dSnOFKCYNX6SR1Do6fwxZ+ExT2onLsh2W77cHpks7ma26W5jv6rI1u7d0+KX9F0aOw==" - dependencies: - "@smithy/querystring-parser" "^2.0.10" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/util-base64@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-2.0.0.tgz#1beeabfb155471d1d41c8d0603be1351f883c444" - integrity "sha1-G+6r+xVUcdHUHI0GA74TUfiDxEQ= sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==" - dependencies: - "@smithy/util-buffer-from" "^2.0.0" - tslib "^2.5.0" - -"@smithy/util-body-length-browser@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.0.tgz#5447853003b4c73da3bc5f3c5e82c21d592d1650" - integrity "sha1-VEeFMAO0xz2jvF88XoLCHVktFlA= sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==" - dependencies: - tslib "^2.5.0" - -"@smithy/util-body-length-node@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@smithy/util-body-length-node/-/util-body-length-node-2.1.0.tgz#313a5f7c5017947baf5fa018bfc22628904bbcfa" - integrity "sha1-MTpffFAXlHuvX6AYv8ImKJBLvPo= sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==" - dependencies: - tslib "^2.5.0" - -"@smithy/util-buffer-from@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz#7eb75d72288b6b3001bc5f75b48b711513091deb" - integrity "sha1-frddciiLazABvF91tItxFRMJHes= sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==" - dependencies: - "@smithy/is-array-buffer" "^2.0.0" - tslib "^2.5.0" - -"@smithy/util-config-provider@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-config-provider/-/util-config-provider-2.0.0.tgz#4dd6a793605559d94267312fd06d0f58784b4c38" - integrity "sha1-Tdank2BVWdlCZzEv0G0PWHhLTDg= sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==" - dependencies: - tslib "^2.5.0" - -"@smithy/util-defaults-mode-browser@^2.0.10": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.13.tgz#8136955f1bef6e66cb8a8702693e7685dcd33e26" - integrity "sha1-gTaVXxvvbmbLiocCaT52hdzTPiY= sha512-UmmOdUzaQjqdsl1EjbpEaQxM0VDFqTj6zDuI26/hXN7L/a1k1koTwkYpogHMvunDX3fjrQusg5gv1Td4UsGyog==" - dependencies: - "@smithy/property-provider" "^2.0.11" - "@smithy/smithy-client" "^2.1.9" - "@smithy/types" "^2.3.4" + "@smithy/property-provider" "^3.1.10" + "@smithy/smithy-client" "^3.4.4" + "@smithy/types" "^3.7.1" bowser "^2.11.0" - tslib "^2.5.0" + tslib "^2.6.2" -"@smithy/util-defaults-mode-node@^2.0.12": - version "2.0.15" - resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.15.tgz#24f7b9de978206909ced7b522f24e7f450187372" - integrity "sha1-JPe53peCBpCc7XtSLyTn9FAYc3I= sha512-g6J7MHAibVPMTlXyH3mL+Iet4lMJKFVhsOhJmn+IKG81uy9m42CkRSDlwdQSJAcprLQBIaOPdFxNXQvrg2w1Uw==" +"@smithy/util-defaults-mode-node@^3.0.26": + version "3.0.27" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.27.tgz#a7248c9d9cb620827ab57ef9d1867bfe8aef42d0" + integrity sha512-7+4wjWfZqZxZVJvDutO+i1GvL6bgOajEkop4FuR6wudFlqBiqwxw3HoH6M9NgeCd37km8ga8NPp2JacQEtAMPg== dependencies: - "@smithy/config-resolver" "^2.0.11" - "@smithy/credential-provider-imds" "^2.0.13" - "@smithy/node-config-provider" "^2.0.13" - "@smithy/property-provider" "^2.0.11" - "@smithy/smithy-client" "^2.1.9" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" + "@smithy/config-resolver" "^3.0.12" + "@smithy/credential-provider-imds" "^3.2.7" + "@smithy/node-config-provider" "^3.1.11" + "@smithy/property-provider" "^3.1.10" + "@smithy/smithy-client" "^3.4.4" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" -"@smithy/util-hex-encoding@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz#0aa3515acd2b005c6d55675e377080a7c513b59e" - integrity "sha1-CqNRWs0rAFxtVWdeN3CAp8UTtZ4= sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==" +"@smithy/util-endpoints@^2.1.5": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-2.1.6.tgz#720cbd1a616ad7c099b77780f0cb0f1f9fc5d2df" + integrity sha512-mFV1t3ndBh0yZOJgWxO9J/4cHZVn5UG1D8DeCc6/echfNkeEJWu9LD7mgGH5fHrEdR7LDoWw7PQO6QiGpHXhgA== + dependencies: + "@smithy/node-config-provider" "^3.1.11" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/util-hex-encoding@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz#32938b33d5bf2a15796cd3f178a55b4155c535e6" + integrity sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ== + dependencies: + tslib "^2.6.2" + +"@smithy/util-middleware@^3.0.10", "@smithy/util-middleware@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-3.0.10.tgz#ab8be99f1aaafe5a5490c344f27a264b72b7592f" + integrity sha512-eJO+/+RsrG2RpmY68jZdwQtnfsxjmPxzMlQpnHKjFPwrYqvlcT+fHdT+ZVwcjlWSrByOhGr9Ff2GG17efc192A== + dependencies: + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/util-retry@^3.0.10", "@smithy/util-retry@^3.0.9": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-3.0.10.tgz#fc13e1b30e87af0cbecadf29ca83b171e2040440" + integrity sha512-1l4qatFp4PiU6j7UsbasUHL2VU023NRB/gfaa1M0rDqVrRN4g3mCArLRyH3OuktApA4ye+yjWQHjdziunw2eWA== + dependencies: + "@smithy/service-error-classification" "^3.0.10" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" + +"@smithy/util-stream@^3.3.0", "@smithy/util-stream@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-3.3.1.tgz#a2636f435637ef90d64df2bb8e71cd63236be112" + integrity sha512-Ff68R5lJh2zj+AUTvbAU/4yx+6QPRzg7+pI7M1FbtQHcRIp7xvguxVsQBKyB3fwiOwhAKu0lnNyYBaQfSW6TNw== + dependencies: + "@smithy/fetch-http-handler" "^4.1.1" + "@smithy/node-http-handler" "^3.3.1" + "@smithy/types" "^3.7.1" + "@smithy/util-base64" "^3.0.0" + "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-hex-encoding" "^3.0.0" + "@smithy/util-utf8" "^3.0.0" + tslib "^2.6.2" + +"@smithy/util-uri-escape@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-1.1.0.tgz#a8c5edaf19c0efdb9b51661e840549cf600a1808" + integrity sha512-/jL/V1xdVRt5XppwiaEU8Etp5WHZj609n0xMTuehmCqdoOFbId1M+aEeDWZsQ+8JbEB/BJ6ynY2SlYmOaKtt8w== dependencies: tslib "^2.5.0" -"@smithy/util-middleware@^2.0.2", "@smithy/util-middleware@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-2.0.3.tgz#478cbf957eaffa36aed624350be342bbf15d3c42" - integrity "sha1-R4y/lX6v+jau1iQ1C+NCu/FdPEI= sha512-+FOCFYOxd2HO7v/0hkFSETKf7FYQWa08wh/x/4KUeoVBnLR4juw8Qi+TTqZI6E2h5LkzD9uOaxC9lAjrpVzaaA==" +"@smithy/util-uri-escape@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz#e43358a78bf45d50bb736770077f0f09195b6f54" + integrity sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg== dependencies: - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/util-retry@^2.0.2", "@smithy/util-retry@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-2.0.3.tgz#a053855ddb51800bd679da03454cf626bc440918" - integrity "sha1-oFOFXdtRgAvWedoDRUz2JrxECRg= sha512-gw+czMnj82i+EaH7NL7XKkfX/ZKrCS2DIWwJFPKs76bMgkhf0y1C94Lybn7f8GkBI9lfIOUdPYtzm19zQOC8sw==" - dependencies: - "@smithy/service-error-classification" "^2.0.3" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" - -"@smithy/util-stream@^2.0.12", "@smithy/util-stream@^2.0.14": - version "2.0.14" - resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-2.0.14.tgz#3fdd934e2bced80331dcaff18aefbcfe39ebf3cd" - integrity "sha1-P92TTivO2AMx3K/xiu+8/jnr880= sha512-XjvlDYe+9DieXhLf7p+EgkXwFtl34kHZcWfHnc5KaILbhyVfDLWuqKTFx6WwCFqb01iFIig8trGwExRIqqkBYg==" - dependencies: - "@smithy/fetch-http-handler" "^2.2.1" - "@smithy/node-http-handler" "^2.1.6" - "@smithy/types" "^2.3.4" - "@smithy/util-base64" "^2.0.0" - "@smithy/util-buffer-from" "^2.0.0" - "@smithy/util-hex-encoding" "^2.0.0" - "@smithy/util-utf8" "^2.0.0" - tslib "^2.5.0" - -"@smithy/util-uri-escape@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz#19955b1a0f517a87ae77ac729e0e411963dfda95" - integrity "sha1-GZVbGg9Reoeud6xyng5BGWPf2pU= sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==" - dependencies: - tslib "^2.5.0" + tslib "^2.6.2" "@smithy/util-utf8@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.0.0.tgz#b4da87566ea7757435e153799df9da717262ad42" - integrity "sha1-tNqHVm6ndXQ14VN5nfnacXJirUI= sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==" + version "2.3.0" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.3.0.tgz#dd96d7640363259924a214313c3cf16e7dd329c5" + integrity sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A== dependencies: - "@smithy/util-buffer-from" "^2.0.0" - tslib "^2.5.0" + "@smithy/util-buffer-from" "^2.2.0" + tslib "^2.6.2" -"@smithy/util-waiter@^2.0.9": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-2.0.10.tgz#6cd28af8340ab54fa9adf10d193c4476a5673363" - integrity "sha1-bNKK+DQKtU+prfENGTxEdqVnM2M= sha512-yQjwWVrwYw+/f3hFQccE3zZF7lk6N6xtNcA6jvhWFYhnyKAm6B2mX8Gzftl0TbgoPUpzCvKYlvhaEpVtRpVfVw==" +"@smithy/util-utf8@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-3.0.0.tgz#1a6a823d47cbec1fd6933e5fc87df975286d9d6a" + integrity sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA== dependencies: - "@smithy/abort-controller" "^2.0.10" - "@smithy/types" "^2.3.4" - tslib "^2.5.0" + "@smithy/util-buffer-from" "^3.0.0" + tslib "^2.6.2" + +"@smithy/util-waiter@^3.1.8": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-3.1.9.tgz#1330ce2e79b58419d67755d25bce7a226e32dc6d" + integrity sha512-/aMXPANhMOlMPjfPtSrDfPeVP8l56SJlz93xeiLmhLe5xvlXA5T3abZ2ilEsDEPeY9T/wnN/vNGn9wa1SbufWA== + dependencies: + "@smithy/abort-controller" "^3.1.8" + "@smithy/types" "^3.7.1" + tslib "^2.6.2" "@socket.io/component-emitter@~3.1.0": version "3.1.0" @@ -5219,14 +5290,7 @@ dependencies: defer-to-connect "^1.0.1" -"@szmarczak/http-timer@^4.0.5": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" - integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== - dependencies: - defer-to-connect "^2.0.0" - -"@techpass/passport-openidconnect@0.3.3", "@techpass/passport-openidconnect@^0.3.0": +"@techpass/passport-openidconnect@0.3.3": version "0.3.3" resolved "https://registry.yarnpkg.com/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.3.tgz#6c01c78bd8da0ca8917378dfbe18024702620352" integrity sha512-i2X/CofjnGBqpTmw6b+Ex3Co/NrR2xjnIHvnOJk62XIlJJHNSTwmhJ1PkXoA5RGKlxZWchADFGjLTJnebvRj7A== @@ -5237,15 +5301,15 @@ request "^2.88.0" webfinger "^0.4.2" -"@techteamer/ocsp@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@techteamer/ocsp/-/ocsp-1.0.0.tgz#7b82b02093fbe351e915bb37685ac1ac5a1233d3" - integrity sha512-lNAOoFHaZN+4huo30ukeqVrUmfC+avoEBYQ11QAnAw1PFhnI5oBCg8O/TNiCoEWix7gNGBIEjrQwtPREqKMPog== +"@techteamer/ocsp@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@techteamer/ocsp/-/ocsp-1.0.1.tgz#420f80c64ff0f74a70b65c88e4031c03a9da6ded" + integrity sha512-q4pW5wAC6Pc3JI8UePwE37CkLQ5gDGZMgjSX4MEEm4D4Di59auDQ8UNIDzC4gRnPNmmcwjpPxozq8p5pjiOmOw== dependencies: asn1.js "^5.4.1" asn1.js-rfc2560 "^5.0.1" asn1.js-rfc5280 "^3.0.0" - async "^3.2.1" + async "^3.2.4" simple-lru-cache "^0.0.2" "@tediousjs/connection-string@^0.5.0": @@ -5393,13 +5457,6 @@ dependencies: "@babel/types" "^7.3.0" -"@types/bcrypt@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.2.tgz#22fddc11945ea4fbc3655b3e8b8847cc9f811477" - integrity sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ== - dependencies: - "@types/node" "*" - "@types/bluebird@*": version "3.5.38" resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.38.tgz#7a671e66750ccd21c9fc9d264d0e1e5330bc9908" @@ -5413,16 +5470,6 @@ "@types/connect" "*" "@types/node" "*" -"@types/cacheable-request@^6.0.1": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" - integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== - dependencies: - "@types/http-cache-semantics" "*" - "@types/keyv" "^3.1.4" - "@types/node" "*" - "@types/responselike" "^1.0.0" - "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -5553,21 +5600,6 @@ "@types/docker-modem" "*" "@types/node" "*" -"@types/eslint@*": - version "9.6.1" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584" - integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/eslint__js@^8.42.3": - version "8.42.3" - resolved "https://registry.yarnpkg.com/@types/eslint__js/-/eslint__js-8.42.3.tgz#d1fa13e5c1be63a10b4e3afe992779f81c1179a0" - integrity sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw== - dependencies: - "@types/eslint" "*" - "@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0", "@types/estree@^1.0.1": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -5621,11 +5653,6 @@ resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661" integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA== -"@types/http-cache-semantics@*": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" - integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== - "@types/http-errors@*": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" @@ -5665,7 +5692,7 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -5687,13 +5714,6 @@ resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== -"@types/keyv@^3.1.4": - version "3.1.4" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" - integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== - dependencies: - "@types/node" "*" - "@types/koa-compose@*": version "3.2.5" resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" @@ -5701,37 +5721,13 @@ dependencies: "@types/koa" "*" -"@types/koa-passport@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/koa-passport/-/koa-passport-4.0.3.tgz#063ec6310edee76cf854aadaa717b97f04b104fb" - integrity sha512-tNMYd/bcv0Zw7fc0CzEBYM9uUzVtn4XWzdUYfkTgSkEljP6nap7eI4E5x43ukrUQvztgXSYFkz3Uk+ujFeUzTg== - dependencies: - "@types/koa" "*" - "@types/passport" "*" - -"@types/koa-send@*", "@types/koa-send@^4.1.6": +"@types/koa-send@^4.1.6": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/koa-send/-/koa-send-4.1.6.tgz#15d90e95e3ccce669a15b6a3c56c3a650a167cea" integrity sha512-vgnNGoOJkx7FrF0Jl6rbK1f8bBecqAchKpXtKuXzqIEdXTDO6dsSTjr+eZ5m7ltSjH4K/E7auNJEQCAd0McUPA== dependencies: "@types/koa" "*" -"@types/koa-session@^6.4.5": - version "6.4.5" - resolved "https://registry.yarnpkg.com/@types/koa-session/-/koa-session-6.4.5.tgz#ac10bac507f4bb722fa6c55c33607b5c8769f779" - integrity sha512-Vc6+fslnPuMH2v9y80WYeo39UMo8mweuNNthKCwYU2ZE6l5vnRrzRU3BRvexKwsoI5sxsRl5CxDsBlLI8kY/XA== - dependencies: - "@types/cookies" "*" - "@types/koa" "*" - -"@types/koa-static@^4.0.2": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/koa-static/-/koa-static-4.0.4.tgz#ce6f2a5d14cc7ef19f9bf6ee8e4f3eadfcc77323" - integrity sha512-j1AUzzl7eJYEk9g01hNTlhmipFh8RFbOQmaMNLvLcNNAkPw0bdTs3XTa3V045XFlrWN0QYnblbDJv2RzawTn6A== - dependencies: - "@types/koa" "*" - "@types/koa-send" "*" - "@types/koa@*": version "2.13.5" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.5.tgz#64b3ca4d54e08c0062e89ec666c9f45443b21a61" @@ -5760,17 +5756,17 @@ "@types/koa-compose" "*" "@types/node" "*" -"@types/koa__cors@^5.0.0": +"@types/koa__cors@5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-5.0.0.tgz#74567a045b599266e2cd3940cef96cedecc2ef1f" integrity sha512-LCk/n25Obq5qlernGOK/2LUwa/2YJb2lxHUkkvYFDOpLXlVI6tKcdfCHRBQnOY4LwH6el5WOLs6PD/a8Uzau6g== dependencies: "@types/koa" "*" -"@types/koa__router@8.0.8": - version "8.0.8" - resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-8.0.8.tgz#b1e0e9a512498777d3366bbdf0e853df27ec831c" - integrity sha512-9pGCaDtzCsj4HJ8HmGuqzk8+s57sPj4njWd08GG5o92n5Xp9io2snc40CPpXFhoKcZ8OKhuu6ht4gNou9e1C2w== +"@types/koa__router@12.0.4": + version "12.0.4" + resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-12.0.4.tgz#a1f9afec9dc7e7d9fa1252d1938c44b403e19a28" + integrity sha512-Y7YBbSmfXZpa/m5UGGzb7XadJIRBRnwNY9cdAojZGp65Cpe5MAP3mOZE7e3bImt8dfKS4UFcR16SLH8L/z7PBw== dependencies: "@types/koa" "*" @@ -5814,10 +5810,10 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/mssql@9.1.4": - version "9.1.4" - resolved "https://registry.yarnpkg.com/@types/mssql/-/mssql-9.1.4.tgz#d485b06494a76d15b957e0952305c55053bac366" - integrity sha512-st2ryK+viraRuptxcGs+66J0RrABytxhGxUlpWcOniNPzpnxIaeNhPJVM3lZn1r+s/6lQARYID6Z+MBoseSD8g== +"@types/mssql@9.1.5": + version "9.1.5" + resolved "https://registry.yarnpkg.com/@types/mssql/-/mssql-9.1.5.tgz#1574a5870aeb029c6d787861af101161b9b8d3b6" + integrity sha512-Q9EsgXwuRoX5wvUSu24YfbKMbFChv7pZ/jeCzPkj47ehcuXYsBcfogwrtVFosSjinD4Q/MY2YPGk9Yy1cM2Ywg== dependencies: "@types/node" "*" "@types/tedious" "*" @@ -5831,7 +5827,15 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node-fetch@^2.5.0", "@types/node-fetch@^2.6.4": +"@types/node-fetch@^2.5.0": + version "2.6.12" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + +"@types/node-fetch@^2.6.4": version "2.6.11" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== @@ -5839,7 +5843,7 @@ "@types/node" "*" form-data "^4.0.0" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.13.4", "@types/node@>=13.7.0", "@types/node@>=8.1.0": +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.13.4", "@types/node@>=13.7.0": version "22.5.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.1.tgz#de01dce265f6b99ed32b295962045d10b5b99560" integrity sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw== @@ -5858,16 +5862,23 @@ dependencies: undici-types "~5.26.4" -"@types/node@>=4.2.0 < 13", "@types/node@^12.20.52": - version "12.20.55" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" - integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@>=18", "@types/node@^22.9.0": + version "22.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.1.tgz#bdf91c36e0e7ecfb7257b2d75bf1b206b308ca71" + integrity sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg== + dependencies: + undici-types "~6.19.8" "@types/node@>=8.0.0 <15": version "14.18.37" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.37.tgz#0bfcd173e8e1e328337473a8317e37b3b14fd30d" integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg== +"@types/node@^12.20.52": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + "@types/node@^18.11.18": version "18.19.10" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.10.tgz#4de314ab66faf6bc8ba691021a091ddcdf13a158" @@ -5882,25 +5893,11 @@ dependencies: undici-types "~5.26.4" -"@types/nodemailer@^6.4.4": - version "6.4.15" - resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.15.tgz#494be695e11c438f7f5df738fb4ab740312a6ed2" - integrity sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ== - dependencies: - "@types/node" "*" - "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== -"@types/oauth@*": - version "0.9.5" - resolved "https://registry.yarnpkg.com/@types/oauth/-/oauth-0.9.5.tgz#acc4209bfa1c8d7d3aaf2c9ad0b32216a29616c1" - integrity sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog== - dependencies: - "@types/node" "*" - "@types/oracledb@6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@types/oracledb/-/oracledb-6.5.1.tgz#17d021cabc9d216dfa6d3d65ae3ee585c33baab3" @@ -5908,36 +5905,10 @@ dependencies: "@types/node" "*" -"@types/passport-google-oauth@^1.0.42": - version "1.0.45" - resolved "https://registry.yarnpkg.com/@types/passport-google-oauth/-/passport-google-oauth-1.0.45.tgz#c986c787ec9706b4a596d2bae43342b50b54973d" - integrity sha512-O3Y3DDKnf9lR8+DSaUOCEGF6aFjVYdI8TLhQYtySZ3Sq75c5tGYJ0KJRDZw0GsyLD/Que0nqFkP/GnDVwZZL9w== - dependencies: - "@types/express" "*" - "@types/passport" "*" - -"@types/passport-microsoft@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/passport-microsoft/-/passport-microsoft-1.0.0.tgz#a2ddc2200843570d38c35c53f6388e33df915b58" - integrity sha512-vD9ajSUc9Sz/8gdCj0ODUbPYQDxcI/imIDdgMPh//c5yMK/PgV6SNUXFLBzJo89Y30LU6bYAfXKn40WJqtMBiA== - dependencies: - "@types/passport-oauth2" "*" - -"@types/passport-oauth2@*": - version "1.4.17" - resolved "https://registry.yarnpkg.com/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz#d5d54339d44f6883d03e69dc0cc0e2114067abb4" - integrity sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg== - dependencies: - "@types/express" "*" - "@types/oauth" "*" - "@types/passport" "*" - -"@types/passport@*": - version "1.0.16" - resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.16.tgz#5a2918b180a16924c4d75c31254c31cdca5ce6cf" - integrity sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A== - dependencies: - "@types/express" "*" +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== "@types/pg@8.6.6": version "8.6.6" @@ -6073,10 +6044,10 @@ "@types/pouchdb-core" "*" "@types/pouchdb-find" "*" -"@types/pouchdb@6.4.0", "@types/pouchdb@^6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@types/pouchdb/-/pouchdb-6.4.0.tgz#f9c41ca64b23029f9bf2eb4bf6956e6431cb79f8" - integrity sha512-eGCpX+NXhd5VLJuJMzwe3L79fa9+IDTrAG3CPaf4s/31PD56hOrhDJTSmRELSXuiqXr6+OHzzP0PldSaWsFt7w== +"@types/pouchdb@6.4.2", "@types/pouchdb@^6.4.0": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@types/pouchdb/-/pouchdb-6.4.2.tgz#54777533d86f4abd1a3989b272e085323623bbe1" + integrity sha512-YsI47rASdtzR+3V3JE2UKY58snhm0AglHBpyckQBkRYoCbTvGagXHtV0x5n8nzN04jQmvTG+Sm85cIzKT3KXBA== dependencies: "@types/pouchdb-adapter-cordova-sqlite" "*" "@types/pouchdb-adapter-fruitdown" "*" @@ -6171,13 +6142,6 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== -"@types/responselike@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50" - integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw== - dependencies: - "@types/node" "*" - "@types/retry@*": version "0.12.5" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.5.tgz#f090ff4bd8d2e5b940ff270ab39fd5ca1834a07e" @@ -6208,13 +6172,6 @@ dependencies: "@types/node" "*" -"@types/server-destroy@^1.0.1": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@types/server-destroy/-/server-destroy-1.0.4.tgz#bd94af933e73e04795042edf38af267ddebd4e98" - integrity sha512-+x8oAQ4Xp1wtDi2Hlmi7gUNXZNVhB5EoSQpi0qEmINdDN5Ab724WLGAalEdT1SudVY/NzMhbfZO7vU+klT0R+A== - dependencies: - "@types/node" "*" - "@types/ssh2-streams@*": version "0.1.12" resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz#e68795ba2bf01c76b93f9c9809e1f42f0eaaec5f" @@ -6305,9 +6262,9 @@ integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== "@types/triple-beam@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8" - integrity sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g== + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" + integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== "@types/tunnel@^0.0.3": version "0.0.3" @@ -6316,7 +6273,7 @@ dependencies: "@types/node" "*" -"@types/uuid@8.3.4", "@types/uuid@^8.3.4": +"@types/uuid@8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== @@ -6366,7 +6323,7 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@7.18.0", "@typescript-eslint/eslint-plugin@^7.16.1": +"@typescript-eslint/eslint-plugin@7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz#b16d3cf3ee76bf572fdf511e79c248bdec619ea3" integrity sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw== @@ -6392,7 +6349,7 @@ "@typescript-eslint/visitor-keys" "6.9.0" debug "^4.3.4" -"@typescript-eslint/parser@7.18.0", "@typescript-eslint/parser@^7.16.1": +"@typescript-eslint/parser@7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.18.0.tgz#83928d0f1b7f4afa974098c64b5ce6f9051f96a0" integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg== @@ -6427,7 +6384,7 @@ "@typescript-eslint/types" "7.18.0" "@typescript-eslint/visitor-keys" "7.18.0" -"@typescript-eslint/type-utils@7.18.0", "@typescript-eslint/type-utils@^7.2.0": +"@typescript-eslint/type-utils@7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz#2165ffaee00b1fbbdd2d40aa85232dab6998f53b" integrity sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA== @@ -6510,7 +6467,7 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@7.18.0", "@typescript-eslint/utils@^7.3.1": +"@typescript-eslint/utils@7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.18.0.tgz#bca01cde77f95fc6a8d5b0dbcbfb3d6ca4be451f" integrity sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw== @@ -6580,23 +6537,6 @@ "@vitest/utils" "0.29.8" chai "^4.3.7" -"@vitest/expect@2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.0.5.tgz#f3745a6a2c18acbea4d39f5935e913f40d26fa86" - integrity sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA== - dependencies: - "@vitest/spy" "2.0.5" - "@vitest/utils" "2.0.5" - chai "^5.1.1" - tinyrainbow "^1.2.0" - -"@vitest/pretty-format@2.0.5", "@vitest/pretty-format@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.0.5.tgz#91d2e6d3a7235c742e1a6cc50e7786e2f2979b1e" - integrity sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ== - dependencies: - tinyrainbow "^1.2.0" - "@vitest/runner@0.29.8": version "0.29.8" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.29.8.tgz#ede8a7be8a074ea1180bc1d1595bd879ed15971c" @@ -6606,23 +6546,6 @@ p-limit "^4.0.0" pathe "^1.1.0" -"@vitest/runner@2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.0.5.tgz#89197e712bb93513537d6876995a4843392b2a84" - integrity sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig== - dependencies: - "@vitest/utils" "2.0.5" - pathe "^1.1.2" - -"@vitest/snapshot@2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.0.5.tgz#a2346bc5013b73c44670c277c430e0334690a162" - integrity sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew== - dependencies: - "@vitest/pretty-format" "2.0.5" - magic-string "^0.30.10" - pathe "^1.1.2" - "@vitest/spy@0.29.8": version "0.29.8" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.29.8.tgz#2e0c3b30e04d317b2197e3356234448aa432e131" @@ -6630,13 +6553,6 @@ dependencies: tinyspy "^1.0.2" -"@vitest/spy@2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.0.5.tgz#590fc07df84a78b8e9dd976ec2090920084a2b9f" - integrity sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA== - dependencies: - tinyspy "^3.0.0" - "@vitest/utils@0.29.8": version "0.29.8" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.29.8.tgz#423da85fd0c6633f3ab496cf7d2fc0119b850df8" @@ -6647,15 +6563,52 @@ loupe "^2.3.6" pretty-format "^27.5.1" -"@vitest/utils@2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.0.5.tgz#6f8307a4b6bc6ceb9270007f73c67c915944e926" - integrity sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ== +"@vue/compiler-core@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05" + integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q== dependencies: - "@vitest/pretty-format" "2.0.5" - estree-walker "^3.0.3" - loupe "^3.1.1" - tinyrainbow "^1.2.0" + "@babel/parser" "^7.25.3" + "@vue/shared" "3.5.13" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58" + integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA== + dependencies: + "@vue/compiler-core" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/compiler-sfc@^3.3.4": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46" + integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/compiler-core" "3.5.13" + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + estree-walker "^2.0.2" + magic-string "^0.30.11" + postcss "^8.4.48" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba" + integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/shared@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f" + integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ== "@xyflow/svelte@^0.1.18": version "0.1.18" @@ -6744,7 +6697,7 @@ abortcontroller-polyfill@^1.4.0: resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ== -abstract-leveldown@^6.2.1, abstract-leveldown@^6.3.0: +abstract-leveldown@^6.2.1: version "6.3.0" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a" integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ== @@ -6755,6 +6708,18 @@ abstract-leveldown@^6.2.1, abstract-leveldown@^6.3.0: level-supports "~1.0.0" xtend "~4.0.0" +abstract-leveldown@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz#08d19d4e26fb5be426f7a57004851b39e1795a2e" + integrity sha512-DnhQwcFEaYsvYDnACLZhMmCWd3rkOeEvglpa4q5i/5Jlm3UIsWaxVzuXvDLFCSCWRO3yy2/+V/G7FusFgejnfQ== + dependencies: + buffer "^6.0.3" + catering "^2.0.0" + is-buffer "^2.0.5" + level-concat-iterator "^3.0.0" + level-supports "^2.0.1" + queue-microtask "^1.2.3" + abstract-leveldown@~0.12.0, abstract-leveldown@~0.12.1: version "0.12.4" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-0.12.4.tgz#29e18e632e60e4e221d5810247852a63d7b2e410" @@ -6804,34 +6769,22 @@ acorn-globals@^7.0.0: acorn "^8.1.0" acorn-walk "^8.0.2" -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== - -acorn-jsx-walk@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz#a5ed648264e68282d7c2aead80216bfdf232573a" - integrity sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-loose@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.4.0.tgz#26d3e219756d1e180d006f5bcc8d261a28530f55" - integrity sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ== - dependencies: - acorn "^8.11.0" - acorn-walk@^7.1.1: version "7.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0, acorn-walk@^8.3.3: +acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0: version "8.3.3" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== @@ -6848,7 +6801,7 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.1.0, acorn@^8.10.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.12.0, acorn@^8.12.1, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.1.0, acorn@^8.10.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -6865,14 +6818,14 @@ agent-base@6, agent-base@^6.0.2: dependencies: debug "4" -agent-base@^7.0.2: +agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== dependencies: debug "^4.3.4" -agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: +agentkeepalive@^4.2.1: version "4.5.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== @@ -6915,7 +6868,7 @@ ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.1.0, ajv@^8.17.1, ajv@^8.4.0: +ajv@^8.0.0, ajv@^8.1.0, ajv@^8.4.0: version "8.17.1" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== @@ -7319,11 +7272,6 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -assertion-error@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" - integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== - ast-module-types@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/ast-module-types/-/ast-module-types-2.7.1.tgz#3f7989ef8dfa1fdb82dfe0ab02bdfc7c77a57dd3" @@ -7347,18 +7295,11 @@ async-lock@^1.4.1: async-retry@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" - integrity "sha1-Dn82wE2EeOeli9vtgM7fl3eF8oA= sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== dependencies: retry "0.13.1" -async@^2.6.3: - version "2.6.4" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" - integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== - dependencies: - lodash "^4.17.14" - -async@^3.2.1, async@^3.2.3, async@^3.2.4: +async@^3.2.3, async@^3.2.4: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== @@ -7397,25 +7338,31 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +await-to-js@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/await-to-js/-/await-to-js-3.0.0.tgz#70929994185616f4675a91af6167eb61cc92868f" + integrity sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g== + aws-cloudfront-sign@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/aws-cloudfront-sign/-/aws-cloudfront-sign-3.0.2.tgz#da5273b0301bcd70312c8c76293d5fec6d414f0a" integrity sha512-Z/yOGZ3Hd1rhYbY13mtRiLCbCDC1Xf/v+dQUyUwMLnyunD/nfDZd/2LMZ9MKxxOhVb2RzEmEwY0F9f+riPaSWQ== -aws-sdk@2.1030.0: - version "2.1030.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1030.0.tgz#24a856af3d2b8b37c14a8f59974993661c66fd82" - integrity sha512-to0STOb8DsSGuSsUb/WCbg/UFnMGfIYavnJH5ZlRCHzvCFjTyR+vfE8ku+qIZvfFM4+5MNTQC/Oxfun2X/TuyA== +aws-sdk@2.1692.0: + version "2.1692.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1692.0.tgz#9dac5f7bfcc5ab45825cc8591b12753aa7d2902c" + integrity sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw== dependencies: buffer "4.9.2" events "1.1.1" ieee754 "1.1.13" - jmespath "0.15.0" + jmespath "0.16.0" querystring "0.2.0" sax "1.2.1" url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" + util "^0.12.4" + uuid "8.0.0" + xml2js "0.6.2" aws-sign2@~0.7.0: version "0.7.0" @@ -7423,16 +7370,16 @@ aws-sign2@~0.7.0: integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + version "1.13.2" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.2.tgz#0aa167216965ac9474ccfa83892cfb6b3e1e52ef" + integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== -axios@1.1.3, axios@1.6.3, axios@^0.21.1, axios@^1.0.0, axios@^1.1.3, axios@^1.4.0, axios@^1.5.0, axios@^1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.3.tgz#7f50f23b3aa246eff43c54834272346c396613f4" - integrity sha512-fWyNdeawGam70jXSVlKl+SUNVcL6j6W79CuSIPfi6HnDUmSCH6gyUys/HrqHeA/wU0Az41rRgean494d0Jb+ww== +axios@1.1.3, axios@1.7.7, axios@^0.21.1, axios@^1.0.0, axios@^1.1.3, axios@^1.4.0, axios@^1.6.2, axios@^1.6.8: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: - follow-redirects "^1.15.0" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -7649,25 +7596,20 @@ before-after-hook@^2.2.0: integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== big-integer@^1.6.43: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -bignumber.js@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8" - integrity sha512-uw4ra6Cv483Op/ebM0GBKKfxZlSmn6NgFRby5L3yGTlunLj53KQgndDlqy2WVFOwgvurocApYkSud0aO+mvrpQ== + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== bignumber.js@^9.0.0: version "9.1.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== +bignumber.js@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -7678,13 +7620,6 @@ binascii@0.0.2: resolved "https://registry.yarnpkg.com/binascii/-/binascii-0.0.2.tgz#a7f8a8801dbccf8b1756b743daa0fee9e2d9e0ee" integrity sha512-rA2CrUl1+6yKrn+XgLs8Hdy18OER1UW146nM+ixzhQXDY+Bd3ySkyIJGwF2a4I45JwbvF1mDL/nWkqBwpOcdBA== -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - bl@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" @@ -7702,10 +7637,10 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -bl@^6.0.3: - version "6.0.13" - resolved "https://registry.yarnpkg.com/bl/-/bl-6.0.13.tgz#dc5f288d3f849771bb6112b29477abee4c0a9d96" - integrity sha512-tMncAcpsyjZgAVbVFupVIaB2xud13xxT59fdHkuszY2jdZkqIWfpQdmII1fOe3kOGAz0mNLTIHEm+KxpYsQKKg== +bl@^6.0.11: + version "6.0.16" + resolved "https://registry.yarnpkg.com/bl/-/bl-6.0.16.tgz#29b190f1a754e2d168de3dc8c74ed8d12bf78e6e" + integrity sha512-V/kz+z2Mx5/6qDfRCilmrukUXcXuCoXKg3/3hDvzKKoSUx8CJKudfIoT29XZc3UE9xBvxs5qictiHdprwtteEg== dependencies: "@types/readable-stream" "^4.0.0" buffer "^6.0.3" @@ -7724,10 +7659,10 @@ bluebird@^3.5.1, bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bmp-js@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233" - integrity sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw== +bmp-ts@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/bmp-ts/-/bmp-ts-1.0.9.tgz#0fd124ba812be9b786b29e5b186ee76d74ff5538" + integrity sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" @@ -7749,15 +7684,10 @@ boolean@^3.0.1: resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== -bottleneck@^2.19.5: - version "2.19.5" - resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" - integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== - bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" - integrity "sha1-XKPDV1enqldxUAxwpzqfke9CCo8= sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== boxen@^5.0.0: version "5.1.2" @@ -7903,10 +7833,10 @@ bson@^6.7.0: resolved "https://registry.yarnpkg.com/bson/-/bson-6.8.0.tgz#5063c41ba2437c2b8ff851b50d9e36cb7aaa7525" integrity sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ== -btoa@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" - integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== +bson@^6.9.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/bson/-/bson-6.10.0.tgz#559c767cc8b605c3ab14e5896214c8f2abdd6a12" + integrity sha512-ROchNosXMJD2cbQGm84KoP7vOGPO6/bOAW0veMMbzhXLqoZptcaYRVLitwvuhwhjjpU1qP4YZRWLhgETdgqUQw== buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -7936,11 +7866,6 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== -buffer-equal@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" - integrity sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA== - buffer-es6@^4.9.2, buffer-es6@^4.9.3: version "4.9.3" resolved "https://registry.yarnpkg.com/buffer-es6/-/buffer-es6-4.9.3.tgz#f26347b82df76fd37e18bcb5288c4970cfd5c404" @@ -7988,7 +7913,7 @@ buffer@6.0.3, buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -buffer@^5.1.0, buffer@^5.2.0, buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: +buffer@^5.1.0, buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -8060,30 +7985,6 @@ cac@^6.7.14: resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== -cacache@^15.2.0: - version "15.3.0" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" - integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== - dependencies: - "@npmcli/fs" "^1.0.0" - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.0.2" - unique-filename "^1.1.1" - cacache@^16.1.0: version "16.1.3" resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" @@ -8135,11 +8036,6 @@ cache-content-type@^1.0.0: mime-types "^2.1.18" ylru "^1.2.0" -cacheable-lookup@^5.0.3: - version "5.0.4" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" - integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== - cacheable-request@^2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d" @@ -8166,19 +8062,6 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -cacheable-request@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" - integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^4.0.0" - lowercase-keys "^2.0.0" - normalize-url "^6.0.1" - responselike "^2.0.0" - call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -8195,6 +8078,11 @@ call-me-maybe@^1.0.1: resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" integrity sha512-wCyFsDQkKPwwF8BDwOiWNx/9K45L/hvggQiDbve+viMNMQnWhrlYIuBk09offfwCRtCO9P6XwUttufzU11WCVw== +callsite@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -8214,7 +8102,7 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.2.0: +camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -8239,6 +8127,11 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +catering@^2.0.0, catering@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" + integrity sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w== + chai@^4.3.7: version "4.5.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" @@ -8252,17 +8145,6 @@ chai@^4.3.7: pathval "^1.1.1" type-detect "^4.1.0" -chai@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.1.tgz#f035d9792a22b481ead1c65908d14bb62ec1c82c" - integrity sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA== - dependencies: - assertion-error "^2.0.1" - check-error "^2.1.1" - deep-eql "^5.0.1" - loupe "^3.1.0" - pathval "^2.0.0" - chalk@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" @@ -8301,6 +8183,11 @@ chance@1.1.8: resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.8.tgz#5d6c2b78c9170bf6eb9df7acdda04363085be909" integrity sha512-v7fi5Hj2VbR6dJEGRWLmJBA83LJMS47pkAbmROFxHWd9qmE1esHRZW8Clf1Fhzr3rjxnNZVCjOEv/ivFxeIMtg== +chance@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/chance/-/chance-1.1.12.tgz#6a263cf241674af50a1b903357f9d328a6f252fb" + integrity sha512-vVBIGQVnwtUG+SYe0ge+3MvF78cvSpuCOEUJr7sVEk2vSBuMW6OXNJjSzdtzrlxNUEaoqH2GBd5Y/+18BEB01Q== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -8311,7 +8198,7 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -cheap-watch@^1.0.2, cheap-watch@^1.0.4: +cheap-watch@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/cheap-watch/-/cheap-watch-1.0.4.tgz#0bcb4a3a8fbd9d5327936493f6b56baa668d8fef" integrity sha512-QR/9FrtRL5fjfUJBhAKCdi0lSRQ3rVRRum3GF9wDKp2TJbEIMGhUEr2yU8lORzm9Isdjx7/k9S0DFDx+z5VGtw== @@ -8323,11 +8210,6 @@ check-error@^1.0.3: dependencies: get-func-name "^2.0.2" -check-error@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" - integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== - chokidar@3.5.3, chokidar@^3.5.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -8644,11 +8526,6 @@ commander@^11.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== -commander@^12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - commander@^2.16.0, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3, commander@^2.5.0, commander@^2.7.1, commander@^2.8.1: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -8659,7 +8536,7 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^7.1.0, commander@^7.2.0: +commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== @@ -8723,7 +8600,7 @@ compress-commons@^6.0.2: normalize-path "^3.0.0" readable-stream "^4.0.0" -compressible@^2.0.0, compressible@^2.0.12: +compressible@^2.0.0: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== @@ -8784,7 +8661,7 @@ config-chain@^1.1.13: ini "^1.3.4" proto-list "~1.2.1" -configent@^2.1.4, configent@^2.2.0: +configent@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/configent/-/configent-2.2.0.tgz#2de230fc43f22c47cfd99016aa6962d6f9546994" integrity sha512-yIN6zfOWk2nycNJ2JFNiWEai0oiqAhISIht8+pbEBP8bdcpwoQ74AhCZPbUv9aRVJwo7wh1MbCBDUV44UJa7Kw== @@ -8813,14 +8690,6 @@ console-control-strings@^1.0.0, console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -console-stamp@^3.0.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/console-stamp/-/console-stamp-3.1.2.tgz#35dac393e16069a4d9d37b71ca6d5d13d7f3f8fd" - integrity sha512-ab66x3NxOTxPuq71dI6gXEiw2X6ql4Le5gZz0bm7FW3FSCB00eztra/oQUuCoCGlsyKOxtULnHwphzMrRtzMBg== - dependencies: - chalk "^4.1.2" - dateformat "^4.6.3" - consolidate@^0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16" @@ -8923,11 +8792,16 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cookie@^0.4.1, cookie@~0.4.1: +cookie@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" @@ -8986,6 +8860,17 @@ cors@~2.8.5: object-assign "^4" vary "^1" +cosmiconfig@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cosmiconfig@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" @@ -9100,6 +8985,15 @@ cron-validate@1.4.5: dependencies: yup "0.32.9" +cross-spawn@7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -9425,54 +9319,92 @@ dateformat@^4.6.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== -dayjs@^1.10.8, dayjs@^1.8.15: +dayjs@^1.10.8: version "1.11.13" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== -dc-polyfill@^0.1.2: - version "0.1.3" - resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.3.tgz#fe9eefc86813439dd46d6f9ad9582ec079c39720" - integrity sha512-Wyk5n/5KUj3GfVKV2jtDbtChC/Ff9fjKsBcg4ZtYW1yQe3DXNHcGURvmoxhqQdfOQ9TwyMjnfyv1lyYcOkFkFA== +dc-polyfill@^0.1.4: + version "0.1.6" + resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.6.tgz#c2940fa68ffb24a7bf127cc6cfdd15b39f0e7f02" + integrity sha512-UV33cugmCC49a5uWAApM+6Ev9ZdvIUMTrtCO9fj96TPGOQiea54oeO3tiEVdVeo3J9N2UdJEmbS4zOkkEA35uQ== -dd-trace@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.2.0.tgz#6ca2d76ece95f08d98468d7782c22f24192afa53" - integrity sha512-Z5ql3ZKzVW3DPstHPkTPcIPvKljHNtzTYY/WuZRlgT4XK7rMaN0j5nA8LlUh7m+tOPWs05IiKngbYVZjsqhRgA== +dd-trace@5.23.0: + version "5.23.0" + resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.23.0.tgz#a0c11863406de440a6675648caf06e1d07d67ba8" + integrity sha512-nLvwSGpTMIk6S3sMSge6yFqqgqI573VgZc8MF31vl6K0ouJoE7OkVx9cmSVjS4CbSi525tcKq9z7tApsNLpVLQ== dependencies: - "@datadog/native-appsec" "7.0.0" - "@datadog/native-iast-rewriter" "2.2.2" - "@datadog/native-iast-taint-tracking" "1.6.4" + "@datadog/native-appsec" "8.1.1" + "@datadog/native-iast-rewriter" "2.4.1" + "@datadog/native-iast-taint-tracking" "3.1.0" "@datadog/native-metrics" "^2.0.0" - "@datadog/pprof" "5.0.0" + "@datadog/pprof" "5.3.0" "@datadog/sketches-js" "^2.1.0" - "@opentelemetry/api" "^1.0.0" + "@opentelemetry/api" ">=1.0.0 <1.9.0" "@opentelemetry/core" "^1.14.0" crypto-randomuuid "^1.0.0" - dc-polyfill "^0.1.2" + dc-polyfill "^0.1.4" ignore "^5.2.4" - import-in-the-middle "^1.7.3" + import-in-the-middle "1.11.2" + int64-buffer "^0.1.9" + istanbul-lib-coverage "3.2.0" + jest-docblock "^29.7.0" + jsonpath-plus "^9.0.0" + koalas "^1.0.2" + limiter "1.1.5" + lodash.sortby "^4.7.0" + lru-cache "^7.14.0" + module-details-from-path "^1.0.3" + msgpack-lite "^0.1.26" + opentracing ">=0.12.1" + path-to-regexp "^0.1.10" + pprof-format "^2.1.0" + protobufjs "^7.2.5" + retry "^0.13.1" + rfdc "^1.3.1" + semver "^7.5.4" + shell-quote "^1.8.1" + tlhunter-sorted-set "^0.1.0" + +dd-trace@5.26.0: + version "5.26.0" + resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.26.0.tgz#cc55061f66742bf01d0d7dc9f75c0e4937c82f40" + integrity sha512-AQ4usxrbAG41f7CKUUe7fayZgfrh24D0L0vNzcU2mMJOmqQ3bXeDz9uSHkF3aFY8Epcsegrep3ifjRC0/zOxTw== + dependencies: + "@datadog/libdatadog" "^0.2.2" + "@datadog/native-appsec" "8.3.0" + "@datadog/native-iast-rewriter" "2.5.0" + "@datadog/native-iast-taint-tracking" "3.2.0" + "@datadog/native-metrics" "^3.0.1" + "@datadog/pprof" "5.4.1" + "@datadog/sketches-js" "^2.1.0" + "@isaacs/ttlcache" "^1.4.1" + "@opentelemetry/api" ">=1.0.0 <1.9.0" + "@opentelemetry/core" "^1.14.0" + crypto-randomuuid "^1.0.0" + dc-polyfill "^0.1.4" + ignore "^5.2.4" + import-in-the-middle "1.11.2" int64-buffer "^0.1.9" - ipaddr.js "^2.1.0" istanbul-lib-coverage "3.2.0" jest-docblock "^29.7.0" koalas "^1.0.2" limiter "1.1.5" lodash.sortby "^4.7.0" lru-cache "^7.14.0" - methods "^1.1.2" module-details-from-path "^1.0.3" msgpack-lite "^0.1.26" - node-abort-controller "^3.1.1" opentracing ">=0.12.1" - path-to-regexp "^0.1.2" - pprof-format "^2.0.7" + path-to-regexp "^0.1.10" + pprof-format "^2.1.0" protobufjs "^7.2.5" retry "^0.13.1" + rfdc "^1.3.1" semver "^7.5.4" + shell-quote "^1.8.1" tlhunter-sorted-set "^0.1.0" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: version "4.3.6" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== @@ -9486,7 +9418,7 @@ debug@4.3.4: dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -9605,11 +9537,6 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" -deep-eql@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" - integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== - deep-equal@^2.0.5: version "2.2.3" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" @@ -9649,11 +9576,6 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge-ts@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a" - integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw== - deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" @@ -9671,11 +9593,6 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== -defer-to-connect@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - deferred-leveldown@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz#2cef1f111e1c57870d8bbb8af2650e587cd2f5b4" @@ -9719,11 +9636,6 @@ defined@^1.0.0: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf" integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q== -defined@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-0.0.0.tgz#f35eea7d705e933baf13b2f03b3f83d921403b3e" - integrity sha512-zpqiCT8bODLu3QSmLLic8xJnYWBFjOSu/fBCm189oAiTtPq/PSanNACKZDS7kgSyCJY7P+IcODzlIogBK/9RBg== - delay@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" @@ -9749,6 +9661,35 @@ denque@^2.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== +depcheck@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/depcheck/-/depcheck-1.4.7.tgz#57976e2fa43625f477efc0f19ad868ef94f8a26c" + integrity sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA== + dependencies: + "@babel/parser" "^7.23.0" + "@babel/traverse" "^7.23.2" + "@vue/compiler-sfc" "^3.3.4" + callsite "^1.0.0" + camelcase "^6.3.0" + cosmiconfig "^7.1.0" + debug "^4.3.4" + deps-regex "^0.2.0" + findup-sync "^5.0.0" + ignore "^5.2.4" + is-core-module "^2.12.0" + js-yaml "^3.14.1" + json5 "^2.2.3" + lodash "^4.17.21" + minimatch "^7.4.6" + multimatch "^5.0.0" + please-upgrade-node "^3.2.0" + readdirp "^3.6.0" + require-package-name "^2.0.1" + resolve "^1.22.3" + resolve-from "^5.0.0" + semver "^7.5.4" + yargs "^16.2.0" + depd@2.0.0, depd@^2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -9759,34 +9700,6 @@ depd@^1.1.0, depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -dependency-cruiser@^16.3.7: - version "16.4.0" - resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-16.4.0.tgz#a1b7d452acddf05045ae4f7942a2e9337aedad35" - integrity sha512-la/NnD23m6esCox8KMiZ/pcmtec6G/r7LgnJvkBepcErdzlGaxWnyaxtpoYB3fgODrU/7E2u81/nX5FNu5zfyw== - dependencies: - acorn "^8.12.1" - acorn-jsx "^5.3.2" - acorn-jsx-walk "^2.0.0" - acorn-loose "^8.4.0" - acorn-walk "^8.3.3" - ajv "^8.17.1" - commander "^12.1.0" - enhanced-resolve "^5.17.1" - ignore "^5.3.2" - interpret "^3.1.1" - is-installed-globally "^1.0.0" - json5 "^2.2.3" - memoize "^10.0.0" - picocolors "^1.0.1" - picomatch "^4.0.2" - prompts "^2.4.2" - rechoir "^0.8.0" - safe-regex "^2.1.1" - semver "^7.6.3" - teamcity-service-messages "^0.1.14" - tsconfig-paths-webpack-plugin "^4.1.0" - watskeburt "^4.1.0" - dependency-tree@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-9.0.0.tgz#9288dd6daf35f6510c1ea30d9894b75369aa50a2" @@ -9803,6 +9716,11 @@ deprecation@^2.0.0: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +deps-regex@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/deps-regex/-/deps-regex-0.2.0.tgz#3ee7ddae5fd784f3accf29d5a711aa6e10044137" + integrity sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q== + dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -9821,6 +9739,11 @@ destroy@^1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== + detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -10033,11 +9956,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -discontinuous-range@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" - integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== - docker-compose@0.23.17: version "0.23.17" resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.17.tgz#8816bef82562d9417dc8c790aa4871350f93a2ba" @@ -10052,13 +9970,6 @@ docker-compose@0.24.0: dependencies: yaml "^1.10.2" -docker-compose@^0.23.6: - version "0.23.19" - resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.19.tgz#9947726e2fe67bdfa9e8efe1ff15aa0de2e10eb8" - integrity sha512-v5vNLIdUqwj4my80wxFDkNH+4S85zsRuH29SO7dCWVWPCMt/ohZBsGN6g6KXWifT0pzQ7uOxqEKCYCDPJ8Vz4g== - dependencies: - yaml "^1.10.2" - docker-compose@^0.24.6: version "0.24.6" resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.24.6.tgz#d1f490a641bdb7ccc07c4d446b264f026f9a1f15" @@ -10267,23 +10178,15 @@ duplexify@^4.0.0, duplexify@^4.1.2: readable-stream "^3.1.1" stream-shift "^1.0.0" -dynalite@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/dynalite/-/dynalite-3.2.2.tgz#34b4f4dd69638f17c0f7551a867959972c892441" - integrity sha512-sx9ZjTgMs/D4gHnba4rnBkw29648dHwHmywJet132KAbiq1ZyWx9W1fMd/eP9cPwTKDXyCBuTYOChE0qMDjaXQ== +duplexify@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== dependencies: - async "^2.6.3" - big.js "^5.2.2" - buffer-crc32 "^0.2.13" - lazy "^1.0.11" - levelup "^4.4.0" - lock "^1.1.0" - memdown "^5.1.0" - minimist "^1.2.5" - once "^1.4.0" - subleveldown "^5.0.1" - optionalDependencies: - leveldown "^5.6.0" + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" eastasianwidth@^0.2.0: version "0.2.0" @@ -10356,6 +10259,11 @@ elliptic@^6.5.3, elliptic@^6.5.5: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +email-validator@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" + integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ== + emittery@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" @@ -10386,7 +10294,7 @@ encodeurl@^1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -encoding-down@^6.2.0, encoding-down@^6.3.0: +encoding-down@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/encoding-down/-/encoding-down-6.3.0.tgz#b1c4eb0e1728c146ecaef8e32963c549e76d082b" integrity sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw== @@ -10396,7 +10304,7 @@ encoding-down@^6.2.0, encoding-down@^6.3.0: level-codec "^9.0.0" level-errors "^2.0.0" -encoding@^0.1.12, encoding@^0.1.13: +encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== @@ -10433,23 +10341,23 @@ engine.io-parser@~5.2.1: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== -engine.io@~6.5.2: - version "6.5.5" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.5.tgz#430b80d8840caab91a50e9e23cb551455195fc93" - integrity sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA== +engine.io@~6.6.0: + version "6.6.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.2.tgz#32bd845b4db708f8c774a4edef4e5c8a98b3da72" + integrity sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" "@types/node" ">=10.0.0" accepts "~1.3.4" base64id "2.0.0" - cookie "~0.4.1" + cookie "~0.7.2" cors "~2.8.5" debug "~4.3.1" engine.io-parser "~5.2.1" ws "~8.17.1" -enhanced-resolve@^5.17.1, enhanced-resolve@^5.7.0, enhanced-resolve@^5.8.3: +enhanced-resolve@^5.8.3: version "5.17.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== @@ -10464,17 +10372,12 @@ enquirer@~2.3.6: dependencies: ansi-colors "^4.1.1" -ent@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" - integrity "sha1-6WQhkyWiHQX0RGai9obtbOX13R0= sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==" - entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: +entities@^4.2.0, entities@^4.3.0, entities@^4.4.0, entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -10489,11 +10392,6 @@ envinfo@7.8.1: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== -err-code@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" - integrity sha512-CJAN+O0/yA1CKfRn9SXOGctSpEM7DCon/r/5r2eXFMY2zCCJBasFhcM5I+1kh3Ap11FsQCX+vGHceNPvpWKhoA== - err-code@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" @@ -10513,7 +10411,7 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0: version "1.23.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== @@ -10565,19 +10463,57 @@ es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23 unbox-primitive "^1.0.2" which-typed-array "^1.1.15" -es-aggregate-error@^1.0.9: - version "1.0.13" - resolved "https://registry.yarnpkg.com/es-aggregate-error/-/es-aggregate-error-1.0.13.tgz#7f28b77c9d8d09bbcd3a466e4be9fe02fa985201" - integrity sha512-KkzhUUuD2CUMqEc8JEqsXEMDHzDPE8RCjZeUBitsnB1eNcAJWQPiciKsMXe3Yytj4Flw1XLl46Qcf9OxvZha7A== +es-abstract@^1.23.3: + version "1.23.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.5.tgz#f4599a4946d57ed467515ed10e4f157289cd52fb" + integrity sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ== dependencies: - define-data-property "^1.1.4" - define-properties "^1.2.1" - es-abstract "^1.23.2" + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" es-errors "^1.3.0" - function-bind "^1.1.2" - globalthis "^1.0.3" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.4" + gopd "^1.0.1" has-property-descriptors "^1.0.2" - set-function-name "^2.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.3" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.3" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" es-define-property@^1.0.0: version "1.0.0" @@ -10652,11 +10588,6 @@ es6-error@^4.0.1, es6-error@^4.1.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -es6-promise@^4.2.4: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - esbuild-node-externals@^1.14.0: version "1.14.0" resolved "https://registry.yarnpkg.com/esbuild-node-externals/-/esbuild-node-externals-1.14.0.tgz#fc2950c67a068dc2b538fd1381ad7d8e20a6f54d" @@ -10693,35 +10624,6 @@ esbuild@^0.18.10, esbuild@^0.18.17: "@esbuild/win32-ia32" "0.18.20" "@esbuild/win32-x64" "0.18.20" -esbuild@^0.21.3: - version "0.21.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" - integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== - optionalDependencies: - "@esbuild/aix-ppc64" "0.21.5" - "@esbuild/android-arm" "0.21.5" - "@esbuild/android-arm64" "0.21.5" - "@esbuild/android-x64" "0.21.5" - "@esbuild/darwin-arm64" "0.21.5" - "@esbuild/darwin-x64" "0.21.5" - "@esbuild/freebsd-arm64" "0.21.5" - "@esbuild/freebsd-x64" "0.21.5" - "@esbuild/linux-arm" "0.21.5" - "@esbuild/linux-arm64" "0.21.5" - "@esbuild/linux-ia32" "0.21.5" - "@esbuild/linux-loong64" "0.21.5" - "@esbuild/linux-mips64el" "0.21.5" - "@esbuild/linux-ppc64" "0.21.5" - "@esbuild/linux-riscv64" "0.21.5" - "@esbuild/linux-s390x" "0.21.5" - "@esbuild/linux-x64" "0.21.5" - "@esbuild/netbsd-x64" "0.21.5" - "@esbuild/openbsd-x64" "0.21.5" - "@esbuild/sunos-x64" "0.21.5" - "@esbuild/win32-arm64" "0.21.5" - "@esbuild/win32-ia32" "0.21.5" - "@esbuild/win32-x64" "0.21.5" - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -10795,18 +10697,6 @@ eslint-module-utils@^2.8.0: dependencies: debug "^3.2.7" -eslint-plugin-functional@^6.6.3: - version "6.6.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-functional/-/eslint-plugin-functional-6.6.3.tgz#85b895afb91835c5ffa2eb97f473fd4182aa5228" - integrity sha512-sVbbvNvwX3HVkXAykKyoNLv57r4DPF7f1sy+/8j4YtzLYVQPGljMUWv3T6Kd4lwnnjmcKuj0EkIbS+knL6P5jw== - dependencies: - "@typescript-eslint/utils" "^7.3.1" - deepmerge-ts "^5.1.0" - escape-string-regexp "^4.0.0" - is-immutable-type "^4.0.0" - semver "^7.6.0" - ts-api-utils "^1.3.0" - eslint-plugin-import@^2.26.0, eslint-plugin-import@^2.29.0: version "2.29.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" @@ -10876,14 +10766,6 @@ eslint-scope@^7.0.0, eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-scope@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.2.tgz#5cbb33d4384c9136083a71190d548158fe128f94" - integrity sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" @@ -10894,11 +10776,6 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint-visitor-keys@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" - integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== - eslint@^8.52.0, eslint@^8.56.0: version "8.57.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" @@ -10943,60 +10820,11 @@ eslint@^8.52.0, eslint@^8.56.0: strip-ansi "^6.0.1" text-table "^0.2.0" -eslint@^9.7.0: - version "9.9.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.9.1.tgz#147ac9305d56696fb84cf5bdecafd6517ddc77ec" - integrity sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.11.0" - "@eslint/config-array" "^0.18.0" - "@eslint/eslintrc" "^3.1.0" - "@eslint/js" "9.9.1" - "@humanwhocodes/module-importer" "^1.0.1" - "@humanwhocodes/retry" "^0.3.0" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - escape-string-regexp "^4.0.0" - eslint-scope "^8.0.2" - eslint-visitor-keys "^4.0.0" - espree "^10.1.0" - esquery "^1.5.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^8.0.0" - find-up "^5.0.0" - glob-parent "^6.0.2" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - esm@^3.2.25: version "3.2.25" resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^10.0.1, espree@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-10.1.0.tgz#8788dae611574c0f070691f522e4116c5a11fc56" - integrity sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA== - dependencies: - acorn "^8.12.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^4.0.0" - espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -11026,7 +10854,7 @@ esprima@~3.1.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" integrity sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg== -esquery@^1.4.2, esquery@^1.5.0: +esquery@^1.4.2: version "1.6.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== @@ -11163,21 +10991,6 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -execa@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" - integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^8.0.1" - human-signals "^5.0.0" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^4.1.0" - strip-final-newline "^3.0.0" - exif-parser@^0.1.12: version "0.1.12" resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" @@ -11193,7 +11006,7 @@ expand-template@^2.0.3: resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -expand-tilde@^2.0.2: +expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" integrity sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw== @@ -11330,13 +11143,18 @@ fast-url-parser@^1.1.3: dependencies: punycode "^1.3.2" -fast-xml-parser@4.2.5, fast-xml-parser@4.4.1, fast-xml-parser@^4.1.3, fast-xml-parser@^4.2.2, fast-xml-parser@^4.2.5: +fast-xml-parser@4.4.1, fast-xml-parser@^4.2.5, fast-xml-parser@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== dependencies: strnum "^1.0.5" +fastest-levenshtein@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + fastq@^1.6.0: version "1.13.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" @@ -11377,13 +11195,6 @@ fengari@^0.1.4: sprintf-js "^1.1.1" tmp "^0.0.33" -fetch-cookie@0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.10.1.tgz#5ea88f3d36950543c87997c27ae2aeafb4b5c4d4" - integrity sha512-beB+VEd4cNeVG1PY+ee74+PkuCQnik78pgLi5Ah/7qdUfov8IctU0vLUbBT8/10Ma5GMBeI4wtxhGrEfKNYs2g== - dependencies: - tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0" - fetch-cookie@0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.11.0.tgz#e046d2abadd0ded5804ce7e2cae06d4331c15407" @@ -11391,6 +11202,14 @@ fetch-cookie@0.11.0: dependencies: tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0" +fetch-cookie@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-2.2.0.tgz#01086b6b5b1c3e08f15ffd8647b02ca100377365" + integrity sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ== + dependencies: + set-cookie-parser "^2.4.8" + tough-cookie "^4.0.0" + fflate@^0.4.8: version "0.4.8" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" @@ -11410,13 +11229,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-entry-cache@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" - integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== - dependencies: - flat-cache "^4.0.0" - file-type@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-11.1.0.tgz#93780f3fed98b599755d846b99a1617a2ad063b8" @@ -11427,7 +11239,7 @@ file-type@^12.1.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.4.2.tgz#a344ea5664a1d01447ee7fb1b635f72feb6169d9" integrity sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg== -file-type@^16.5.4: +file-type@^16.0.0: version "16.5.4" resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd" integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw== @@ -11456,11 +11268,6 @@ file-type@^6.1.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - filelist@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" @@ -11548,6 +11355,16 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +findup-sync@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-5.0.0.tgz#54380ad965a7edca00cc8f63113559aadc541bd2" + integrity sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.3" + micromatch "^4.0.4" + resolve-dir "^1.0.1" + flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -11556,20 +11373,12 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" -flat-cache@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" - integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.4" - flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flatted@^3.1.0, flatted@^3.2.9: +flatted@^3.1.0: version "3.3.1" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== @@ -11584,10 +11393,10 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== -follow-redirects@^1.15.0: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== for-each@^0.3.3: version "0.3.3" @@ -11678,6 +11487,15 @@ formidable@^2.1.2: once "^1.4.0" qs "^6.11.0" +formidable@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.2.tgz#207c33fecdecb22044c82ba59d0c63a12fb81d77" + integrity sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg== + dependencies: + dezalgo "^1.0.4" + hexoid "^2.0.0" + once "^1.4.0" + fresh@^0.5.2, fresh@~0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" @@ -11705,7 +11523,7 @@ fs-extra@^11.1.0, fs-extra@^11.1.1: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: +fs-extra@^9.0.0, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -11734,7 +11552,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -11754,7 +11572,7 @@ function.prototype.name@^1.1.6: es-abstract "^1.22.1" functions-have-names "^1.2.3" -functional-red-black-tree@^1.0.1, functional-red-black-tree@~1.0.1: +functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= @@ -11821,6 +11639,17 @@ gaxios@^6.0.0, gaxios@^6.1.1: node-fetch "^2.6.9" uuid "^10.0.0" +gaxios@^6.0.2: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + uuid "^9.0.1" + gcp-metadata@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.3.0.tgz#6f45eb473d0cb47d15001476b48b663744d25408" @@ -11975,11 +11804,6 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -get-stream@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" - integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== - get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -12111,7 +11935,7 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^10.0.0, glob@^10.2.2, glob@^10.3.7: +glob@^10.0.0, glob@^10.2.2: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -12179,13 +12003,6 @@ global-agent@3.0.0: semver "^7.3.2" serialize-error "^7.0.1" -global-directory@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/global-directory/-/global-directory-4.0.1.tgz#4d7ac7cfd2cb73f304c53b8810891748df5e361e" - integrity sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q== - dependencies: - ini "4.1.1" - global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" @@ -12193,6 +12010,26 @@ global-dirs@^3.0.0: dependencies: ini "2.0.0" +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg== + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + global@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" @@ -12213,11 +12050,6 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" -globals@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" - integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== - globalthis@^1.0.1, globalthis@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" @@ -12225,6 +12057,14 @@ globalthis@^1.0.1, globalthis@^1.0.3: dependencies: define-properties "^1.1.3" +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + globalyzer@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" @@ -12281,6 +12121,18 @@ google-auth-library@^9.3.0: gtoken "^7.0.0" jws "^4.0.0" +google-auth-library@^9.6.3: + version "9.15.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.0.tgz#1b009c08557929c881d72f953f17e839e91b009b" + integrity sha512-7ccSEJFDFO7exFbO6NRyC+xH8/mZ1GZGG2xxx9iHxZWcjUjJpjWxIMw3cofAKcueZ6DATiukmmprD7yavQHOyQ== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + google-gax@^4.3.3: version "4.3.7" resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-4.3.7.tgz#f1870902d09c54c5d1735ef1ee7903d4458d6a49" @@ -12321,23 +12173,6 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -got@^11.8.6: - version "11.8.6" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" - integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== - dependencies: - "@sindresorhus/is" "^4.0.0" - "@szmarczak/http-timer" "^4.0.5" - "@types/cacheable-request" "^6.0.1" - "@types/responselike" "^1.0.0" - cacheable-lookup "^5.0.3" - cacheable-request "^7.0.2" - decompress-response "^6.0.0" - http2-wrapper "^1.0.0-beta.5.2" - lowercase-keys "^2.0.0" - p-cancelable "^2.0.0" - responselike "^2.0.0" - got@^8.3.1: version "8.3.2" resolved "https://registry.yarnpkg.com/got/-/got-8.3.2.tgz#1d23f64390e97f776cac52e5b936e5f514d2e937" @@ -12569,6 +12404,11 @@ hexoid@^1.0.0: resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== +hexoid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-2.0.0.tgz#fb36c740ebbf364403fa1ec0c7efd268460ec5b9" + integrity sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -12630,6 +12470,11 @@ html-encoding-sniffer@^3.0.0: dependencies: whatwg-encoding "^2.0.0" +html-entities@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -12678,7 +12523,7 @@ http-cookie-agent@^4.0.2: dependencies: agent-base "^6.0.2" -http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -12728,6 +12573,14 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-proxy-agent@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -12737,14 +12590,6 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -http2-wrapper@^1.0.0-beta.5.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" - integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.0.0" - https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -12753,7 +12598,7 @@ https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" -https-proxy-agent@^7.0.1: +https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2: version "7.0.5" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== @@ -12766,11 +12611,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -human-signals@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" - integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== - humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -12783,11 +12623,6 @@ husky@^8.0.3: resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== -husky@^9.1.4: - version "9.1.5" - resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.5.tgz#2b6edede53ee1adbbd3a3da490628a23f5243b83" - integrity sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag== - ical-generator@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ical-generator/-/ical-generator-4.1.0.tgz#2a336c951864c5583a2aa715d16f2edcdfd2d90b" @@ -12860,7 +12695,7 @@ ignore-walk@^6.0.0: dependencies: minimatch "^7.4.2" -ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1, ignore@^5.3.2: +ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== @@ -12882,16 +12717,6 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -immediate@~3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c" - integrity sha512-RrGCXRm/fRVqMIhqXrGEX9rRADavPiDFSoMb/k64i9XMk8uH4r/Omi5Ctierj6XzNecwDbO4WuFbDD1zmpl3Tg== - -immutable@^4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" - integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== - import-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92" @@ -12914,13 +12739,13 @@ import-from@^3.0.0: dependencies: resolve-from "^5.0.0" -import-in-the-middle@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.7.3.tgz#ffa784cdd57a47d2b68d2e7dd33070ff06baee43" - integrity sha512-R2I11NRi0lI3jD2+qjqyVlVEahsejw7LDnYEbGb47QEFjczE3bZYsmWheCTQA+LFs2DzOQxR7Pms7naHW1V4bQ== +import-in-the-middle@1.11.2: + version "1.11.2" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz#dd848e72b63ca6cd7c34df8b8d97fc9baee6174f" + integrity sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA== dependencies: acorn "^8.8.2" - acorn-import-assertions "^1.9.0" + acorn-import-attributes "^1.9.5" cjs-module-lexer "^1.2.2" module-details-from-path "^1.0.3" @@ -12990,11 +12815,6 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" - integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== - ini@^1.3.2, ini@^1.3.4, ini@^1.3.8, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -13072,11 +12892,6 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -interpret@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" - integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== - into-stream@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6" @@ -13138,12 +12953,7 @@ ip@^2.0.0: resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== -ipaddr.js@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" - integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== - -is-arguments@^1.1.1: +is-arguments@^1.0.4, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -13191,6 +13001,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-builtin-module@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" @@ -13222,6 +13037,13 @@ is-class-hotfix@~0.0.6: resolved "https://registry.yarnpkg.com/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz#a527d31fb23279281dde5f385c77b5de70a72435" integrity sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ== +is-core-module@^2.12.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0, is-core-module@^2.8.1: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" @@ -13248,11 +13070,6 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== -is-electron@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.2.tgz#3778902a2044d76de98036f5dc58089ac4d80bb9" - integrity sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg== - is-extendable@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -13302,15 +13119,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-immutable-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-immutable-type/-/is-immutable-type-4.0.0.tgz#d62ad1ff411eef8dfa3a87222960ec3b645db1a1" - integrity sha512-gyFBCXv+NikTs8/PGZhgjbMmFZQ5jvHGZIsVu6+/9Bk4K7imlWBIDN7hTr9fNioGzFg71I4YM3z8f0aKXarTAw== - dependencies: - "@typescript-eslint/type-utils" "^7.2.0" - ts-api-utils "^1.3.0" - ts-declaration-location "^1.0.0" - is-installed-globally@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -13319,14 +13127,6 @@ is-installed-globally@^0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" -is-installed-globally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-1.0.0.tgz#08952c43758c33d815692392f7f8437b9e436d5a" - integrity sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ== - dependencies: - global-directory "^4.0.1" - is-path-inside "^4.0.0" - is-interactive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" @@ -13406,11 +13206,6 @@ is-path-inside@^3.0.2, is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-path-inside@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db" - integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== - is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -13516,11 +13311,6 @@ is-stream@^2.0.0, is-stream@^2.0.1: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== - is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -13551,7 +13341,7 @@ is-type-of@^1.0.0: is-class-hotfix "~0.0.6" isstream "~0.1.2" -is-typed-array@^1.1.13: +is-typed-array@^1.1.13, is-typed-array@^1.1.3: version "1.1.13" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== @@ -13608,6 +13398,11 @@ is-whitespace@^0.3.0: resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" integrity sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg== +is-windows@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -13679,14 +13474,6 @@ isolated-vm@^4.7.2: dependencies: prebuild-install "^7.1.1" -isomorphic-fetch@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" - integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== - dependencies: - node-fetch "^2.6.1" - whatwg-fetch "^3.4.1" - isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -13879,15 +13666,6 @@ jest-docblock@^29.7.0: dependencies: detect-newline "^3.0.0" -jest-dynalite@^3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/jest-dynalite/-/jest-dynalite-3.6.1.tgz#8bae305a3c33d9a8036f563827b173b54a323ca5" - integrity sha512-MERtTt8Pj39vFmbItMC3YuIaqLf1kh/pJIE0DRcjeP/2Fa8Nni9IxwN6XWIMgXNbFKtlOM6ppH+Bsy0rWIdPiw== - dependencies: - "@aws/dynamodb-auto-marshaller" "^0.7.1" - dynalite "^3.2.1" - setimmediate "^1.0.5" - jest-each@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" @@ -14194,20 +13972,43 @@ jest@29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" -jimp@0.22.12: - version "0.22.12" - resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.22.12.tgz#f99d1f3ec0d9d930cb7bd8f5b479859ee3a15694" - integrity sha512-R5jZaYDnfkxKJy1dwLpj/7cvyjxiclxU3F4TrI/J4j2rS0niq6YDUMoPn5hs8GDpO+OZGo7Ky057CRtWesyhfg== +jimp@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/jimp/-/jimp-1.1.4.tgz#943356f27559815690a3c2e29fa67ecfd9a92658" + integrity sha512-DL82Spu4H7B332nhddz5Cq9J0WEa5mc9d6BJQfeLHf2LOAMg79A+74KRKKzogaLgqK8APGfoWLwca7KjjvBgig== dependencies: - "@jimp/custom" "^0.22.12" - "@jimp/plugins" "^0.22.12" - "@jimp/types" "^0.22.12" - regenerator-runtime "^0.13.3" + "@jimp/core" "1.1.4" + "@jimp/diff" "1.1.4" + "@jimp/js-bmp" "1.1.4" + "@jimp/js-gif" "1.1.4" + "@jimp/js-jpeg" "1.1.4" + "@jimp/js-png" "1.1.4" + "@jimp/js-tiff" "1.1.4" + "@jimp/plugin-blit" "1.1.4" + "@jimp/plugin-blur" "1.1.4" + "@jimp/plugin-circle" "1.1.4" + "@jimp/plugin-color" "1.1.4" + "@jimp/plugin-contain" "1.1.4" + "@jimp/plugin-cover" "1.1.4" + "@jimp/plugin-crop" "1.1.4" + "@jimp/plugin-displace" "1.1.4" + "@jimp/plugin-dither" "1.1.4" + "@jimp/plugin-fisheye" "1.1.4" + "@jimp/plugin-flip" "1.1.4" + "@jimp/plugin-hash" "1.1.4" + "@jimp/plugin-mask" "1.1.4" + "@jimp/plugin-print" "1.1.4" + "@jimp/plugin-quantize" "1.1.4" + "@jimp/plugin-resize" "1.1.4" + "@jimp/plugin-rotate" "1.1.4" + "@jimp/plugin-threshold" "1.1.4" + "@jimp/types" "1.1.4" + "@jimp/utils" "1.1.4" -jmespath@0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" - integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w== +jmespath@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" + integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== joi@17.6.0: version "17.6.0" @@ -14220,17 +14021,6 @@ joi@17.6.0: "@sideway/formula" "^3.0.0" "@sideway/pinpoint" "^2.0.0" -joi@^17.13.1: - version "17.13.3" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" - integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== - dependencies: - "@hapi/hoek" "^9.3.0" - "@hapi/topo" "^5.1.0" - "@sideway/address" "^4.1.5" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - joycon@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" @@ -14268,7 +14058,7 @@ js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js-yaml@^3.10.0, js-yaml@^3.13.1: +js-yaml@^3.10.0, js-yaml@^3.13.1, js-yaml@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -14276,11 +14066,6 @@ js-yaml@^3.10.0, js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -jsbi@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-4.3.0.tgz#b54ee074fb6fcbc00619559305c8f7e912b04741" - integrity sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g== - jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -14351,6 +14136,11 @@ jsdom@^21.1.1: ws "^8.13.0" xml-name-validator "^4.0.0" +jsep@^1.3.8: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsep/-/jsep-1.4.0.tgz#19feccbfa51d8a79f72480b4b8e40ce2e17152f0" + integrity sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw== + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -14378,11 +14168,6 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - json-format-highlight@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/json-format-highlight/-/json-format-highlight-1.0.4.tgz#2e44277edabcec79a3d2c84e984c62e2258037b9" @@ -14423,16 +14208,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json-stable-stringify@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454" - integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg== - dependencies: - call-bind "^1.0.5" - isarray "^2.0.5" - jsonify "^0.0.1" - object-keys "^1.1.1" - json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -14464,16 +14239,20 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" - integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== - jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jsonpath-plus@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-9.0.0.tgz#bb8703ee481531142bca8dee9a42fe72b8358a7f" + integrity sha512-bqE77VIDStrOTV/czspZhTn+o27Xx9ZJRGVkdVShEtPoqsIx5yALv3lWVU6y+PqYvWPJNWE7ORCQheQkEe0DDA== + dependencies: + "@jsep-plugin/assignment" "^1.2.1" + "@jsep-plugin/regex" "^1.0.3" + jsep "^1.3.8" + jsonschema@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2" @@ -14550,11 +14329,6 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" -jwt-decode@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" - integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== - keygrip@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" @@ -14576,13 +14350,6 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" -keyv@^4.0.0, keyv@^4.5.4: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - kill-port@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/kill-port/-/kill-port-1.6.1.tgz#560fe79484583bdf3a5e908557dae614447618aa" @@ -14677,7 +14444,7 @@ koa-mount@^4.0.0: debug "^4.0.1" koa-compose "^4.1.0" -koa-passport@4.1.4, koa-passport@^4.1.4: +koa-passport@4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.4.tgz#5f1665c1c2a37ace79af9f970b770885ca30ccfa" integrity sha512-dJBCkl4X+zdYxbI2V2OtoGy0PUenpvp2ZLLWObc8UJhsId0iQpTFT8RVcuA0709AL2txGwRHnSPoT1bYNGa6Kg== @@ -14718,7 +14485,7 @@ koa-send@5.0.1, koa-send@^5.0.0: http-errors "^1.7.3" resolve-path "^1.4.0" -koa-session@5.13.1, koa-session@^5.12.0: +koa-session@5.13.1: version "5.13.1" resolved "https://registry.yarnpkg.com/koa-session/-/koa-session-5.13.1.tgz#a47e39015a4b464e21e3e1e2deeca48eb83916ee" integrity sha512-TfYiun6xiFosyfIJKnEw0aoG5XmLIwM+K3OVWfkz84qY0NP2gbk0F/olRn0/Hrxq0f14s8amHVXeWyKYH3Cx3Q== @@ -14736,7 +14503,7 @@ koa-static@5.0.0, koa-static@^5.0.0: debug "^3.1.0" koa-send "^5.0.0" -koa-useragent@4.1.0, koa-useragent@^4.1.0: +koa-useragent@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/koa-useragent/-/koa-useragent-4.1.0.tgz#d3f128b552c6da3e5e9e9e9c887b2922b16e4468" integrity sha512-x/HUDZ1zAmNNh5hA9hHbPm9p3UVg2prlpHzxCXQCzbibrNS0kmj7MkCResCbAbG7ZT6FVxNSMjR94ZGamdMwxA== @@ -14836,11 +14603,6 @@ latest-version@^5.1.0: dependencies: package-json "^6.3.0" -lazy@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/lazy/-/lazy-1.0.11.tgz#daa068206282542c088288e975c297c1ae77b690" - integrity sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA== - lazystream@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" @@ -14957,6 +14719,13 @@ level-codec@9.0.2, level-codec@^9.0.0: dependencies: buffer "^5.6.0" +level-concat-iterator@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-3.1.0.tgz#5235b1f744bc34847ed65a50548aa88d22e881cf" + integrity sha512-BWRCMHBxbIqPxJ8vHOvKUsaO0v1sLYZtjN3K2iZJsRBYtp+ONsY6Jfi6hy9K3+zolgQRryhIn2NRZjZnWJ9NmQ== + dependencies: + catering "^2.1.0" + level-concat-iterator@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz#1d1009cf108340252cb38c51f9727311193e6263" @@ -15034,13 +14803,6 @@ level-js@^5.0.0: inherits "^2.0.3" ltgt "^2.1.2" -level-option-wrap@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/level-option-wrap/-/level-option-wrap-1.1.0.tgz#ad20e68d9f3c22c8897531cc6aa7af596b1ed129" - integrity sha512-gQouC22iCqHuBLNl4BHxEZUxLvUKALAtT/Q0c6ziOxZQ8c02G/gyxHWNbLbxUzRNfMrRnbt6TZT3gNe8VBqQeg== - dependencies: - defined "~0.0.0" - level-packager@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/level-packager/-/level-packager-5.1.1.tgz#323ec842d6babe7336f70299c14df2e329c18939" @@ -15066,6 +14828,11 @@ level-sublevel@^5.2.0: string-range "~1.2.1" xtend "~2.0.4" +level-supports@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-2.1.0.tgz#9af908d853597ecd592293b2fad124375be79c5f" + integrity sha512-E486g1NCjW5cF78KGPrMDRBYzPuueMZ6VBXHT6gC7A8UYWGiM14fGgp+s/L1oFfDWSPV/+SFkYCmZ0SiESkRKA== + level-supports@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d" @@ -15089,7 +14856,7 @@ level@6.0.1: level-packager "^5.1.0" leveldown "^5.4.0" -leveldown@5.6.0, leveldown@^5.4.0, leveldown@^5.6.0: +leveldown@5.6.0, leveldown@^5.4.0: version "5.6.0" resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98" integrity sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ== @@ -15098,7 +14865,16 @@ leveldown@5.6.0, leveldown@^5.4.0, leveldown@^5.6.0: napi-macros "~2.0.0" node-gyp-build "~4.1.0" -levelup@4.4.0, levelup@^4.3.2, levelup@^4.4.0: +leveldown@6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-6.1.1.tgz#0f0e480fa88fd807abf94c33cb7e40966ea4b5ce" + integrity sha512-88c+E+Eizn4CkQOBHwqlCJaTNEjGpaEIikn1S+cINc5E9HEvJ77bqY4JY/HxT5u0caWqsc3P3DcFIKBI1vHt+A== + dependencies: + abstract-leveldown "^7.2.0" + napi-macros "~2.0.0" + node-gyp-build "^4.3.0" + +levelup@4.4.0, levelup@^4.3.2: version "4.4.0" resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.4.0.tgz#f89da3a228c38deb49c48f88a70fb71f01cafed6" integrity sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ== @@ -15192,20 +14968,6 @@ lines-and-columns@~2.0.3: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b" integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== -load-bmfont@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.4.1.tgz#c0f5f4711a1e2ccff725a7b6078087ccfcddd3e9" - integrity sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA== - dependencies: - buffer-equal "0.0.1" - mime "^1.3.4" - parse-bmfont-ascii "^1.0.3" - parse-bmfont-binary "^1.0.5" - parse-bmfont-xml "^1.1.4" - phin "^2.9.1" - xhr "^2.0.1" - xtend "^4.0.0" - load-json-file@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" @@ -15271,11 +15033,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lock@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/lock/-/lock-1.1.0.tgz#53157499d1653b136ca66451071fca615703fa55" - integrity sha512-NZQIJJL5Rb9lMJ0Yl1JoVr9GSdo4HTPsUEWsSFzB8dE8DSoiLCVavWZPi7Rnlv/o73u6I24S/XYc/NmG4l8EKA== - lodash-es@^4.17.15, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" @@ -15416,7 +15173,7 @@ lodash.xor@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.xor/-/lodash.xor-4.5.0.tgz#4d48ed7e98095b0632582ba714d3ff8ae8fb1db6" integrity sha512-sVN2zimthq7aZ5sPGXnSz32rZPuqcparVW50chJQe+mzTYV+IsxSsl/2gnkWWE2Of7K3myBQBqtLKOUEHJKRsQ== -lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -15436,12 +15193,12 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -logform@^2.3.2, logform@^2.4.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/logform/-/logform-2.5.1.tgz#44c77c34becd71b3a42a3970c77929e52c6ed48b" - integrity sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg== +logform@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1" + integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ== dependencies: - "@colors/colors" "1.5.0" + "@colors/colors" "1.6.0" "@types/triple-beam" "^1.3.2" fecha "^4.2.0" ms "^2.1.1" @@ -15463,11 +15220,6 @@ lookpath@1.1.0: resolved "https://registry.yarnpkg.com/lookpath/-/lookpath-1.1.0.tgz#932d68371a2f0b4a5644f03d6a2b4728edba96d2" integrity sha512-B9NM7XpVfkyWqfOBI/UW0kVhGw7pJztsduch+1wkbYDi90mYK6/InFul3lG0hYko/VEcVMARVBJ5daFRc5aKCw== -lossless-json@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lossless-json/-/lossless-json-4.0.1.tgz#d45229e3abb213a0235812780ca894ea8c5b2c6b" - integrity sha512-l0L+ppmgPDnb+JGxNLndPtJZGNf6+ZmVaQzoxQm3u6TXmhdnsA+YtdVR8DjzZd/em58686CQhOFDPewfJ4l7MA== - loupe@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" @@ -15475,13 +15227,6 @@ loupe@^2.3.6: dependencies: get-func-name "^2.0.1" -loupe@^3.1.0, loupe@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.1.tgz#71d038d59007d890e3247c5db97c1ec5a92edc54" - integrity sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw== - dependencies: - get-func-name "^2.0.1" - lowercase-keys@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" @@ -15599,7 +15344,14 @@ magic-string@^0.26.7: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.30.10, magic-string@^0.30.3, magic-string@^0.30.4: +magic-string@^0.30.11: + version "0.30.13" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.13.tgz#92438e3ff4946cf54f18247c981e5c161c46683c" + integrity sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + +magic-string@^0.30.3, magic-string@^0.30.4: version "0.30.11" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954" integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== @@ -15613,7 +15365,7 @@ make-dir@4.0.0: dependencies: semver "^7.5.3" -make-dir@^1.0.0, make-dir@^1.3.0: +make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== @@ -15683,28 +15435,6 @@ make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.0: socks-proxy-agent "^7.0.0" ssri "^10.0.0" -make-fetch-happen@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" - integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== - dependencies: - agentkeepalive "^4.1.3" - cacache "^15.2.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^6.0.0" - minipass "^3.1.3" - minipass-collect "^1.0.2" - minipass-fetch "^1.3.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.2" - promise-retry "^2.0.1" - socks-proxy-agent "^6.0.0" - ssri "^8.0.0" - makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -15786,25 +15516,6 @@ memdown@1.4.1: ltgt "~2.2.0" safe-buffer "~5.1.1" -memdown@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/memdown/-/memdown-5.1.0.tgz#608e91a9f10f37f5b5fe767667a8674129a833cb" - integrity sha512-B3J+UizMRAlEArDjWHTMmadet+UKwHd3UjMgGBkZcKAxAYVPS9o0Yeiha4qvz7iGiL2Sb3igUft6p7nbFWctpw== - dependencies: - abstract-leveldown "~6.2.1" - functional-red-black-tree "~1.0.1" - immediate "~3.2.3" - inherits "~2.0.1" - ltgt "~2.2.0" - safe-buffer "~5.2.0" - -memoize@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/memoize/-/memoize-10.0.0.tgz#43fa66b2022363c7c50cf5dfab732a808a3d7147" - integrity sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA== - dependencies: - mimic-function "^5.0.0" - memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" @@ -15881,7 +15592,7 @@ mime-kind@^3.0.0: file-type "^12.1.0" mime-types "^2.1.24" -mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.24, mime-types@^2.1.29, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.24, mime-types@^2.1.29, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -15893,31 +15604,21 @@ mime@2.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== +mime@3, mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mime@^1.3.4: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" - integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== - mimic-fn@^2.0.0, mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - -mimic-function@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" - integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== - mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -15969,13 +15670,6 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" -minimatch@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" - integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== - dependencies: - brace-expansion "^2.0.1" - minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" @@ -15983,7 +15677,7 @@ minimatch@^5.0.1, minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimatch@^7.4.2: +minimatch@^7.4.2, minimatch@^7.4.6: version "7.4.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb" integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw== @@ -16025,17 +15719,6 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" -minipass-fetch@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" - integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== - dependencies: - minipass "^3.1.0" - minipass-sized "^1.0.3" - minizlib "^2.0.0" - optionalDependencies: - encoding "^0.1.12" - minipass-fetch@^2.0.3: version "2.1.2" resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" @@ -16073,7 +15756,7 @@ minipass-json-stream@^1.0.1: jsonparse "^1.3.1" minipass "^3.0.0" -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: +minipass-pipeline@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== @@ -16087,7 +15770,7 @@ minipass-sized@^1.0.3: dependencies: minipass "^3.0.0" -minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3.1.6: +minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: version "3.3.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== @@ -16109,7 +15792,7 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2: +minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -16182,22 +15865,17 @@ module-lookup-amd@^7.0.1: requirejs-config-file "^4.0.0" moment-timezone@^0.5.15: - version "0.5.41" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.41.tgz#a7ad3285fd24aaf5f93b8119a9d749c8039c64c5" - integrity sha512-e0jGNZDOHfBXJGz8vR/sIMXvBIGJJcqFjmlg9lmE+5KX1U7/RZNMswfD8nKnNCnQdKTIj50IaRKwl1fvMLyyRg== + version "0.5.46" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.46.tgz#a21aa6392b3c6b3ed916cd5e95858a28d893704a" + integrity sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw== dependencies: moment "^2.29.4" -moment@^2.27.0: +moment@^2.29.4: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== -moment@^2.29.4: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== - mongodb-connection-string-url@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz#b4f87f92fd8593f3b9365f592515a06d304a1e9c" @@ -16215,11 +15893,6 @@ mongodb@6.7.0: bson "^6.7.0" mongodb-connection-string-url "^3.0.0" -moo@^0.5.0, moo@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" - integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -16261,17 +15934,17 @@ msgpackr@1.10.1, msgpackr@^1.5.2: optionalDependencies: msgpackr-extract "^3.0.2" -mssql@10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/mssql/-/mssql-10.0.1.tgz#96053ae91b96fdc0469b9d8ca34663d448075bdf" - integrity sha512-k0Xkav/3OppZs8Kj+FIo7k7ejbcsVNxp5/ePayxfXzuBZhxD/Y/RhIhrtfHyH6FmlJnBQPj7eDI2IN7B0BiSxQ== +mssql@11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/mssql/-/mssql-11.0.1.tgz#a32ab7763bfbb3f5d970e47563df3911fc04e21d" + integrity sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w== dependencies: "@tediousjs/connection-string" "^0.5.0" commander "^11.0.0" debug "^4.3.3" rfdc "^1.3.0" tarn "^3.0.2" - tedious "^16.4.0" + tedious "^18.2.1" multi-part-lite@^1.0.0: version "1.0.0" @@ -16286,7 +15959,7 @@ multi-part@^3.0.0: mime-kind "^3.0.0" multi-part-lite "^1.0.0" -multimatch@5.0.0: +multimatch@5.0.0, multimatch@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== @@ -16337,11 +16010,6 @@ named-placeholders@^1.1.3: dependencies: lru-cache "^7.14.1" -nan@^2.15.0: - version "2.22.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3" - integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== - nan@^2.17.0, nan@^2.18.0: version "2.18.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" @@ -16392,36 +16060,16 @@ ndjson@^1.4.3: split2 "^2.1.0" through2 "^2.0.3" -nearley@^2.19.5: - version "2.20.1" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" - integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== - dependencies: - commander "^2.19.0" - moo "^0.5.0" - railroad-diagrams "^1.0.0" - randexp "0.4.6" - negotiator@0.6.3, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -negotiator@^0.6.2: - version "0.6.4" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" - integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== - neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -neon-env@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/neon-env/-/neon-env-0.1.3.tgz#071e86fde3c698e9314f057d209e0b79ddab16e9" - integrity sha512-Zo+L6Nm19gJrjyfhxn/ZDm8eIIDzr75o64ZhijBau4LNuhLzjEAteRg3gchIvgaN8XTo5BxN6iTNP5clZQ0agA== - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -16436,6 +16084,15 @@ nock@13.5.4, nock@^13.5.4: json-stringify-safe "^5.0.1" propagate "^2.0.0" +nock@^13.5.6: + version "13.5.6" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.6.tgz#5e693ec2300bbf603b61dae6df0225673e6c4997" + integrity sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + propagate "^2.0.0" + node-abi@^3.3.0: version "3.54.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.54.0.tgz#f6386f7548817acac6434c6cba02999c9aebcc69" @@ -16443,7 +16100,7 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" -node-abort-controller@^3.0.1, node-abort-controller@^3.1.1: +node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== @@ -16468,14 +16125,14 @@ node-domexception@1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@2.6.0, node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0: +node-fetch@2.6.7, node-fetch@2.6.9, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -node-forge@^1.2.1, node-forge@^1.3.1: +node-forge@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== @@ -16491,31 +16148,15 @@ node-gyp-build@<4.0, node-gyp-build@^3.9.0: integrity sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A== node-gyp-build@^4.3.0, node-gyp-build@^4.5.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== node-gyp-build@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb" integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ== -node-gyp@^8.2.0: - version "8.4.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" - integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^9.1.0" - nopt "^5.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - node-gyp@^9.0.0: version "9.3.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.1.tgz#1e19f5f290afcc9c46973d68700cbd21a96192e4" @@ -16577,11 +16218,6 @@ node-source-walk@^5.0.0: dependencies: "@babel/parser" "^7.0.0" -nodemailer@6.9.13: - version "6.9.13" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.13.tgz#5b292bf1e92645f4852ca872c56a6ba6c4a3d3d6" - integrity sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA== - nodemailer@6.9.9: version "6.9.9" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.9.tgz#4549bfbf710cc6addec5064dd0f19874d24248d9" @@ -16795,13 +16431,6 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npm-run-path@^5.1.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" - integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== - dependencies: - path-key "^4.0.0" - npmlog@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" @@ -16832,7 +16461,7 @@ nth-check@^2.0.1: nunjucks@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/nunjucks/-/nunjucks-3.2.4.tgz#f0878eef528ce7b0aa35d67cc6898635fd74649e" - integrity "sha1-8IeO71KM57CqNdZ8xomGNf10ZJ4= sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==" + integrity sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ== dependencies: a-sync-waterfall "^1.0.0" asap "^2.0.3" @@ -16932,11 +16561,6 @@ object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-hash@^2.0.3: - version "2.2.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" - integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== - object-hash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" @@ -16947,6 +16571,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-inspect@^1.13.3: + version "1.13.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" + integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== + object-is@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" @@ -17033,7 +16662,7 @@ octal@^1.0.0: resolved "https://registry.yarnpkg.com/octal/-/octal-1.0.0.tgz#63e7162a68efbeb9e213588d58e989d1e5c4530b" integrity sha512-nnda7W8d+A3vEIY+UrDQzzboPf1vhs4JYVhff5CDkq9QNoZY7Xrxeo/htox37j9dZf7yNHevZzqtejWgy1vCqQ== -omggif@^1.0.10, omggif@^1.0.9: +omggif@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw== @@ -17076,13 +16705,6 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - only@~0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" @@ -17244,11 +16866,6 @@ p-cancelable@^1.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== -p-cancelable@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" - integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== - p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -17466,23 +17083,23 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.7: pbkdf2 "^3.1.2" safe-buffer "^5.2.1" -parse-bmfont-ascii@^1.0.3: +parse-bmfont-ascii@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz#11ac3c3ff58f7c2020ab22769079108d4dfa0285" integrity sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA== -parse-bmfont-binary@^1.0.5: +parse-bmfont-binary@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz#d038b476d3e9dd9db1e11a0b0e53a22792b69006" integrity sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA== -parse-bmfont-xml@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/parse-bmfont-xml/-/parse-bmfont-xml-1.1.4.tgz#015319797e3e12f9e739c4d513872cd2fa35f389" - integrity sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ== +parse-bmfont-xml@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz#016b655da7aebe6da38c906aca16bf0415773767" + integrity sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA== dependencies: xml-parse-from-string "^1.0.0" - xml2js "^0.4.5" + xml2js "^0.5.0" parse-headers@^2.0.0: version "2.0.5" @@ -17567,7 +17184,7 @@ passport-google-oauth20@2.x.x: dependencies: passport-oauth2 "1.x.x" -passport-google-oauth@2.0.0, passport-google-oauth@^2.0.0: +passport-google-oauth@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/passport-google-oauth/-/passport-google-oauth-2.0.0.tgz#f6eb4bc96dd6c16ec0ecfdf4e05ec48ca54d4dae" integrity sha512-JKxZpBx6wBQXX1/a1s7VmdBgwOugohH+IxCy84aPTZNq/iIPX6u7Mqov1zY7MKRz3niFPol0KJz8zPLBoHKtYA== @@ -17582,14 +17199,6 @@ passport-local@1.0.0: dependencies: passport-strategy "1.x.x" -passport-microsoft@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/passport-microsoft/-/passport-microsoft-1.0.0.tgz#78954cf3201fdce61beeb6587a3b158f8e9db86c" - integrity sha512-L1JHeCbSObSZZXiG7jU2KoKie6nzZLwGt38HXz1GasKrsCQdOnf5kH8ltV4BWNUfBL2Pt1csWn1iuBSerprrcg== - dependencies: - passport-oauth2 "1.6.1" - pkginfo "0.4.x" - passport-oauth1@1.x.x: version "1.3.0" resolved "https://registry.yarnpkg.com/passport-oauth1/-/passport-oauth1-1.3.0.tgz#5d57f1415c8e28e46b461a12ec1b492934f7c354" @@ -17604,17 +17213,6 @@ passport-oauth2-refresh@^2.1.0: resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.1.0.tgz#c31cd133826383f5539d16ad8ab4f35ca73ce4a4" integrity sha512-4ML7ooCESCqiTgdDBzNUFTBcPR8zQq9iM6eppEUGMMvLdsjqRL93jKwWm4Az3OJcI+Q2eIVyI8sVRcPFvxcF/A== -passport-oauth2@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" - integrity sha512-ZbV43Hq9d/SBSYQ22GOiglFsjsD1YY/qdiptA+8ej+9C1dL1TVB+mBE5kDH/D4AJo50+2i8f4bx0vg4/yDDZCQ== - dependencies: - base64url "3.x.x" - oauth "0.9.x" - passport-strategy "1.x.x" - uid2 "0.0.x" - utils-merge "1.x.x" - passport-oauth2@1.x.x: version "1.7.0" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.7.0.tgz#5c4766c8531ac45ffe9ec2c09de9809e2c841fc4" @@ -17665,11 +17263,6 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-key@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" - integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== - path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -17691,22 +17284,15 @@ path-scurry@^1.11.1, path-scurry@^1.6.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@1.x: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" +path-to-regexp@^0.1.10: + version "0.1.11" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.11.tgz#a527e662c89efc4646dbfa8100bf3e847e495761" + integrity sha512-c0t+KCuUkO/YDLPG4WWzEwx3J5F/GHXsD1h/SNZfySqAIKe/BaP95x8fWtOfRJokpS5yYHRJjMtYlXD8jxnpbw== -path-to-regexp@^0.1.2: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -path-to-regexp@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" - integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== +path-to-regexp@^6.1.0, path-to-regexp@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== path-type@^3.0.0: version "3.0.0" @@ -17730,11 +17316,6 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pathval@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" - integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== - pause@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" @@ -17775,17 +17356,12 @@ periscopic@^3.1.0: estree-walker "^3.0.0" is-reference "^3.0.0" -pg-cloudflare@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" - integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== - pg-connection-string@2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== -pg-connection-string@^2.5.0, pg-connection-string@^2.7.0: +pg-connection-string@^2.5.0: version "2.7.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37" integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA== @@ -17795,46 +17371,16 @@ pg-int8@1.0.1: resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== -pg-mem@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/pg-mem/-/pg-mem-3.0.3.tgz#0c8862f9bd096a3dcd2addf97e96675fd687a7ad" - integrity sha512-Bwg8T46AEMjAmqFT5KIuR2ukRE4GzkVSic7U8CAXqlzFr631N9fTGo4JqNgBHrRCT1qR4Nt2bjZA1AL9JhP4uw== - dependencies: - functional-red-black-tree "^1.0.1" - immutable "^4.3.4" - json-stable-stringify "^1.0.1" - lru-cache "^6.0.0" - moment "^2.27.0" - object-hash "^2.0.3" - pgsql-ast-parser "^12.0.1" - -pg-pool@^3.6.0, pg-pool@^3.7.0: +pg-pool@^3.6.0: version "3.7.0" resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.7.0.tgz#d4d3c7ad640f8c6a2245adc369bafde4ebb8cbec" integrity sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g== -pg-protocol@*, pg-protocol@^1.6.0, pg-protocol@^1.7.0: +pg-protocol@*, pg-protocol@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.7.0.tgz#ec037c87c20515372692edac8b63cf4405448a93" integrity sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ== -pg-query-native@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/pg-query-native/-/pg-query-native-1.3.1.tgz#66ac04d6e0a4d3f964cadc4b7b452461f843b004" - integrity sha512-EXajlKmSVIKqZip7VL05lvwsUqq5LfiNKYodhWcTfrTvK1w6TatkVPoyp0rhHv7+msR9hhT9t7BgJaaGK1QFLA== - dependencies: - bindings "^1.5.0" - nan "^2.15.0" - node-gyp "^8.2.0" - -pg-query-parser@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/pg-query-parser/-/pg-query-parser-0.3.0.tgz#f7fc0d909674b02b69c46b91c37d12a9fde90ef6" - integrity sha512-7RF8ijM2PtXTQ0B6TZVtP9fYBc1nc6ULq6f5CGlq29qXQFkTaumlQqSxlNAYOMVQGA1Vt2sSfqeViqM+nQiVEQ== - dependencies: - lodash "^4.17.21" - pg-query-native "^1.3.1" - pg-types@^2.1.0, pg-types@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" @@ -17859,19 +17405,6 @@ pg@8.10.0: pg-types "^2.1.0" pgpass "1.x" -pg@^8.12.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.13.0.tgz#e3d245342eb0158112553fcc1890a60720ae2a3d" - integrity sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw== - dependencies: - pg-connection-string "^2.7.0" - pg-pool "^3.7.0" - pg-protocol "^1.7.0" - pg-types "^2.1.0" - pgpass "1.x" - optionalDependencies: - pg-cloudflare "^1.1.1" - pgpass@1.x: version "1.0.5" resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" @@ -17879,34 +17412,21 @@ pgpass@1.x: dependencies: split2 "^4.1.0" -pgsql-ast-parser@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/pgsql-ast-parser/-/pgsql-ast-parser-12.0.1.tgz#7b9d9880cb62df4b4c595c258b8e7a4b5f44ce53" - integrity sha512-pe8C6Zh5MsS+o38WlSu18NhrTjAv1UNMeDTs2/Km2ZReZdYBYtwtbWGZKK2BM2izv5CrQpbmP0oI10wvHOwv4A== - dependencies: - moo "^0.5.1" - nearley "^2.19.5" - -phin@^2.9.1: - version "2.9.3" - resolved "https://registry.yarnpkg.com/phin/-/phin-2.9.3.tgz#f9b6ac10a035636fb65dfc576aaaa17b8743125c" - integrity sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA== - picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" - integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== - pify@5.0.0, pify@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" @@ -18049,12 +17569,12 @@ pirates@^4.0.4: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== -pixelmatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854" - integrity sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA== +pixelmatch@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.3.0.tgz#5e5321a7abedfb7962d60dbf345deda87cb9560a" + integrity sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q== dependencies: - pngjs "^3.0.0" + pngjs "^6.0.0" pkg-dir@^4.2.0: version "4.2.0" @@ -18072,26 +17592,28 @@ pkg-types@^1.1.1: mlly "^1.7.1" pathe "^1.1.2" -pkginfo@0.4.x: - version "0.4.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff" - integrity sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ== +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== -pngjs@^3.0.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" - integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== - pngjs@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821" integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg== +pngjs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-7.0.0.tgz#a8b7446020ebbc6ac739db6c5415a65d17090e26" + integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow== + possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" @@ -18385,7 +17907,7 @@ postcss-values-parser@^6.0.2: is-url-superb "^4.0.0" quote-unquote "^1.0.0" -postcss@^8.1.7, postcss@^8.2.9, postcss@^8.3.11, postcss@^8.4.12, postcss@^8.4.27, postcss@^8.4.29, postcss@^8.4.35, postcss@^8.4.41, postcss@^8.4.5: +postcss@^8.1.7, postcss@^8.2.9, postcss@^8.3.11, postcss@^8.4.12, postcss@^8.4.27, postcss@^8.4.29, postcss@^8.4.35, postcss@^8.4.5: version "8.4.41" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681" integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ== @@ -18394,6 +17916,15 @@ postcss@^8.1.7, postcss@^8.2.9, postcss@^8.3.11, postcss@^8.4.12, postcss@^8.4.2 picocolors "^1.0.1" source-map-js "^1.2.0" +postcss@^8.4.48: + version "8.4.49" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" + integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.1" + source-map-js "^1.2.1" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -18416,7 +17947,7 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" -posthog-js@^1.118.0, posthog-js@^1.13.4: +posthog-js@^1.118.0: version "1.160.0" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.160.0.tgz#ad686f3c161c7dc2ba716281b5cef94c64ce41b1" integrity sha512-K/RRgmPYIpP69nnveCJfkclb8VU+R+jsgqlrKaLGsM5CtQM9g01WOzAiT3u36WLswi58JiFMXgJtECKQuoqTgQ== @@ -18441,19 +17972,18 @@ pouch-stream@^0.4.0: inherits "^2.0.1" readable-stream "^1.0.27-1" -pouchdb-abstract-mapreduce@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-abstract-mapreduce/-/pouchdb-abstract-mapreduce-7.2.2.tgz#dd1b10a83f8d24361dce9aaaab054614b39f766f" - integrity sha512-7HWN/2yV2JkwMnGnlp84lGvFtnm0Q55NiBUdbBcaT810+clCGKvhssBCrXnmwShD1SXTwT83aszsgiSfW+SnBA== +pouchdb-abstract-mapreduce@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-abstract-mapreduce/-/pouchdb-abstract-mapreduce-9.0.0.tgz#d5f189a6f8980931835c41ea1f0b692368ce4686" + integrity sha512-SnTtqwAEiAa3uxKbc1J7LfiBViwEkKe2xkK92zxyTXPqWBvMnh4UU3GXxx7GrXTM4L9llsQ3lSjpbH4CNqG1Mw== dependencies: - pouchdb-binary-utils "7.2.2" - pouchdb-collate "7.2.2" - pouchdb-collections "7.2.2" - pouchdb-errors "7.2.2" - pouchdb-fetch "7.2.2" - pouchdb-mapreduce-utils "7.2.2" - pouchdb-md5 "7.2.2" - pouchdb-utils "7.2.2" + pouchdb-binary-utils "9.0.0" + pouchdb-collate "9.0.0" + pouchdb-errors "9.0.0" + pouchdb-fetch "9.0.0" + pouchdb-mapreduce-utils "9.0.0" + pouchdb-md5 "9.0.0" + pouchdb-utils "9.0.0" pouchdb-adapter-leveldb-core@7.2.2: version "7.2.2" @@ -18514,10 +18044,15 @@ pouchdb-binary-utils@7.2.2: dependencies: buffer-from "1.1.1" -pouchdb-collate@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-collate/-/pouchdb-collate-7.2.2.tgz#fc261f5ef837c437e3445fb0abc3f125d982c37c" - integrity sha512-/SMY9GGasslknivWlCVwXMRMnQ8myKHs4WryQ5535nq1Wj/ehpqWloMwxEQGvZE1Sda3LOm7/5HwLTcB8Our+w== +pouchdb-binary-utils@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-binary-utils/-/pouchdb-binary-utils-9.0.0.tgz#eafed32c21e92ef4b253456f9e53c4cf2cfd99fd" + integrity sha512-2OMtgDZi82vqs+zNDE0YiYjOaWkYCUcZJZKK3WkRr+XYRu+2B7umJrnygJFhUwoGedBbHSrlQBLhdNV3F1AX1A== + +pouchdb-collate@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-collate/-/pouchdb-collate-9.0.0.tgz#654f6766927ada60603ba25b6b2ae533564fa302" + integrity sha512-TrnEDNZEmIIl+W3xKUO8h+geqVLQ90oZe5ujPkl8myUzpREULWXWQBnV5EzPXVEKDBpJlb8T3I6oy/zdWGQpdA== pouchdb-collections@7.2.2: version "7.2.2" @@ -18531,27 +18066,31 @@ pouchdb-errors@7.2.2: dependencies: inherits "2.0.4" -pouchdb-fetch@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-fetch/-/pouchdb-fetch-7.2.2.tgz#492791236d60c899d7e9973f9aca0d7b9cc02230" - integrity sha512-lUHmaG6U3zjdMkh8Vob9GvEiRGwJfXKE02aZfjiVQgew+9SLkuOxNw3y2q4d1B6mBd273y1k2Lm0IAziRNxQnA== - dependencies: - abort-controller "3.0.0" - fetch-cookie "0.10.1" - node-fetch "2.6.0" +pouchdb-errors@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-errors/-/pouchdb-errors-9.0.0.tgz#f84269ce3327abef9455c0a90a51c26d7dca20c6" + integrity sha512-961PSMLhW0UqqdJ566g+CdLZ5pkBJRd6l4WWpCDdD0USvE4xYfYGzv43w7nZZBw1k3Xdy092yqPge7yX/tfnyw== -pouchdb-find@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-find/-/pouchdb-find-7.2.2.tgz#1227afdd761812d508fe0794b3e904518a721089" - integrity sha512-BmFeFVQ0kHmDehvJxNZl9OmIztCjPlZlVSdpijuFbk/Fi1EFPU1BAv3kLC+6DhZuOqU/BCoaUBY9sn66pPY2ag== +pouchdb-fetch@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-fetch/-/pouchdb-fetch-9.0.0.tgz#a2cf407c75c9fc68a1924b08c9b574d28e1be7dd" + integrity sha512-TbE3cUcAJQrwb9kr44tDP0X+NAbcqgjsTvcL30L4xzBNJeCPTIRjukYX80s154SHJUXBxcWRiPsMmNqpXsjfCA== dependencies: - pouchdb-abstract-mapreduce "7.2.2" - pouchdb-collate "7.2.2" - pouchdb-errors "7.2.2" - pouchdb-fetch "7.2.2" - pouchdb-md5 "7.2.2" - pouchdb-selector-core "7.2.2" - pouchdb-utils "7.2.2" + fetch-cookie "2.2.0" + node-fetch "2.6.9" + +pouchdb-find@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-find/-/pouchdb-find-9.0.0.tgz#3d1b80d2adc9f9fd86c2ad559cd0144e406cb539" + integrity sha512-vvVhq4eEOmSkwSRwf2NBYtdhURB7ryJ7sUI4WDN00GuLUj2g8jAXBJuZIryVgdYt/5S5cfn70iRL6Eow+LFhpA== + dependencies: + pouchdb-abstract-mapreduce "9.0.0" + pouchdb-collate "9.0.0" + pouchdb-errors "9.0.0" + pouchdb-fetch "9.0.0" + pouchdb-md5 "9.0.0" + pouchdb-selector-core "9.0.0" + pouchdb-utils "9.0.0" pouchdb-json@7.2.2: version "7.2.2" @@ -18560,15 +18099,12 @@ pouchdb-json@7.2.2: dependencies: vuvuzela "1.0.3" -pouchdb-mapreduce-utils@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-mapreduce-utils/-/pouchdb-mapreduce-utils-7.2.2.tgz#13a46a3cc2a3f3b8e24861da26966904f2963146" - integrity sha512-rAllb73hIkU8rU2LJNbzlcj91KuulpwQu804/F6xF3fhZKC/4JQMClahk+N/+VATkpmLxp1zWmvmgdlwVU4HtQ== +pouchdb-mapreduce-utils@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-mapreduce-utils/-/pouchdb-mapreduce-utils-9.0.0.tgz#8a2edf30ca0fa24d095eabcfbe8ebb8f3f1160e3" + integrity sha512-Bjh8W6QXqp1j7MKmHhYYp5cYlcQsm5drD8Jd/F+ZlfNt18uiD2SQXWzGM5797+tiW/LszFGb8ttw0uHWjxufCQ== dependencies: - argsarray "0.0.1" - inherits "2.0.4" - pouchdb-collections "7.2.2" - pouchdb-utils "7.2.2" + pouchdb-utils "9.0.0" pouchdb-md5@7.2.2: version "7.2.2" @@ -18578,6 +18114,14 @@ pouchdb-md5@7.2.2: pouchdb-binary-utils "7.2.2" spark-md5 "3.0.1" +pouchdb-md5@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-md5/-/pouchdb-md5-9.0.0.tgz#f67a2ba627309e65f8d1ce4d4baf6a5f29164617" + integrity sha512-58xUYBvW3/s+aH0j4uOhhN8yCk0LQ254cxBzI/gbKA9PrfwHpe4zrr0L/ia5ml3A30oH1f8aTnuVMwWDkFcuww== + dependencies: + pouchdb-binary-utils "9.0.0" + spark-md5 "3.0.2" + pouchdb-merge@7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/pouchdb-merge/-/pouchdb-merge-7.2.2.tgz#940d85a2b532d6a93a6cab4b250f5648511bcc16" @@ -18590,13 +18134,13 @@ pouchdb-promise@6.4.3, pouchdb-promise@^6.0.4: dependencies: lie "3.1.1" -pouchdb-selector-core@7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/pouchdb-selector-core/-/pouchdb-selector-core-7.2.2.tgz#264d7436a8c8ac3801f39960e79875ef7f3879a0" - integrity sha512-XYKCNv9oiNmSXV5+CgR9pkEkTFqxQGWplnVhO3W9P154H08lU0ZoNH02+uf+NjZ2kjse7Q1fxV4r401LEcGMMg== +pouchdb-selector-core@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-selector-core/-/pouchdb-selector-core-9.0.0.tgz#6fee1df82cd5ecdbd0a034b38e6c604557d2e22a" + integrity sha512-ZYHYsdoedwm8j5tYofz+3+uUSK8i+7tRCBb01T0OuqDQb17+w5mzjHF8Ppi160xdPUPaWCo1Un+nLWGJzkmA3g== dependencies: - pouchdb-collate "7.2.2" - pouchdb-utils "7.2.2" + pouchdb-collate "9.0.0" + pouchdb-utils "9.0.0" pouchdb-utils@7.2.2: version "7.2.2" @@ -18612,6 +18156,15 @@ pouchdb-utils@7.2.2: pouchdb-md5 "7.2.2" uuid "8.1.0" +pouchdb-utils@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb-utils/-/pouchdb-utils-9.0.0.tgz#b68f3259add50163998201d1a6d16e6a35d5d57f" + integrity sha512-xWZE5c+nAslgmLC8JBZbky8AYgdz7pKtv7KTSi6CD2tuQD0WyNKib0YnhZndeE84dksTeZlqlg56RQHsHoB2LQ== + dependencies: + pouchdb-errors "9.0.0" + pouchdb-md5 "9.0.0" + uuid "8.3.2" + pouchdb@7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/pouchdb/-/pouchdb-7.3.0.tgz#440fbef12dfd8f9002320802528665e883a3b7f8" @@ -18638,10 +18191,30 @@ pouchdb@7.3.0: uuid "8.3.2" vuvuzela "1.0.3" -pprof-format@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.0.7.tgz#526e4361f8b37d16b2ec4bb0696b5292de5046a4" - integrity sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA== +pouchdb@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/pouchdb/-/pouchdb-9.0.0.tgz#569ee3941f7b03dd34b4b4e53132a9772981a35e" + integrity sha512-6wjFc/PzwaWz86rmMXoqdBlR/fBSkNoWO1mEJO7RZNS6n3xf+fhhXWAWtws741KpLKx84IkmmJ48tp+fhFzj4A== + dependencies: + double-ended-queue "2.1.0-0" + fetch-cookie "2.2.0" + level "6.0.1" + level-codec "9.0.2" + level-write-stream "1.0.0" + leveldown "6.1.1" + levelup "4.4.0" + ltgt "2.2.1" + node-fetch "2.6.9" + readable-stream "1.1.14" + spark-md5 "3.0.2" + through2 "3.0.2" + uuid "8.3.2" + vuvuzela "1.0.3" + +pprof-format@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.1.0.tgz#acc8d7773bcf4faf0a3d3df11bceefba7ac06664" + integrity sha512-0+G5bHH0RNr8E5hoZo/zJYsL92MhkZjwrHp3O2IxmY8RJL9ooKeuZ8Tm0ZNBw5sGZ9TiM71sthTjWoR2Vf5/xw== preact@^10.19.3: version "10.20.1" @@ -18812,14 +18385,6 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== -promise-retry@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" - integrity sha512-StEy2osPr28o17bIW776GtwO6+Q+M9zPiZkYfosciUUMYqjhU/ffwRAH0zN2+uvGyUsn8/YICIHRzLbPacpZGw== - dependencies: - err-code "^1.0.0" - retry "^0.10.0" - promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -18833,7 +18398,7 @@ promise.series@^0.2.0: resolved "https://registry.yarnpkg.com/promise.series/-/promise.series-0.2.0.tgz#2cc7ebe959fc3a6619c04ab4dbdc9e452d864bbd" integrity sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ== -prompts@^2.0.1, prompts@^2.4.2: +prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -19039,7 +18604,7 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== -queue-microtask@^1.2.2: +queue-microtask@^1.2.2, queue-microtask@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== @@ -19059,29 +18624,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - quote-unquote@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/quote-unquote/-/quote-unquote-1.0.0.tgz#67a9a77148effeaf81a4d428404a710baaac8a0b" integrity sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg== -railroad-diagrams@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" - integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== - -randexp@0.4.6: - version "0.4.6" - resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" - integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== - dependencies: - discontinuous-range "1.0.0" - ret "~0.1.10" - randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -19129,11 +18676,6 @@ rc@1.2.8, rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -reachdown@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/reachdown/-/reachdown-1.1.0.tgz#c3b85b459dbd0fe2c79782233a0a38e66a9b5454" - integrity sha512-6LsdRe4cZyOjw4NnvbhUd/rGG7WQ9HMopPr+kyL018Uci4kijtxcGR5kVb5Ln13k4PEE+fEFQbjfOvNw7cnXmA== - react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" @@ -19242,6 +18784,15 @@ readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.2.2, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@^4.0.0, readable-stream@^4.2.0: version "4.5.1" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.1.tgz#3f2e4e66eab45606ac8f31597b9edb80c13b12ab" @@ -19282,7 +18833,7 @@ readdir-glob@^1.1.2: dependencies: minimatch "^5.1.0" -readdirp@~3.6.0: +readdirp@^3.6.0, readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== @@ -19372,11 +18923,6 @@ redlock@4.2.0: dependencies: bluebird "^3.7.2" -reflect-metadata@^0.2.1: - version "0.2.2" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" - integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== - regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" @@ -19389,11 +18935,6 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.3: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - regenerator-runtime@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" @@ -19406,11 +18947,6 @@ regenerator-transform@^0.15.1: dependencies: "@babel/runtime" "^7.8.4" -regexp-tree@~0.1.1: - version "0.1.27" - resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" - integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== - regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" @@ -19421,6 +18957,16 @@ regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: es-errors "^1.3.0" set-function-name "^2.0.1" +regexp.prototype.flags@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" + integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.2" + regexparam@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-2.0.2.tgz#a0f6aa057c67b1c9c09508c45823c0755b1f6e58" @@ -19512,6 +19058,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +require-package-name@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9" + integrity sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q== + requirejs-config-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/requirejs-config-file/-/requirejs-config-file-4.0.0.tgz#4244da5dd1f59874038cc1091d078d620abb6ebc" @@ -19535,11 +19086,6 @@ resize-observer-polyfill@^1.5.1: resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== -resolve-alpn@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -19552,6 +19098,14 @@ resolve-dependency-path@^2.0.0: resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-2.0.0.tgz#11700e340717b865d216c66cabeb4a2a3c696736" integrity sha512-DIgu+0Dv+6v2XwRaNWnumKu7GPufBBOr5I1gRPJHkvghrfCGOooJODFvgFimX/KRxk9j0whD2MnKHzM1jYvk9w== +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg== + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + resolve-from@5.0.0, resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" @@ -19575,7 +19129,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== -resolve@^1.10.0, resolve@^1.11.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.21.0, resolve@^1.22.1, resolve@^1.22.4: +resolve@^1.10.0, resolve@^1.11.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.21.0, resolve@^1.22.1, resolve@^1.22.3, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -19591,13 +19145,6 @@ responselike@1.0.2, responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" -responselike@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" - integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== - dependencies: - lowercase-keys "^2.0.0" - restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -19606,19 +19153,6 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -retry-request@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-5.0.2.tgz#143d85f90c755af407fcc46b7166a4ba520e44da" - integrity sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ== - dependencies: - debug "^4.1.1" - extend "^3.0.2" - retry-request@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-7.0.2.tgz#60bf48cfb424ec01b03fca6665dee91d06dd95f3" @@ -19633,11 +19167,6 @@ retry@0.13.1, retry@^0.13.1: resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity "sha1-GFsVh6z2eRnWOzVzSeA1N7JIRlg= sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" -retry@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ== - retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -19648,10 +19177,10 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== +rfdc@^1.3.0, rfdc@^1.3.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" @@ -19667,13 +19196,6 @@ rimraf@^4.4.1: dependencies: glob "^9.2.0" -rimraf@^5.0.7: - version "5.0.10" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c" - integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ== - dependencies: - glob "^10.3.7" - ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -19836,7 +19358,7 @@ rollup@^3.27.1: optionalDependencies: fsevents "~2.3.2" -rollup@^4.20.0, rollup@^4.9.4, rollup@^4.9.6: +rollup@^4.9.4, rollup@^4.9.6: version "4.21.2" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.2.tgz#f41f277a448d6264e923dd1ea179f0a926aaf9b7" integrity sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw== @@ -19936,13 +19458,6 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -safe-regex@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" - integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== - dependencies: - regexp-tree "~0.1.1" - safe-stable-stringify@^2.1.0, safe-stable-stringify@^2.3.1: version "2.4.3" resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" @@ -20054,7 +19569,7 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@7.5.3, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3, semver@~2.3.1: +"semver@2 || 3 || 4 || 5", semver@7.5.3, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1, semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@~2.3.1: version "7.5.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== @@ -20087,7 +19602,7 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" -server-destroy@1.0.1, server-destroy@^1.0.1: +server-destroy@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd" integrity sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ== @@ -20097,6 +19612,11 @@ set-blocking@^2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^2.4.8: + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== + set-function-length@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -20119,11 +19639,6 @@ set-function-name@^2.0.1, set-function-name@^2.0.2: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -20178,6 +19693,11 @@ shell-exec@1.0.2: resolved "https://registry.yarnpkg.com/shell-exec/-/shell-exec-1.0.2.tgz#2e9361b0fde1d73f476c4b6671fa17785f696756" integrity sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg== +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + shortid@2.2.15: version "2.2.15" resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122" @@ -20217,7 +19737,7 @@ signal-exit@3.0.7, signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, s resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1, signal-exit@^4.1.0: +signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -20291,38 +19811,30 @@ smob@^1.0.0: resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab" integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== -snowflake-promise@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/snowflake-promise/-/snowflake-promise-4.5.0.tgz#ceba611d27b3792966bc752c545760e0ce168c1c" - integrity sha512-IFY7Y1alCTY1WRFPIEcgCbjy7wCajwLNnJsvw2L7xdePir7y5ohh+S00PnF9zFRGbfVVlRh/VYqOYHEfERK2lg== - dependencies: - snowflake-sdk "^1.6.0" - -snowflake-sdk@^1.6.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/snowflake-sdk/-/snowflake-sdk-1.9.0.tgz#3bd089427549efc8efa4829c2d08deeffe4aded3" - integrity "sha1-O9CJQnVJ78jvpIKcLQje7/5K3tM= sha512-RtFRV2KC+ebQk/kOUg8WV42LnAu9puoan2wMXykgrAj1u4sGP/GgQyQhsAfLGwXWzn+J9JAwij07h3+6HYBmFw==" +snowflake-sdk@^1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/snowflake-sdk/-/snowflake-sdk-1.15.0.tgz#cc32fa0f2869d9e5a026e293b50d387ddbd67aca" + integrity sha512-u7eNIT2JWkA8USJF6gTOCcReNrdh8V9LCazJi3F0XnX5ZJkgPz2gNSn67drT4ywqNaXdXfFM0i/yNSa58fi2Rg== dependencies: "@aws-sdk/client-s3" "^3.388.0" - "@azure/storage-blob" "^12.11.0" - "@google-cloud/storage" "^6.9.3" - "@techteamer/ocsp" "1.0.0" - agent-base "^6.0.2" + "@aws-sdk/node-http-handler" "^3.374.0" + "@azure/storage-blob" "12.18.x" + "@google-cloud/storage" "^7.7.0" + "@techteamer/ocsp" "1.0.1" asn1.js-rfc2560 "^5.0.0" asn1.js-rfc5280 "^3.0.0" - axios "^1.5.0" + axios "^1.6.8" big-integer "^1.6.43" - bignumber.js "^2.4.0" + bignumber.js "^9.1.2" binascii "0.0.2" bn.js "^5.2.1" browser-request "^0.3.3" - debug "^3.2.6" expand-tilde "^2.0.2" - extend "^3.0.2" fast-xml-parser "^4.2.5" + fastest-levenshtein "^1.0.16" generic-pool "^3.8.2" - glob "^7.1.6" - https-proxy-agent "^5.0.1" + glob "^10.0.0" + https-proxy-agent "^7.0.2" jsonwebtoken "^9.0.0" mime-types "^2.1.29" mkdirp "^1.0.3" @@ -20331,8 +19843,7 @@ snowflake-sdk@^1.6.0: open "^7.3.1" python-struct "^1.1.3" simple-lru-cache "^0.0.2" - string-similarity "^4.0.4" - tmp "^0.2.1" + toml "^3.0.0" uuid "^8.3.2" winston "^3.1.0" @@ -20362,28 +19873,19 @@ socket.io-parser@~4.2.4: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" -socket.io@4.7.5: - version "4.7.5" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8" - integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA== +socket.io@4.8.1: + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a" + integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg== dependencies: accepts "~1.3.4" base64id "~2.0.0" cors "~2.8.5" debug "~4.3.2" - engine.io "~6.5.2" + engine.io "~6.6.0" socket.io-adapter "~2.5.2" socket.io-parser "~4.2.4" -socks-proxy-agent@^6.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" - integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - socks-proxy-agent@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" @@ -20441,6 +19943,11 @@ source-map-js@^1.0.1, source-map-js@^1.2.0: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -20568,6 +20075,11 @@ sprintf-js@^1.1.1, sprintf-js@^1.1.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -20598,9 +20110,9 @@ ssh2@^1.11.0, ssh2@^1.4.0: nan "^2.18.0" sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -20619,13 +20131,6 @@ ssri@^10.0.0, ssri@^10.0.1: dependencies: minipass "^4.0.0" -ssri@^8.0.0, ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - ssri@^9.0.0, ssri@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" @@ -20638,7 +20143,7 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== -stack-trace@0.0.10, stack-trace@0.0.x: +stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== @@ -20670,7 +20175,7 @@ statuses@2.0.1, statuses@^2.0.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -std-env@^3.3.1, std-env@^3.7.0: +std-env@^3.3.1: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== @@ -20704,6 +20209,11 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + stream-to-array@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" @@ -20749,21 +20259,7 @@ string-range@~1.2, string-range@~1.2.1: resolved "https://registry.yarnpkg.com/string-range/-/string-range-1.2.2.tgz#a893ed347e72299bc83befbbf2a692a8d239d5dd" integrity sha512-tYft6IFi8SjplJpxCUxyqisD3b+R2CSkomrtJYCkvuf1KuCAWgz7YXt4O0jip7efpfCemwHEzTEAO8EuOYgh3w== -string-similarity@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" - integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== - -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -20791,12 +20287,13 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: strip-ansi "^7.0.1" string.prototype.startswith@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.startswith/-/string.prototype.startswith-1.0.0.tgz#92a361fb1ac172033d53eb1db3d659b0cfab6280" - integrity sha512-VHhsDkuf8gsw4JNRK9cIZjYe6r7PsVUutVohaBhqYAoPaRADoQH+mMgUg7Cs/TgQeDGEvI+PzPEMOdvdsCMvpg== + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.startswith/-/string.prototype.startswith-1.0.1.tgz#623e2d013d93d3d2bbfbc9eed9e1010ba3f50ce8" + integrity sha512-7FoHkxvUevSBxSBXqsJgQy+IwuSPVl1jF31FEagFxkKnNKnmRLcHY6cJgxy074qrFq9T0OE36OU5aPw+z1v0yw== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" string.prototype.trim@^1.2.9: version "1.2.9" @@ -20854,7 +20351,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20868,13 +20365,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -20916,11 +20406,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -20952,14 +20437,6 @@ strip-outer@^1.0.0: dependencies: escape-string-regexp "^1.0.2" -stripe@9.16.0: - version "9.16.0" - resolved "https://registry.yarnpkg.com/stripe/-/stripe-9.16.0.tgz#94c24549c91fced457b9e3259e8a1a1bdb6dbd0e" - integrity sha512-Dn8K+jSoQcXjxCobRI4HXUdHjOXsiF/KszK49fJnkbeCFjZ3EZxLG2JiM/CX+Hcq27NBDtv/Sxhvy+HhTmvyaQ== - dependencies: - "@types/node" ">=8.1.0" - qs "^6.10.3" - striptags@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052" @@ -21028,17 +20505,20 @@ sublevel-pouchdb@7.2.2: ltgt "2.2.1" readable-stream "1.1.14" -subleveldown@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/subleveldown/-/subleveldown-5.0.1.tgz#aa2b4e4698a48d9a86856b2c4df1b6bce2d2ce53" - integrity sha512-cVqd/URpp7si1HWu5YqQ3vqQkjuolAwHypY1B4itPlS71/lsf6TQPZ2Y0ijT22EYVkvH5ove9JFJf4u7VGPuZw== +superagent@^10.1.1: + version "10.1.1" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-10.1.1.tgz#2f112591a5701a1d4467048580bcfda104e5a94d" + integrity sha512-9pIwrHrOj3uAnqg9gDlW7EA2xv+N5au/dSM0kM22HTqmUu8jBxNT+8uA7tA3UoCnmiqzpSbu8rasIUZvbyamMQ== dependencies: - abstract-leveldown "^6.3.0" - encoding-down "^6.2.0" - inherits "^2.0.3" - level-option-wrap "^1.1.0" - levelup "^4.4.0" - reachdown "^1.1.0" + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^3.5.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" superagent@^8.0.5: version "8.1.2" @@ -21133,6 +20613,26 @@ svelte-spa-router@^4.0.1: dependencies: regexparam "2.0.2" +svelte@4.2.19: + version "4.2.19" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.19.tgz#4e6e84a8818e2cd04ae0255fcf395bc211e61d4c" + integrity sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw== + dependencies: + "@ampproject/remapping" "^2.2.1" + "@jridgewell/sourcemap-codec" "^1.4.15" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/estree" "^1.0.1" + acorn "^8.9.0" + aria-query "^5.3.0" + axobject-query "^4.0.0" + code-red "^1.0.3" + css-tree "^2.3.1" + estree-walker "^3.0.3" + is-reference "^3.0.1" + locate-character "^3.0.0" + magic-string "^0.30.4" + periscopic "^3.1.0" + svelte@^4.2.10: version "4.2.12" resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.12.tgz#13d98d2274d24d3ad216c8fdc801511171c70bb1" @@ -21326,7 +20826,7 @@ tar@6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -tar@6.2.1, tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: +tar@6.2.1, tar@^6.1.11, tar@^6.1.2: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== @@ -21343,38 +20843,21 @@ tarn@^3.0.1, tarn@^3.0.2: resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== -teamcity-service-messages@^0.1.14: - version "0.1.14" - resolved "https://registry.yarnpkg.com/teamcity-service-messages/-/teamcity-service-messages-0.1.14.tgz#193d420a5e4aef8e5e50b8c39e7865e08fbb5d8a" - integrity sha512-29aQwaHqm8RMX74u2o/h1KbMLP89FjNiMxD9wbF2BbWOnbM+q+d1sCEC+MqCc4QW3NJykn77OMpTFw/xTHIc0w== - -tedious@^16.4.0: - version "16.7.1" - resolved "https://registry.yarnpkg.com/tedious/-/tedious-16.7.1.tgz#1190f30fd99a413f1dc9250dee4835cf0788b650" - integrity sha512-NmedZS0NJiTv3CoYnf1FtjxIDUgVYzEmavrc8q2WHRb+lP4deI9BpQfmNnBZZaWusDbP5FVFZCcvzb3xOlNVlQ== +tedious@^18.2.1: + version "18.6.1" + resolved "https://registry.yarnpkg.com/tedious/-/tedious-18.6.1.tgz#1c4a3f06c891be67a032117e2e25193286d44496" + integrity sha512-9AvErXXQTd6l7TDd5EmM+nxbOGyhnmdbp/8c3pw+tjaiSXW9usME90ET/CRG1LN1Y9tPMtz/p83z4Q97B4DDpw== dependencies: - "@azure/identity" "^3.4.1" + "@azure/core-auth" "^1.7.2" + "@azure/identity" "^4.2.1" "@azure/keyvault-keys" "^4.4.0" - "@js-joda/core" "^5.5.3" - bl "^6.0.3" - es-aggregate-error "^1.0.9" + "@js-joda/core" "^5.6.1" + "@types/node" ">=18" + bl "^6.0.11" iconv-lite "^0.6.3" js-md4 "^0.3.2" - jsbi "^4.3.0" native-duplexpair "^1.0.0" - node-abort-controller "^3.1.1" - sprintf-js "^1.1.2" - -teeny-request@^8.0.0: - version "8.0.3" - resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-8.0.3.tgz#5cb9c471ef5e59f2fca8280dc3c5909595e6ca24" - integrity "sha1-XLnEce9eWfL8qCgNw8WQlZXmyiQ= sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==" - dependencies: - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - node-fetch "^2.6.1" - stream-events "^1.0.5" - uuid "^9.0.0" + sprintf-js "^1.1.3" teeny-request@^9.0.0: version "9.0.0" @@ -21516,11 +20999,6 @@ timekeeper@^2.2.0: resolved "https://registry.yarnpkg.com/timekeeper/-/timekeeper-2.3.1.tgz#2deb6e0b95d93625fda84c18d47f84a99e4eba01" integrity sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g== -timm@^1.6.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f" - integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw== - tiny-glob@^0.2.9: version "0.2.9" resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" @@ -21534,7 +21012,7 @@ tiny-queue@^0.2.0: resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046" integrity sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A== -tinybench@^2.3.1, tinybench@^2.8.0: +tinybench@^2.3.1: version "2.9.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== @@ -21549,26 +21027,11 @@ tinypool@^0.4.0: resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.4.0.tgz#3cf3ebd066717f9f837e8d7d31af3c127fdb5446" integrity sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA== -tinypool@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe" - integrity sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA== - -tinyrainbow@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" - integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== - tinyspy@^1.0.2: version "1.1.1" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-1.1.1.tgz#0cb91d5157892af38cb2d217f5c7e8507a5bf092" integrity sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g== -tinyspy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.0.tgz#cb61644f2713cd84dee184863f4642e06ddf0585" - integrity sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA== - tlhunter-sorted-set@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/tlhunter-sorted-set/-/tlhunter-sorted-set-0.1.0.tgz#1c3eae28c0fa4dff97e9501d2e3c204b86406f4b" @@ -21643,6 +21106,11 @@ token-types@^4.1.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + toposort@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" @@ -21697,22 +21165,15 @@ trim-repeated@^1.0.0: escape-string-regexp "^1.0.2" triple-beam@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" - integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + version "1.4.1" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" + integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== ts-api-utils@^1.0.1, ts-api-utils@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -ts-declaration-location@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/ts-declaration-location/-/ts-declaration-location-1.0.4.tgz#60c64133202ec5d171fdf0395f70f786f92f14c0" - integrity sha512-r4JoxYhKULbZuH81Pjrp9OEG5St7XWk7zXwGkLKhmVcjiBVHTJXV5wK6dEa9JKW5QGSTW6b1lOjxAKp8R1SQhg== - dependencies: - minimatch "^10.0.0" - ts-graphviz@^1.5.0: version "1.5.4" resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.5.4.tgz#61a3059afeac4f6d4be3c6729a4d88546ca9e095" @@ -21751,39 +21212,6 @@ ts-node@10.8.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tsconfck@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.1.tgz#c7284913262c293b43b905b8b034f524de4a3162" - integrity sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ== - -tsconfig-paths-webpack-plugin@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763" - integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA== - dependencies: - chalk "^4.1.0" - enhanced-resolve "^5.7.0" - tsconfig-paths "^4.1.2" - tsconfig-paths@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.0.0.tgz#1082f5d99fd127b72397eef4809e4dd06d229b64" @@ -21812,16 +21240,21 @@ tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.2: +tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.5.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsscmp@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" @@ -21971,11 +21404,6 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" -typed-duration@^1.0.12: - version "1.0.13" - resolved "https://registry.yarnpkg.com/typed-duration/-/typed-duration-1.0.13.tgz#a40f9ba563b6e20674cac491e15ecbf6811d85a7" - integrity sha512-HLwA+hNq/2eXe03isJSfa7YJt6NikplBGdNKvlhyuR6WL5iZi2uXJIZv1SSOMEIukCZbeQ8QwIcQ801S0/Qulw== - typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -22005,7 +21433,7 @@ typeof@^1.0.0: resolved "https://registry.yarnpkg.com/typeof/-/typeof-1.0.0.tgz#9c84403f2323ae5399167275497638ea1d2f2440" integrity sha512-Pze0mIxYXhaJdpw1ayMzOA7rtGr1OmsTY/Z+FWtRKIqXFz6aoDLjqdbWE/tcIBSC8nhnVXiRrEXujodR/xiFAA== -typescript-eslint@^7.16.1, typescript-eslint@^7.3.1: +typescript-eslint@^7.3.1: version "7.18.0" resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-7.18.0.tgz#e90d57649b2ad37a7475875fa3e834a6d9f61eb2" integrity sha512-PonBkP603E3tt05lDkbOMyaxJjvKqQrXsnow72sVeOFINDE/qNmnnd+f9b4N+U7W6MXnnYyrhtmF2t08QWwUbA== @@ -22019,7 +21447,7 @@ typescript@5.5.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== -"typescript@>=3 < 6", typescript@^5.5.3: +"typescript@>=3 < 6": version "5.5.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== @@ -22087,7 +21515,7 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~6.19.2: +undici-types@~6.19.2, undici-types@~6.19.8: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== @@ -22125,13 +21553,6 @@ uniq@^1.0.1: resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" integrity sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA== -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - unique-filename@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" @@ -22146,13 +21567,6 @@ unique-filename@^3.0.0: dependencies: unique-slug "^4.0.0" -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - unique-slug@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" @@ -22220,7 +21634,7 @@ update-browserslist-db@^1.0.10: escalade "^3.1.1" picocolors "^1.0.0" -update-dotenv@1.1.1, update-dotenv@^1.1.1: +update-dotenv@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/update-dotenv/-/update-dotenv-1.1.1.tgz#17146f302f216c3c92419d5a327a45be910050ca" integrity sha512-3cIC18In/t0X/yH793c00qqxcKD8jVCgNOPif/fGQkFpYMGecM9YAc+kaAKXuZsM2dE9I9wFI7KvAuNX22SGMQ== @@ -22252,11 +21666,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -urijs@^1.19.2: - version "1.19.11" - resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.11.tgz#204b0d6b605ae80bea54bea39280cdb7c9f923cc" - integrity sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ== - url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" @@ -22264,7 +21673,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.4.3, url-parse@^1.5.3: +url-parse@^1.5.3: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== @@ -22292,7 +21701,7 @@ utf-8-validate@^5.0.2: dependencies: node-gyp-build "^4.3.0" -utif2@^4.0.1: +utif2@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/utif2/-/utif2-4.1.0.tgz#e768d37bd619b995d56d9780b5d2b4611a3d932b" integrity sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w== @@ -22304,6 +21713,17 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +util@^0.12.4: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + utils-merge@1.x.x, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -22314,10 +21734,10 @@ uuid-random@^1.3.2: resolved "https://registry.yarnpkg.com/uuid-random/-/uuid-random-1.3.2.tgz#96715edbaef4e84b1dcf5024b00d16f30220e2d0" integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ== -uuid@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== +uuid@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== uuid@8.1.0: version "8.1.0" @@ -22339,11 +21759,6 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" - integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== - uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" @@ -22426,17 +21841,6 @@ vite-node@0.29.8: picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0" -vite-node@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.0.5.tgz#36d909188fc6e3aba3da5fc095b3637d0d18e27b" - integrity sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q== - dependencies: - cac "^6.7.14" - debug "^4.3.5" - pathe "^1.1.2" - tinyrainbow "^1.2.0" - vite "^5.0.0" - vite-plugin-static-copy@^0.17.0: version "0.17.0" resolved "https://registry.yarnpkg.com/vite-plugin-static-copy/-/vite-plugin-static-copy-0.17.0.tgz#e45527da186c4a3818d09635797b6fc7cc9e035f" @@ -22447,15 +21851,6 @@ vite-plugin-static-copy@^0.17.0: fs-extra "^11.1.0" picocolors "^1.0.0" -vite-tsconfig-paths@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9" - integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA== - dependencies: - debug "^4.1.1" - globrex "^0.1.2" - tsconfck "^3.0.3" - "vite@^3.0.0 || ^4.0.0", vite@^4.5.0: version "4.5.3" resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.3.tgz#d88a4529ea58bae97294c7e2e6f0eab39a50fb1a" @@ -22467,17 +21862,6 @@ vite-tsconfig-paths@^4.3.2: optionalDependencies: fsevents "~2.3.2" -vite@^5.0.0: - version "5.4.2" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.2.tgz#8acb6ec4bfab823cdfc1cb2d6c53ed311bc4e47e" - integrity sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA== - dependencies: - esbuild "^0.21.3" - postcss "^8.4.41" - rollup "^4.20.0" - optionalDependencies: - fsevents "~2.3.3" - vitefu@^0.2.2: version "0.2.5" resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969" @@ -22513,31 +21897,6 @@ vitest@^0.29.2: vite-node "0.29.8" why-is-node-running "^2.2.2" -vitest@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.0.5.tgz#2f15a532704a7181528e399cc5b754c7f335fd62" - integrity sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA== - dependencies: - "@ampproject/remapping" "^2.3.0" - "@vitest/expect" "2.0.5" - "@vitest/pretty-format" "^2.0.5" - "@vitest/runner" "2.0.5" - "@vitest/snapshot" "2.0.5" - "@vitest/spy" "2.0.5" - "@vitest/utils" "2.0.5" - chai "^5.1.1" - debug "^4.3.5" - execa "^8.0.1" - magic-string "^0.30.10" - pathe "^1.1.2" - std-env "^3.7.0" - tinybench "^2.8.0" - tinypool "^1.0.0" - tinyrainbow "^1.2.0" - vite "^5.0.0" - vite-node "2.0.5" - why-is-node-running "^2.3.0" - vlq@^0.2.2: version "0.2.3" resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" @@ -22586,11 +21945,6 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -watskeburt@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/watskeburt/-/watskeburt-4.1.0.tgz#3c0227669be646a97424b631164b1afe3d4d5344" - integrity sha512-KkY5H51ajqy9HYYI+u9SIURcWnqeVVhdH0I+ab6aXPGHfZYxgRCwnR6Lm3+TYB6jJVt5jFqw4GAKmwf1zHmGQw== - wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" @@ -22650,11 +22004,6 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" -whatwg-fetch@^3.4.1: - version "3.6.20" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" - integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== - whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" @@ -22724,7 +22073,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15: +which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2: version "1.1.15" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== @@ -22735,7 +22084,7 @@ which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15: gopd "^1.0.1" has-tostringtag "^1.0.2" -which@^1.2.9: +which@^1.2.14, which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -22756,7 +22105,7 @@ which@^3.0.0: dependencies: isexe "^2.0.0" -why-is-node-running@^2.2.2, why-is-node-running@^2.3.0: +why-is-node-running@^2.2.2: version "2.3.0" resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== @@ -22778,41 +22127,31 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -win-ca@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/win-ca/-/win-ca-3.5.1.tgz#2ef37ac24b0a1daa2714b4c5ef258c5242429e00" - integrity sha512-RNy9gpBS6cxWHjfbqwBA7odaHyT+YQNhtdpJZwYCFoxB/Dq22oeOZ9YCXMwjhLytKpo7JJMnKdJ/ve7N12zzfQ== +winston-transport@^4.9.0: + version "4.9.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9" + integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A== dependencies: - is-electron "^2.2.0" - make-dir "^1.3.0" - node-forge "^1.2.1" - split "^1.0.1" - -winston-transport@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" - integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== - dependencies: - logform "^2.3.2" - readable-stream "^3.6.0" + logform "^2.7.0" + readable-stream "^3.6.2" triple-beam "^1.3.0" winston@^3.1.0: - version "3.8.2" - resolved "https://registry.yarnpkg.com/winston/-/winston-3.8.2.tgz#56e16b34022eb4cff2638196d9646d7430fdad50" - integrity sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew== + version "3.17.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.17.0.tgz#74b8665ce9b4ea7b29d0922cfccf852a08a11423" + integrity sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw== dependencies: - "@colors/colors" "1.5.0" + "@colors/colors" "^1.6.0" "@dabh/diagnostics" "^2.0.2" async "^3.2.3" is-stream "^2.0.0" - logform "^2.4.0" + logform "^2.7.0" one-time "^1.0.0" readable-stream "^3.4.0" safe-stable-stringify "^2.3.1" stack-trace "0.0.x" triple-beam "^1.3.0" - winston-transport "^4.5.0" + winston-transport "^4.9.0" word-wrap@~1.2.3: version "1.2.5" @@ -22831,7 +22170,7 @@ worker-farm@1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22849,15 +22188,6 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -22960,7 +22290,7 @@ xdg-basedir@^4.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== -xhr@^2.0.1, xhr@^2.4.1: +xhr@^2.4.1: version "2.6.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" integrity sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA== @@ -22985,7 +22315,7 @@ xml-parse-from-string@^1.0.0: resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" integrity sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g== -xml2js@0.1.x, xml2js@0.4.19, xml2js@0.5.0, xml2js@0.6.2, xml2js@^0.4.19, xml2js@^0.4.5: +xml2js@0.1.x, xml2js@0.6.2, xml2js@^0.5.0: version "0.6.2" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA== @@ -23068,7 +22398,7 @@ yaml@2.0.0-1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== -yaml@^1.10.2: +yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -23244,4 +22574,9 @@ zip-stream@^6.0.1: dependencies: archiver-utils "^5.0.0" compress-commons "^6.0.2" - readable-stream "^4.0.0" \ No newline at end of file + readable-stream "^4.0.0" + +zod@^3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==