Merge remote-tracking branch 'origin/master' into feat/update-automation-tests

This commit is contained in:
Peter Clement 2024-11-26 20:08:10 +00:00
commit 977c0e44f3
114 changed files with 21637 additions and 18024 deletions

View File

@ -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/**/**

View File

@ -27,9 +27,8 @@
"extends": "plugin:svelte/recommended",
"parser": "svelte-eslint-parser",
"parserOptions": {
"parser": "@babel/eslint-parser",
"parser": "@typescript-eslint/parser",
"ecmaVersion": 2019,
"sourceType": "module",
"allowImportExportEverywhere": true
}
},

View File

@ -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,16 +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: ubuntu-latest
strategy:
matrix:
datasource: [mssql, mysql, postgres, mongodb, mariadb, oracle, none]
datasource:
[mssql, mysql, postgres, mongodb, mariadb, oracle, sqs, none]
steps:
- name: Checkout repo
uses: actions/checkout@v4
@ -213,7 +218,7 @@ jobs:
fi
cd packages/server
yarn test --filter $FILTER --passWithNoTests
yarn test --filter $FILTER --verbose --reporters=default --reporters=github-actions
check-pro-submodule:
runs-on: ubuntu-latest
@ -274,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:

1
.gitignore vendored
View File

@ -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

3
.gitmodules vendored
View File

@ -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

View File

@ -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

10
.vscode/launch.json vendored
View File

@ -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",

View File

@ -12,12 +12,12 @@ metadata:
type: Opaque
data:
{{- if $existingSecret }}
internalApiKey: {{ index $existingSecret.data "internalApiKey" }}
jwtSecret: {{ index $existingSecret.data "jwtSecret" }}
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" }}
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" }}
bbEncryptionKey: {{ index $existingSecret.data "bbEncryptionKey" }}
apiEncryptionKey: {{ index $existingSecret.data "apiEncryptionKey" }}
internalApiKey: {{ index $existingSecret.data "internalApiKey" | quote }}
jwtSecret: {{ index $existingSecret.data "jwtSecret" | quote }}
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" | quote }}
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" | quote }}
bbEncryptionKey: {{ index $existingSecret.data "bbEncryptionKey" | quote }}
apiEncryptionKey: {{ index $existingSecret.data "apiEncryptionKey" | quote }}
{{- else }}
internalApiKey: {{ template "budibase.defaultsecret" .Values.globals.internalApiKey }}
jwtSecret: {{ template "budibase.defaultsecret" .Values.globals.jwtSecret }}

View File

@ -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"

View File

@ -1,12 +1,7 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.2.5",
"version": "3.2.12",
"npmClient": "yarn",
"packages": [
"packages/*",
"!packages/account-portal",
"packages/account-portal/packages/*"
],
"concurrency": 20,
"command": {
"publish": {

View File

@ -2,7 +2,6 @@
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test", "check:types"]
}

View File

@ -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": {}
}
}

@ -1 +0,0 @@
Subproject commit 9bef5d1656b4f3c991447ded6d65b0eba393a140

View File

@ -2,5 +2,3 @@
!dist/**/*
dist/tsconfig.build.tsbuildinfo
!package.json
!src/**
!tests/**

View File

@ -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",

View File

@ -190,7 +190,7 @@ export class DatabaseImpl implements Database {
}
}
private async performCall<T>(call: DBCallback<T>): Promise<any> {
private async performCall<T>(call: DBCallback<T>): Promise<T> {
const db = this.getDb()
const fnc = await call(db)
try {
@ -467,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)
}

View File

@ -27,7 +27,7 @@ export class DDInstrumentedDatabase implements Database {
exists(docId?: string): Promise<boolean> {
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<T extends Document>(id?: string | undefined): Promise<T> {
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<T extends Document>(id?: string | undefined): Promise<T | undefined> {
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<T>(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<T[]> {
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<T>(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<DocumentDestroyResponse> {
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<void> {
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<DocumentInsertResponse> {
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<DocumentBulkResponse[]> {
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<T extends Document | RowValue>(
params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> {
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<T>(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<AllDocsResponse<T>> {
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<T>(viewName, params)
span.addTags({
total_rows: resp.total_rows,
rows_length: resp.rows.length,
offset: resp.offset,
})
return resp
})
}
destroy(): Promise<void | OkResponse> {
return tracer.trace("db.destroy", span => {
span?.addTags({ db_name: this.name })
return this.db.destroy()
destroy(): Promise<OkResponse> {
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<void | OkResponse> {
return tracer.trace("db.compact", span => {
span?.addTags({ db_name: this.name })
return this.db.compact()
compact(): Promise<OkResponse> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<T[]> {
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<T>(sql, parameters)
span.addTags({ num_rows: resp.length })
return resp
})
}
sqlPurgeDocument(docIds: string[] | string): Promise<void> {
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<void> {
return tracer.trace("db.sqlDiskCleanup", span => {
span?.addTags({ db_name: this.name })
span.addTags({ db_name: this.name })
return this.db.sqlDiskCleanup()
})
}

View File

@ -19,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()
@ -231,6 +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,
BSON_BUFFER_SIZE: parseIntSafe(process.env.BSON_BUFFER_SIZE),
}
export function setEnv(newEnvVars: Partial<typeof environment>): () => void {

View File

@ -3,7 +3,7 @@
import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
import { createEventDispatcher } from "svelte"
export let type
export let type = undefined
export let disabled = false
export let size = "M"
export let cta = false
@ -16,8 +16,8 @@
export let active = false
export let tooltip = undefined
export let newStyles = true
export let id
export let ref
export let id = undefined
export let ref = undefined
export let reverse = false
const dispatch = createEventDispatcher()

View File

@ -2,13 +2,6 @@
import CoreDatePicker from "./DatePicker/DatePicker.svelte"
import Icon from "../../Icon/Icon.svelte"
export let value = null
export let disabled = false
export let readonly = false
export let error = null
export let appendTo = undefined
export let ignoreTimezones = false
let fromDate
let toDate
</script>

View File

@ -10,7 +10,7 @@
export let disabled = false
export let updateOnChange = true
export let quiet = false
export let inputRef
export let inputRef = undefined
export let helpText = null
const dispatch = createEventDispatcher()

View File

@ -17,18 +17,18 @@
export let getOptionIcon = option => option?.icon
export let getOptionColour = option => option?.colour
export let useOptionIconImage = false
export let isOptionEnabled
export let isOptionEnabled = undefined
export let quiet = false
export let autoWidth = false
export let sort = false
export let tooltip = ""
export let autocomplete = false
export let customPopoverHeight
export let align
export let customPopoverHeight = undefined
export let align = undefined
export let footer = null
export let tag = null
export let helpText = null
export let compare
export let compare = undefined
export let onOptionMouseenter = () => {}
export let onOptionMouseleave = () => {}

View File

@ -43,7 +43,7 @@
export let showHeaderBorder = true
export let placeholderText = "No rows found"
export let snippets = []
export let defaultSortColumn
export let defaultSortColumn = undefined
export let defaultSortOrder = "Ascending"
const dispatch = createEventDispatcher()

View File

@ -1,9 +1,9 @@
<script>
<script lang="ts">
import "@spectrum-css/typography/dist/index-vars.css"
// Sizes
export let size = "M"
export let textAlign
export let textAlign = undefined
export let noPadding = false
export let weight = "default" // light, heavy, default
</script>

View File

@ -6,4 +6,3 @@ release/
dist/
routify
.routify/
svelte.config.js

View File

@ -4,7 +4,8 @@
"license": "GPL-3.0",
"private": true,
"scripts": {
"build": "routify -b && vite build --emptyOutDir",
"svelte-check": "svelte-check --no-tsconfig",
"build": "yarn svelte-check && routify -b && vite build --emptyOutDir",
"start": "routify -c rollup",
"dev": "routify -c dev:vite",
"dev:vite": "vite --host 0.0.0.0",
@ -97,6 +98,7 @@
"jest": "29.7.0",
"jsdom": "^21.1.1",
"resize-observer-polyfill": "^1.5.1",
"svelte-check": "^4.1.0",
"svelte-jester": "^1.3.2",
"vite": "^4.5.0",
"vite-plugin-static-copy": "^0.17.0",

View File

@ -114,7 +114,7 @@
$: schemaFields = search.getFields(
$tables.list,
Object.values(schema || {}),
{ allowLinks: true }
{ allowLinks: false }
)
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
$: isTrigger = $memoBlock?.type === AutomationStepType.TRIGGER

View File

@ -1141,10 +1141,11 @@ export const buildFormSchema = (component, asset) => {
const fieldSetting = settings.find(
setting => setting.key === "field" && setting.type.startsWith("field/")
)
if (fieldSetting && component.field) {
if (fieldSetting) {
const type = fieldSetting.type.split("field/")[1]
if (type) {
schema[component.field] = { type }
const key = component.field || component._instanceName
if (type && key) {
schema[key] = { type }
}
}
component._children?.forEach(child => {

View File

@ -117,7 +117,4 @@
align-items: center;
margin-bottom: 12px;
}
.tabs {
}
</style>

View File

@ -50,8 +50,6 @@
border-radius: 4px;
pointer-events: none;
}
.indicator.above {
}
.indicator.below {
margin-top: 32px;
}

View File

@ -1,4 +1,4 @@
<script>
<script lang="ts">
import {
Layout,
Heading,
@ -42,23 +42,25 @@
{ column: "edit", component: EditPluginRenderer },
]
let modal
let searchTerm = ""
let filter = "all"
let modal: any
let searchTerm: any = ""
let filter: any = "all"
let filterOptions = [
{ label: "All plugins", value: "all" },
{ label: "Components", value: "component" },
]
const searchPlaceholder: any = "Search"
if (!$admin.cloud) {
filterOptions.push({ label: "Datasources", value: "datasource" })
}
$: filteredPlugins = $plugins
.filter(plugin => {
.filter((plugin: any) => {
return filter === "all" || plugin.schema.type === filter
})
.filter(plugin => {
.filter((plugin: any) => {
return (
!searchTerm ||
plugin?.name?.toLowerCase().includes(searchTerm.toLowerCase())
@ -85,8 +87,8 @@
<Button
on:click={() =>
window
.open("https://github.com/Budibase/plugins", "_blank")
.focus()}
?.open("https://github.com/Budibase/plugins", "_blank")
?.focus()}
secondary
>
GitHub repo
@ -98,12 +100,12 @@
<div class="select">
<Select
bind:value={filter}
placeholder={null}
placeholder={undefined}
options={filterOptions}
autoWidth
/>
</div>
<Search bind:value={searchTerm} placeholder="Search" />
<Search bind:value={searchTerm} placeholder={searchPlaceholder} />
</div>
{/if}
</div>

View File

@ -16,6 +16,7 @@ import {
AutomationTriggerStepId,
AutomationEventType,
AutomationStepType,
AutomationActionStepId,
} from "@budibase/types"
import { ActionStepID } from "constants/backend/automations"
import { FIELDS } from "constants/backend"
@ -466,9 +467,13 @@ const automationActions = store => ({
.getPathSteps(block.pathTo, automation)
.slice(0, -1)
// Current step will always be the last step of the path
const currentBlock = store.actions
.getPathSteps(block.pathTo, automation)
.at(-1)
// Extract all outputs from all previous steps as available bindingsx§x
let bindings = []
const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => {
if (!name) return
const runtimeBinding = determineRuntimeBinding(
@ -519,9 +524,24 @@ const automationActions = store => ({
runtimeName = `loop.${name}`
} else if (idx === 0) {
runtimeName = `trigger.${name}`
} else {
runtimeName = `steps.${pathSteps[idx]?.id}.${name}`
} else if (
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT
) {
const stepId = pathSteps[idx].id
if (!stepId) {
notifications.error("Error generating binding: Step ID not found.")
return null
}
runtimeName = `steps["${stepId}"].${name}`
} else {
const stepId = pathSteps[idx].id
if (!stepId) {
notifications.error("Error generating binding: Step ID not found.")
return null
}
runtimeName = `steps.${stepId}.${name}`
}
return runtimeName
}
@ -637,7 +657,6 @@ const automationActions = store => ({
console.error("Loop block missing.")
}
}
Object.entries(schema).forEach(([name, value]) => {
addBinding(name, value, icon, blockIdx, isLoopBlock, bindingName)
})

View File

@ -0,0 +1,7 @@
const { vitePreprocess } = require("@sveltejs/vite-plugin-svelte")
const config = {
preprocess: vitePreprocess(),
}
module.exports = config

View File

@ -3096,7 +3096,6 @@
"name": "Text Field",
"icon": "Text",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -3106,8 +3105,7 @@
{
"type": "field/string",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -3226,13 +3224,22 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "string"
}
]
}
},
"numberfield": {
"name": "Number Field",
"icon": "123",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -3242,8 +3249,7 @@
{
"type": "field/number",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -3328,13 +3334,22 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "number"
}
]
}
},
"bigintfield": {
"name": "BigInt Field",
"icon": "TagBold",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -3344,8 +3359,7 @@
{
"type": "field/bigint",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -3414,13 +3428,22 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "number"
}
]
}
},
"passwordfield": {
"name": "Password Field",
"icon": "LockClosed",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -3430,8 +3453,7 @@
{
"type": "field/string",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -3500,13 +3522,22 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "string"
}
]
}
},
"optionsfield": {
"name": "Options Picker",
"icon": "Menu",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -3516,8 +3547,7 @@
{
"type": "field/options",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -3714,13 +3744,22 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "string"
}
]
}
},
"multifieldselect": {
"name": "Multi-select Picker",
"icon": "ViewList",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -3730,8 +3769,7 @@
{
"type": "field/array",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -3922,13 +3960,22 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "array"
}
]
}
},
"booleanfield": {
"name": "Checkbox",
"icon": "SelectBox",
"editable": true,
"requiredAncestors": ["form"],
"size": {
"width": 400,
"height": 60
@ -3937,8 +3984,7 @@
{
"type": "field/boolean",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -4047,13 +4093,22 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "boolean"
}
]
}
},
"longformfield": {
"name": "Long Form Field",
"icon": "TextAlignLeft",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -4063,8 +4118,7 @@
{
"type": "field/longform",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -4171,13 +4225,22 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "string"
}
]
}
},
"datetimefield": {
"name": "Date Picker",
"icon": "Date",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -4187,8 +4250,7 @@
{
"type": "field/datetime",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -4291,7 +4353,17 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "datetime"
}
]
}
},
"codescanner": {
"name": "Barcode/QR Scanner",
@ -4305,8 +4377,7 @@
{
"type": "field/barcodeqr",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -4451,7 +4522,17 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "string"
}
]
}
},
"signaturesinglefield": {
"name": "Signature",
@ -4924,7 +5005,6 @@
"icon": "Brackets",
"styles": ["size"],
"editable": true,
"requiredAncestors": ["form"],
"size": {
"width": 400,
"height": 100
@ -4933,8 +5013,7 @@
{
"type": "field/json",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -5014,7 +5093,17 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "string"
}
]
}
},
"s3upload": {
"name": "S3 File Upload",
@ -5029,8 +5118,7 @@
{
"type": "field/s3",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -5075,7 +5163,17 @@
"label": "Validation",
"key": "validation"
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "array"
}
]
}
},
"dataprovider": {
"name": "Data Provider",
@ -7643,7 +7741,6 @@
"name": "User List Field",
"icon": "UserGroup",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -7653,8 +7750,7 @@
{
"type": "field/bb_reference",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -7744,14 +7840,23 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "array"
}
]
}
},
"bbreferencesinglefield": {
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",
"name": "User Field",
"icon": "User",
"styles": ["size"],
"requiredAncestors": ["form"],
"editable": true,
"size": {
"width": 400,
@ -7761,8 +7866,7 @@
{
"type": "field/bb_reference_single",
"label": "Field",
"key": "field",
"required": true
"key": "field"
},
{
"type": "text",
@ -7852,6 +7956,16 @@
}
]
}
],
"context": {
"type": "static",
"values": [
{
"label": "Value",
"key": "value",
"type": "string"
}
]
}
}
}

View File

@ -18,8 +18,6 @@
export let palette
export let c1, c2, c3, c4, c5
// Area specific props
export let area
export let stacked
export let gradient

View File

@ -1,7 +1,10 @@
<script>
import Placeholder from "../Placeholder.svelte"
import { getContext, onDestroy } from "svelte"
import { writable } from "svelte/store"
import { Icon } from "@budibase/bbui"
import { memo } from "@budibase/frontend-core"
import Placeholder from "../Placeholder.svelte"
import InnerForm from "./InnerForm.svelte"
export let label
export let field
@ -20,26 +23,39 @@
const formContext = getContext("form")
const formStepContext = getContext("form-step")
const fieldGroupContext = getContext("field-group")
const { styleable, builderStore } = getContext("sdk")
const { styleable, builderStore, Provider } = getContext("sdk")
const component = getContext("component")
// Register field with form
const formApi = formContext?.formApi
const labelPos = fieldGroupContext?.labelPosition || "above"
let formField
let touched = false
let labelNode
$: formStep = formStepContext ? $formStepContext || 1 : 1
$: formField = formApi?.registerField(
field,
// Memoize values required to register the field to avoid loops
const formStep = formStepContext || writable(1)
const fieldInfo = memo({
field: field || $component.name,
type,
defaultValue,
disabled,
readonly,
validation,
formStep
)
formStep: $formStep || 1,
})
$: fieldInfo.set({
field: field || $component.name,
type,
defaultValue,
disabled,
readonly,
validation,
formStep: $formStep || 1,
})
$: registerField($fieldInfo)
$: schemaType =
fieldSchema?.type !== "formula" && fieldSchema?.type !== "bigint"
? fieldSchema?.type
@ -58,6 +74,18 @@
// Determine label class from position
$: labelClass = labelPos === "above" ? "" : `spectrum-FieldLabel--${labelPos}`
const registerField = info => {
formField = formApi?.registerField(
info.field,
info.type,
info.defaultValue,
info.disabled,
info.readonly,
info.validation,
info.formStep
)
}
const updateLabel = e => {
if (touched) {
builderStore.actions.updateProp("label", e.target.textContent)
@ -71,14 +99,27 @@
})
</script>
<div
<Provider data={{ value: fieldState?.value }}>
{#if !formContext}
<InnerForm
{disabled}
{readonly}
currentStep={writable(1)}
provideContext={false}
>
<svelte:self {...$$props} bind:fieldState bind:fieldApi bind:fieldSchema>
<slot />
</svelte:self>
</InnerForm>
{:else}
<div
class="spectrum-Form-item"
class:span-2={span === 2}
class:span-3={span === 3}
class:span-6={span === 6 || !span}
use:styleable={$component.styles}
class:above={labelPos === "above"}
>
>
{#key $component.editing}
<label
bind:this={labelNode}
@ -94,9 +135,7 @@
</label>
{/key}
<div class="spectrum-Form-itemField">
{#if !formContext}
<Placeholder text="Form components need to be wrapped in a form" />
{:else if !fieldState}
{#if !fieldState}
<Placeholder />
{:else if schemaType && schemaType !== type && !["options", "longform"].includes(type)}
<Placeholder
@ -116,7 +155,9 @@
{/if}
{/if}
</div>
</div>
</div>
{/if}
</Provider>
<style>
:global(.form-block .spectrum-Form-item.span-2) {

View File

@ -5,7 +5,6 @@
import { writable } from "svelte/store"
export let dataSource
export let theme
export let size
export let disabled = false
export let readonly = false
@ -113,11 +112,9 @@
{#key resetKey}
<InnerForm
{dataSource}
{theme}
{size}
{disabled}
{readonly}
{actionType}
{schema}
{definition}
{initialValues}

View File

@ -14,6 +14,10 @@
export let disableSchemaValidation = false
export let editAutoColumns = false
// For internal use only, to disable context when being used with standalone
// fields
export let provideContext = true
// We export this store so that when we remount the inner form we can still
// persist what step we're on
export let currentStep
@ -442,8 +446,14 @@
]
</script>
<Provider {actions} data={dataContext}>
{#if provideContext}
<Provider {actions} data={dataContext}>
<div use:styleable={$component.styles} class={size}>
<slot />
</div>
</Provider>
</Provider>
{:else}
<div use:styleable={$component.styles} class={size}>
<slot />
</div>
{/if}

View File

@ -1,5 +1,7 @@
import { makePropSafe as safe } from "@budibase/string-templates"
import { API } from "../api/index.js"
import { UILogicalOperator } from "@budibase/types"
import { OnEmptyFilter } from "@budibase/frontend-core/src/constants.js"
// Map of data types to component types for search fields inside blocks
const schemaComponentMap = {
@ -60,7 +62,11 @@ export const enrichSearchColumns = async (searchColumns, schema) => {
* @param formId the ID of the form containing the search fields
*/
export const enrichFilter = (filter, columns, formId) => {
let enrichedFilter = [...(filter || [])]
if (!columns?.length) {
return filter
}
let newFilters = []
columns?.forEach(column => {
const safePath = column.name.split(".").map(safe).join(".")
const stringType = column.type === "string" || column.type === "formula"
@ -69,7 +75,7 @@ export const enrichFilter = (filter, columns, formId) => {
// For dates, use a range of the entire day selected
if (dateType) {
enrichedFilter.push({
newFilters.push({
field: column.name,
type: column.type,
operator: "rangeLow",
@ -79,7 +85,7 @@ export const enrichFilter = (filter, columns, formId) => {
const format = "YYYY-MM-DDTHH:mm:ss.SSSZ"
let hbs = `{{ date (add (date ${binding} "x") 86399999) "${format}" }}`
hbs = `{{#if ${binding} }}${hbs}{{/if}}`
enrichedFilter.push({
newFilters.push({
field: column.name,
type: column.type,
operator: "rangeHigh",
@ -90,7 +96,7 @@ export const enrichFilter = (filter, columns, formId) => {
// For other fields, do an exact match
else {
enrichedFilter.push({
newFilters.push({
field: column.name,
type: column.type,
operator: stringType ? "string" : "equal",
@ -99,5 +105,16 @@ export const enrichFilter = (filter, columns, formId) => {
})
}
})
return enrichedFilter
return {
logicalOperator: UILogicalOperator.ALL,
onEmptyFilter: OnEmptyFilter.RETURN_ALL,
groups: [
...(filter?.groups || []),
{
logicalOperator: UILogicalOperator.ALL,
filters: newFilters,
},
],
}
}

@ -1 +1 @@
Subproject commit bfeece324a03a3a5f25137bf3f8c66d5ed6103d8
Subproject commit 25dd40ee12b048307b558ebcedb36548d6e042cd

View File

@ -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",

File diff suppressed because it is too large Load Diff

View File

@ -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: []

View File

@ -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",

View File

@ -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(),
}

View File

@ -1,10 +1,7 @@
import { object } from "./utils"
import Resource from "./utils/Resource"
export default new Resource().setSchemas({
rowSearch: object(
{
query: {
export const searchSchema = {
type: "object",
properties: {
allOr: {
@ -93,7 +90,12 @@ export default new Resource().setSchemas({
},
},
},
},
}
export default new Resource().setSchemas({
rowSearch: object(
{
query: searchSchema,
paginate: {
type: "boolean",
description: "Enables pagination, by default this is disabled.",

View File

@ -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,
},
}),
})

View File

@ -1,6 +1,7 @@
import { Application } from "./types"
import { RequiredKeys } from "@budibase/types"
function application(body: any): Application {
function application(body: any): RequiredKeys<Application> {
let app = body?.application ? body.application : body
return {
_id: app.appId,

View File

@ -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,
}

View File

@ -1,6 +1,7 @@
import { Query, ExecuteQuery } from "./types"
import { RequiredKeys } from "@budibase/types"
function query(body: any): Query {
function query(body: any): RequiredKeys<Query> {
return {
_id: body._id,
datasourceId: body.datasourceId,

View File

@ -1,6 +1,7 @@
import { Row, RowSearch } from "./types"
import { RequiredKeys } from "@budibase/types"
function row(body: any): Row {
function row(body: any): RequiredKeys<Row> {
delete body._rev
// have to input everything, since structure unknown
return {

View File

@ -1,6 +1,7 @@
import { Table } from "./types"
import { RequiredKeys } from "@budibase/types"
function table(body: any): Table {
function table(body: any): RequiredKeys<Table> {
return {
_id: body._id,
name: body.name,

View File

@ -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"]

View File

@ -1,6 +1,7 @@
import { User } from "./types"
import { RequiredKeys } from "@budibase/types"
function user(body: any): User {
function user(body: any): RequiredKeys<User> {
return {
_id: body._id,
email: body.email,

View File

@ -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<View> {
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,
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -4,7 +4,7 @@ import { URL } from "url"
const curlconverter = require("curlconverter")
const parseCurl = (data: string): any => {
const parseCurl = (data: string): Promise<any> => {
const curlJson = curlconverter.toJsonString(data)
return JSON.parse(curlJson)
}
@ -53,8 +53,7 @@ export class Curl extends ImportSource {
isSupported = async (data: string): Promise<boolean> => {
try {
const curl = parseCurl(data)
this.curl = curl
this.curl = parseCurl(data)
} catch (err) {
return false
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -12,6 +12,7 @@ import {
RelationSchemaField,
ViewFieldMetadata,
CalculationType,
ViewFetchResponseEnriched,
CountDistinctCalculationFieldMetadata,
CountCalculationFieldMetadata,
} from "@budibase/types"
@ -125,6 +126,12 @@ export async function get(ctx: Ctx<void, ViewResponseEnriched>) {
}
}
export async function fetch(ctx: Ctx<void, ViewFetchResponseEnriched>) {
ctx.body = {
data: await sdk.views.getAllEnriched(),
}
}
export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
const view = ctx.request.body
const { tableId } = view

View File

@ -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

View File

@ -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
}

View File

@ -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 }

View File

@ -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<T> = { data: T }
export interface PublicAPIExpectations {
status?: number
body?: Record<string, any>
headers?: Record<string, string>
}
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<Response<Table>> {
return this.request.send("post", "/tables", table, expectations)
}
async search(
name: string,
expectations?: PublicAPIExpectations
): Promise<Response<Table[]>> {
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<Response<Row>> {
return this.request.send(
"post",
`/tables/${tableId}/rows`,
row,
expectations
)
}
async search(
tableId: string,
query: SearchFilters,
expectations?: PublicAPIExpectations
): Promise<{ data: Row[] }> {
): Promise<Response<Row[]>> {
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<Response<Row[]>> {
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<PublicAPIView, "id" | "version">,
expectations?: PublicAPIExpectations
): Promise<Response<PublicAPIView>> {
return this.request.send("post", "/views", view, expectations)
}
async update(
viewId: string,
view: Omit<PublicAPIView, "id" | "version">,
expectations?: PublicAPIExpectations
): Promise<Response<PublicAPIView>> {
return this.request.send("put", `/views/${viewId}`, view, expectations)
}
async destroy(
viewId: string,
expectations?: PublicAPIExpectations
): Promise<void> {
return this.request.send(
"delete",
`/views/${viewId}`,
undefined,
expectations
)
}
async find(
viewId: string,
expectations?: PublicAPIExpectations
): Promise<Response<PublicAPIView>> {
return this.request.send("get", `/views/${viewId}`, undefined, expectations)
}
async search(
viewName: string,
expectations?: PublicAPIExpectations
): Promise<Response<PublicAPIView[]>> {
return this.request.send(
"post",
"/views/search",
{
name: viewName,
},
expectations
)
}
}

View File

@ -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": "*",
},
})
})
})

View File

@ -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)
})
})

View File

@ -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 }

View File

@ -164,9 +164,12 @@ describe("/datasources", () => {
})
})
datasourceDescribe(
{ name: "%s", exclude: [DatabaseName.MONGODB, DatabaseName.SQS] },
({ config, 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
@ -492,5 +495,5 @@ datasourceDescribe(
)
})
})
}
)
})
}

View File

@ -14,8 +14,13 @@ import { events } from "@budibase/backend-core"
import { Knex } from "knex"
import { generator } from "@budibase/backend-core/tests"
datasourceDescribe(
{ name: "queries (%s)", exclude: [DatabaseName.MONGODB, DatabaseName.SQS] },
const descriptions = datasourceDescribe({
exclude: [DatabaseName.MONGODB, DatabaseName.SQS],
})
if (descriptions.length) {
describe.each(descriptions)(
"queries ($dbName)",
({ config, dsProvider, isOracle, isMSSQL, isPostgres }) => {
let rawDatasource: Datasource
let datasource: Datasource
@ -945,4 +950,5 @@ datasourceDescribe(
})
})
}
)
)
}

View File

@ -9,8 +9,11 @@ import { generator } from "@budibase/backend-core/tests"
const expectValidId = expect.stringMatching(/^\w{24}$/)
const expectValidBsonObjectId = expect.any(BSON.ObjectId)
datasourceDescribe(
{ name: "/queries", only: [DatabaseName.MONGODB] },
const descriptions = datasourceDescribe({ only: [DatabaseName.MONGODB] })
if (descriptions.length) {
describe.each(descriptions)(
"/queries ($dbName)",
({ config, dsProvider }) => {
let collection: string
let datasource: Datasource
@ -714,4 +717,5 @@ datasourceDescribe(
})
})
}
)
)
}

View File

@ -85,8 +85,11 @@ function encodeJS(binding: string) {
return `{{ js "${Buffer.from(binding).toString("base64")}"}}`
}
datasourceDescribe(
{ name: "/rows (%s)", exclude: [DatabaseName.MONGODB] },
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
if (descriptions.length) {
describe.each(descriptions)(
"/rows ($dbName)",
({ config, dsProvider, isInternal, isMSSQL, isOracle }) => {
let table: Table
let datasource: Datasource | undefined
@ -338,7 +341,9 @@ datasourceDescribe(
await new Promise(r => setTimeout(r, Math.random() * 50))
}
}
throw new Error(`Failed to create row after ${attempts} attempts`)
throw new Error(
`Failed to create row after ${attempts} attempts`
)
})
)
@ -1495,7 +1500,9 @@ datasourceDescribe(
it("should return no errors on valid row", async () => {
const rowUsage = await getRowUsage()
const res = await config.api.row.validate(table._id!, { name: "ivan" })
const res = await config.api.row.validate(table._id!, {
name: "ivan",
})
expect(res.valid).toBe(true)
expect(Object.keys(res.errors)).toEqual([])
@ -2244,7 +2251,10 @@ datasourceDescribe(
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 }) }))
.map(number => ({
number,
string: generator.word({ length: 30 }),
}))
const rows = await Promise.all(
toCreate.map(d => config.api.row.save(table._id!, d))
@ -3019,7 +3029,10 @@ datasourceDescribe(
},
],
["from original saved row", (row: Row) => row],
["from updated row", (row: Row) => config.api.row.save(viewId, row)],
[
"from updated row",
(row: Row) => config.api.row.save(viewId, row),
],
]
it.each(testScenarios)(
@ -3243,7 +3256,10 @@ datasourceDescribe(
async function updateFormulaColumn(
formula: string,
opts?: { responseType?: FormulaResponseType; formulaType?: FormulaType }
opts?: {
responseType?: FormulaResponseType
formulaType?: FormulaType
}
) {
table = await config.api.table.save({
...table,
@ -3480,6 +3496,5 @@ datasourceDescribe(
})
})
}
)
// todo: remove me
)
}

View File

@ -977,8 +977,13 @@ describe("/rowsActions", () => {
})
})
datasourceDescribe(
{ name: "row actions (%s)", only: [DatabaseName.SQS, DatabaseName.POSTGRES] },
const descriptions = datasourceDescribe({
only: [DatabaseName.SQS, DatabaseName.POSTGRES],
})
if (descriptions.length) {
describe.each(descriptions)(
"row actions ($dbName)",
({ config, dsProvider, isInternal }) => {
let datasource: Datasource | undefined
@ -1036,4 +1041,5 @@ datasourceDescribe(
expect(await getRowActionsFromDb(tableId)).toBeUndefined()
})
}
)
)
}

View File

@ -59,11 +59,11 @@ jest.mock("@budibase/pro", () => ({
},
}))
datasourceDescribe(
{
name: "search (%s)",
exclude: [DatabaseName.MONGODB],
},
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
@ -198,7 +198,9 @@ datasourceDescribe(
])
}
describe.each(tableOrView)("from %s", (sourceType, createTableOrView) => {
describe.each(tableOrView)(
"from %s",
(sourceType, createTableOrView) => {
const isView = sourceType === "view"
class SearchAssertion {
@ -263,12 +265,16 @@ datasourceDescribe(
expectedRow: T,
foundRows: T[]
): NonNullable<T> {
const row = foundRows.find(row => this.isMatch(expectedRow, row))
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))
const searchedObjects = foundRows.map(row =>
_.pick(row, fields)
)
throw new Error(
`Failed to find row:\n\n${JSON.stringify(
expectedRow,
@ -316,7 +322,9 @@ datasourceDescribe(
expect([...foundRows]).toEqual(
expect.arrayContaining(
expectedRows.map((expectedRow: any) =>
expect.objectContaining(this.popRow(expectedRow, foundRows))
expect.objectContaining(
this.popRow(expectedRow, foundRows)
)
)
)
)
@ -344,7 +352,9 @@ datasourceDescribe(
}
// Asserts that the query doesn't return a property, e.g. pagination parameters.
async toNotHaveProperty(properties: (keyof SearchResponse<Row>)[]) {
async toNotHaveProperty(
properties: (keyof SearchResponse<Row>)[]
) {
const response = await this.performSearch()
const cloned = cloneDeep(response)
for (let property of properties) {
@ -366,7 +376,9 @@ datasourceDescribe(
expect([...foundRows]).toEqual(
expect.arrayContaining(
expectedRows.map((expectedRow: any) =>
expect.objectContaining(this.popRow(expectedRow, foundRows))
expect.objectContaining(
this.popRow(expectedRow, foundRows)
)
)
)
)
@ -403,15 +415,15 @@ datasourceDescribe(
describe("equal", () => {
it("successfully finds true row", async () => {
await expectQuery({ equal: { isTrue: true } }).toMatchExactly([
{ isTrue: true },
])
await expectQuery({ equal: { isTrue: true } }).toMatchExactly(
[{ isTrue: true }]
)
})
it("successfully finds false row", async () => {
await expectQuery({ equal: { isTrue: false } }).toMatchExactly([
{ isTrue: false },
])
await expectQuery({
equal: { isTrue: false },
}).toMatchExactly([{ isTrue: false }])
})
})
@ -431,9 +443,9 @@ datasourceDescribe(
describe("oneOf", () => {
it("successfully finds true row", async () => {
await expectQuery({ oneOf: { isTrue: [true] } }).toContainExactly(
[{ isTrue: true }]
)
await expectQuery({
oneOf: { isTrue: [true] },
}).toContainExactly([{ isTrue: true }])
})
it("successfully finds false row", async () => {
@ -485,7 +497,10 @@ datasourceDescribe(
name: currentUser.firstName,
appointment: future.toISOString(),
},
{ name: "serverDate", appointment: serverTime.toISOString() },
{
name: "serverDate",
appointment: serverTime.toISOString(),
},
{
name: "single user, session user",
single_user: currentUser,
@ -540,7 +555,10 @@ datasourceDescribe(
tableOrViewId = await createTableOrView({
name: { name: "name", type: FieldType.STRING },
appointment: { name: "appointment", type: FieldType.DATETIME },
appointment: {
name: "appointment",
type: FieldType.DATETIME,
},
single_user: {
name: "single_user",
type: FieldType.BB_REFERENCE_SINGLE,
@ -586,7 +604,9 @@ datasourceDescribe(
it("should return all rows matching the session user firstname when logical operator used", async () => {
await expectQuery({
$and: {
conditions: [{ equal: { name: "{{ [user].firstName }}" } }],
conditions: [
{ equal: { name: "{{ [user].firstName }}" } },
],
},
}).toContainExactly([
{
@ -610,7 +630,10 @@ datasourceDescribe(
name: config.getUser().firstName,
appointment: future.toISOString(),
},
{ name: "serverDate", appointment: serverTime.toISOString() },
{
name: "serverDate",
appointment: serverTime.toISOString(),
},
])
})
})
@ -626,7 +649,10 @@ datasourceDescribe(
}).toContainExactly([
{ name: "foo", appointment: "1982-01-05T00:00:00.000Z" },
{ name: "bar", appointment: "1995-05-06T00:00:00.000Z" },
{ name: "serverDate", appointment: serverTime.toISOString() },
{
name: "serverDate",
appointment: serverTime.toISOString(),
},
])
})
@ -736,7 +762,9 @@ datasourceDescribe(
it("should not match the session user id in a deprecated multi user field", async () => {
await expectQuery({
notContains: { deprecated_multi_user: ["{{ [user]._id }}"] },
notContains: {
deprecated_multi_user: ["{{ [user]._id }}"],
},
notEmpty: { deprecated_multi_user: true },
}).toContainExactly([
{
@ -870,9 +898,9 @@ datasourceDescribe(
describe("equal", () => {
it("successfully finds a row", async () => {
await expectQuery({ equal: { name: "foo" } }).toContainExactly([
{ name: "foo" },
])
await expectQuery({
equal: { name: "foo" },
}).toContainExactly([{ name: "foo" }])
})
it("fails to find nonexistent row", async () => {
@ -897,27 +925,29 @@ datasourceDescribe(
describe("notEqual", () => {
it("successfully finds a row", async () => {
await expectQuery({ notEqual: { name: "foo" } }).toContainExactly(
[{ name: "bar" }]
)
await expectQuery({
notEqual: { name: "foo" },
}).toContainExactly([{ name: "bar" }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({ notEqual: { name: "bar" } }).toContainExactly(
[{ name: "foo" }]
)
await expectQuery({
notEqual: { name: "bar" },
}).toContainExactly([{ name: "foo" }])
})
})
describe("oneOf", () => {
it("successfully finds a row", async () => {
await expectQuery({ oneOf: { name: ["foo"] } }).toContainExactly([
{ name: "foo" },
])
await expectQuery({
oneOf: { name: ["foo"] },
}).toContainExactly([{ name: "foo" }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({ oneOf: { name: ["none"] } }).toFindNothing()
await expectQuery({
oneOf: { name: ["none"] },
}).toFindNothing()
})
it("can have multiple values for same column", async () => {
@ -965,9 +995,9 @@ datasourceDescribe(
describe("fuzzy", () => {
it("successfully finds a row", async () => {
await expectQuery({ fuzzy: { name: "oo" } }).toContainExactly([
{ name: "foo" },
])
await expectQuery({ fuzzy: { name: "oo" } }).toContainExactly(
[{ name: "foo" }]
)
})
it("fails to find nonexistent row", async () => {
@ -977,19 +1007,21 @@ datasourceDescribe(
describe("string", () => {
it("successfully finds a row", async () => {
await expectQuery({ string: { name: "fo" } }).toContainExactly([
{ name: "foo" },
])
await expectQuery({
string: { name: "fo" },
}).toContainExactly([{ name: "foo" }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({ string: { name: "none" } }).toFindNothing()
await expectQuery({
string: { name: "none" },
}).toFindNothing()
})
it("is case-insensitive", async () => {
await expectQuery({ string: { name: "FO" } }).toContainExactly([
{ name: "foo" },
])
await expectQuery({
string: { name: "FO" },
}).toContainExactly([{ name: "foo" }])
})
})
@ -1048,10 +1080,9 @@ datasourceDescribe(
describe("notEmpty", () => {
it("finds all non-empty rows", async () => {
await expectQuery({ notEmpty: { name: null } }).toContainExactly([
{ name: "foo" },
{ name: "bar" },
])
await expectQuery({
notEmpty: { name: null },
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
})
it("should not be affected by when filter empty behaviour", async () => {
@ -1167,9 +1198,9 @@ datasourceDescribe(
})
it("fails to find nonexistent row", async () => {
await expectQuery({ notEqual: { age: 10 } }).toContainExactly([
{ age: 1 },
])
await expectQuery({ notEqual: { age: 10 } }).toContainExactly(
[{ age: 1 }]
)
})
})
@ -1317,9 +1348,9 @@ datasourceDescribe(
describe("equal", () => {
it("successfully finds a row", async () => {
await expectQuery({ equal: { dob: JAN_1ST } }).toContainExactly([
{ dob: JAN_1ST },
])
await expectQuery({
equal: { dob: JAN_1ST },
}).toContainExactly([{ dob: JAN_1ST }])
})
it("fails to find nonexistent row", async () => {
@ -1343,13 +1374,15 @@ datasourceDescribe(
describe("oneOf", () => {
it("successfully finds a row", async () => {
await expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContainExactly(
[{ dob: JAN_1ST }]
)
await expectQuery({
oneOf: { dob: [JAN_1ST] },
}).toContainExactly([{ dob: JAN_1ST }])
})
it("fails to find nonexistent row", async () => {
await expectQuery({ oneOf: { dob: [JAN_2ND] } }).toFindNothing()
await expectQuery({
oneOf: { dob: [JAN_2ND] },
}).toFindNothing()
})
})
@ -1381,7 +1414,10 @@ datasourceDescribe(
it("greater than equal to", async () => {
await expectQuery({
range: {
dob: { low: JAN_10TH, high: MAX_VALID_DATE.toISOString() },
dob: {
low: JAN_10TH,
high: MAX_VALID_DATE.toISOString(),
},
},
}).toContainExactly([{ dob: JAN_10TH }])
})
@ -1484,9 +1520,9 @@ datasourceDescribe(
describe("equal", () => {
it("successfully finds a row", async () => {
await expectQuery({ equal: { time: T_1000 } }).toContainExactly(
[{ time: "10:00:00" }]
)
await expectQuery({
equal: { time: T_1000 },
}).toContainExactly([{ time: "10:00:00" }])
})
it("fails to find nonexistent row", async () => {
@ -1692,7 +1728,9 @@ datasourceDescribe(
describe("oneOf", () => {
it("successfully finds a row", async () => {
await expectQuery({
oneOf: { ai: ["Mock LLM Response", "Other LLM Response"] },
oneOf: {
ai: ["Mock LLM Response", "Other LLM Response"],
},
}).toContainExactly([
{ product: "Big Mac" },
{ product: "McCrispy" },
@ -1745,9 +1783,12 @@ datasourceDescribe(
})
it("finds all with empty list", async () => {
await expectQuery({ contains: { numbers: [] } }).toContainExactly(
[{ numbers: ["one", "two"] }, { numbers: ["three"] }]
)
await expectQuery({
contains: { numbers: [] },
}).toContainExactly([
{ numbers: ["one", "two"] },
{ numbers: ["three"] },
])
})
})
@ -1817,14 +1858,18 @@ datasourceDescribe(
tableOrViewId = await createTableOrView({
num: { name: "num", type: FieldType.BIGINT },
})
await createRows([{ num: SMALL }, { num: MEDIUM }, { num: BIG }])
await createRows([
{ num: SMALL },
{ num: MEDIUM },
{ num: BIG },
])
})
describe("equal", () => {
it("successfully finds a row", async () => {
await expectQuery({ equal: { num: SMALL } }).toContainExactly([
{ num: SMALL },
])
await expectQuery({ equal: { num: SMALL } }).toContainExactly(
[{ num: SMALL }]
)
})
it("successfully finds a big value", async () => {
@ -1840,26 +1885,23 @@ datasourceDescribe(
describe("notEqual", () => {
it("successfully finds a row", async () => {
await expectQuery({ notEqual: { num: SMALL } }).toContainExactly([
{ num: MEDIUM },
{ num: BIG },
])
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 },
])
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 },
])
await expectQuery({
oneOf: { num: [SMALL] },
}).toContainExactly([{ num: SMALL }])
})
it("successfully finds all rows", async () => {
@ -1944,7 +1986,9 @@ datasourceDescribe(
describe("not equal", () => {
it("successfully finds a row", async () => {
await expectQuery({ notEqual: { auto: 1 } }).toContainExactly([
await expectQuery({
notEqual: { auto: 1 },
}).toContainExactly([
{ auto: 2 },
{ auto: 3 },
{ auto: 4 },
@ -1958,7 +2002,9 @@ datasourceDescribe(
})
it("fails to find nonexistent row", async () => {
await expectQuery({ notEqual: { auto: 0 } }).toContainExactly([
await expectQuery({
notEqual: { auto: 0 },
}).toContainExactly([
{ auto: 1 },
{ auto: 2 },
{ auto: 3 },
@ -1975,9 +2021,9 @@ datasourceDescribe(
describe("oneOf", () => {
it("successfully finds a row", async () => {
await expectQuery({ oneOf: { auto: [1] } }).toContainExactly([
{ auto: 1 },
])
await expectQuery({
oneOf: { auto: [1] },
}).toContainExactly([{ auto: 1 }])
})
it("fails to find nonexistent row", async () => {
@ -2081,13 +2127,16 @@ datasourceDescribe(
hasNextPage: boolean | undefined = true,
rowCount: number = 0
do {
const response = await config.api.row.search(tableOrViewId, {
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)
@ -2105,13 +2154,16 @@ datasourceDescribe(
// eslint-disable-next-line no-constant-condition
while (true) {
const response = await config.api.row.search(tableOrViewId, {
const response = await config.api.row.search(
tableOrViewId,
{
tableId: tableOrViewId,
limit: 3,
query: {},
bookmark,
paginate: true,
})
}
)
rows.push(...response.rows)
@ -2144,7 +2196,9 @@ datasourceDescribe(
})
it("fails to find nonexistent row", async () => {
await expectQuery({ equal: { "1:1:name": "none" } }).toFindNothing()
await expectQuery({
equal: { "1:1:name": "none" },
}).toFindNothing()
})
})
@ -2221,7 +2275,11 @@ datasourceDescribe(
},
})
await createRows([{ user: user1 }, { user: user2 }, { user: null }])
await createRows([
{ user: user1 },
{ user: user2 },
{ user: null },
])
})
describe("equal", () => {
@ -2232,7 +2290,9 @@ datasourceDescribe(
})
it("fails to find nonexistent row", async () => {
await expectQuery({ equal: { user: "us_none" } }).toFindNothing()
await expectQuery({
equal: { user: "us_none" },
}).toFindNothing()
})
})
@ -2270,15 +2330,17 @@ datasourceDescribe(
describe("empty", () => {
it("finds empty rows", async () => {
await expectQuery({ empty: { user: null } }).toContainExactly([
{},
])
await expectQuery({ empty: { user: null } }).toContainExactly(
[{}]
)
})
})
describe("notEmpty", () => {
it("finds non-empty rows", async () => {
await expectQuery({ notEmpty: { user: null } }).toContainExactly([
await expectQuery({
notEmpty: { user: null },
}).toContainExactly([
{ user: { _id: user1._id } },
{ user: { _id: user2._id } },
])
@ -2385,7 +2447,9 @@ datasourceDescribe(
await expectQuery({
equal: { number: 1 },
contains: { users: [user1._id] },
}).toContainExactly([{ users: [{ _id: user1._id }], number: 1 }])
}).toContainExactly([
{ users: [{ _id: user1._id }], number: 1 },
])
})
it("fails to find nonexistent row", async () => {
@ -2408,15 +2472,18 @@ datasourceDescribe(
let productCategoryTable: Table, productCatRows: Row[]
beforeAll(async () => {
const { relatedTable, tableId } = await basicRelationshipTables(
relationshipType
)
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" }),
config.api.row.save(productCategoryTable._id!, {
name: "foo",
}),
config.api.row.save(productCategoryTable._id!, {
name: "bar",
}),
])
await Promise.all([
@ -2439,7 +2506,10 @@ datasourceDescribe(
await expectQuery({
equal: { ["productCat.name"]: "foo" },
}).toContainExactly([
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
{
name: "foo",
productCat: [{ _id: productCatRows[0]._id }],
},
])
})
@ -2447,7 +2517,10 @@ datasourceDescribe(
await expectQuery({
equal: { [`${productCategoryTable.name}.name`]: "foo" },
}).toContainExactly([
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
{
name: "foo",
productCat: [{ _id: productCatRows[0]._id }],
},
])
})
@ -2458,7 +2531,10 @@ datasourceDescribe(
})
describe("logical filters", () => {
const logicalOperators = [LogicalOperator.AND, LogicalOperator.OR]
const logicalOperators = [
LogicalOperator.AND,
LogicalOperator.OR,
]
describe("$and", () => {
it("should allow single conditions", async () => {
@ -2699,9 +2775,8 @@ datasourceDescribe(
RelationshipType.MANY_TO_MANY,
])("big relations (%s)", relationshipType => {
beforeAll(async () => {
const { relatedTable, tableId } = await basicRelationshipTables(
relationshipType
)
const { relatedTable, tableId } =
await basicRelationshipTables(relationshipType)
tableOrViewId = tableId
const mainRow = await config.api.row.save(tableOrViewId, {
name: "foo",
@ -2715,12 +2790,15 @@ datasourceDescribe(
})
it("can only pull 10 related rows", async () => {
await withCoreEnv({ SQL_MAX_RELATED_ROWS: "10" }, 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 () => {
@ -2935,9 +3013,11 @@ datasourceDescribe(
})
})
describe.each(["data_name_test", "name_data_test", "name_test_data_"])(
"special (%s) case",
column => {
describe.each([
"data_name_test",
"name_data_test",
"name_test_data_",
])("special (%s) case", column => {
beforeAll(async () => {
tableOrViewId = await createTableOrView({
[column]: {
@ -2957,8 +3037,7 @@ datasourceDescribe(
},
}).toContainExactly([{ [column]: "a" }])
})
}
)
})
isInternal &&
describe("sample data", () => {
@ -2980,10 +3059,22 @@ datasourceDescribe(
})
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" },
{
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"
@ -3260,13 +3351,17 @@ datasourceDescribe(
},
})
const toRelateTable = await config.api.table.get(toRelateTableId)
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" }),
config.api.row.save(toRelateTable._id!, {
name: "related",
}),
])
await config.api.row.save(tableOrViewId, {
name: "test",
@ -3645,7 +3740,9 @@ datasourceDescribe(
"'; SHUTDOWN --",
]
describe.each(badStrings)("bad string: %s", badStringTemplate => {
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.
@ -3665,12 +3762,17 @@ datasourceDescribe(
...table,
schema: {
...table.schema,
[badString]: { name: badString, type: FieldType.STRING },
[badString]: {
name: badString,
type: FieldType.STRING,
},
},
})
if (docIds.isViewId(tableOrViewId)) {
const view = await config.api.viewV2.get(tableOrViewId)
const view = await config.api.viewV2.get(
tableOrViewId
)
await config.api.viewV2.update({
...view,
schema: {
@ -3726,9 +3828,12 @@ datasourceDescribe(
await assertTableExists(table)
await assertTableNumRows(table, 1)
})
})
})
})
}
)
})
}
)
)
})
}
)
}

View File

@ -38,8 +38,11 @@ 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$/
datasourceDescribe(
{ name: "/tables (%s)", exclude: [DatabaseName.MONGODB] },
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
if (descriptions.length) {
describe.each(descriptions)(
"/tables ($dbName)",
({ config, dsProvider, isInternal, isOracle }) => {
let datasource: Datasource | undefined
@ -332,7 +335,9 @@ datasourceDescribe(
expect(updatedTable).toEqual(expect.objectContaining(expected))
const persistedTable = await config.api.table.get(updatedTable._id!)
const persistedTable = await config.api.table.get(
updatedTable._id!
)
expected = {
...table,
name: newName,
@ -561,8 +566,14 @@ datasourceDescribe(
await config.api.table.save(saveTableRequest, {
status: 200,
})
saveTableRequest.schema.foo = { type: FieldType.STRING, name: "foo" }
saveTableRequest.schema.FOO = { type: FieldType.STRING, name: "FOO" }
saveTableRequest.schema.foo = {
type: FieldType.STRING,
name: "foo",
}
saveTableRequest.schema.FOO = {
type: FieldType.STRING,
name: "FOO",
}
await config.api.table.save(saveTableRequest, {
status: 400,
@ -1180,10 +1191,12 @@ datasourceDescribe(
schema,
})
)
const result = await config.api.table.validateExistingTableImport({
const result = await config.api.table.validateExistingTableImport(
{
tableId: table._id,
rows,
})
}
)
return result
},
],
@ -1267,7 +1280,9 @@ datasourceDescribe(
isInternal &&
it.each(
isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS
isInternal
? PROTECTED_INTERNAL_COLUMNS
: PROTECTED_EXTERNAL_COLUMNS
)(
"don't allow protected names in the rows (%s)",
async columnName => {
@ -1487,7 +1502,8 @@ datasourceDescribe(
schema: basicSchema,
})
)
const result = await config.api.table.validateExistingTableImport({
const result = await config.api.table.validateExistingTableImport(
{
tableId: table._id,
rows: [
{
@ -1496,7 +1512,8 @@ datasourceDescribe(
name: generator.first(),
},
],
})
}
)
expect(result).toEqual({
allValid: true,
@ -1512,4 +1529,5 @@ datasourceDescribe(
})
})
}
)
)
}

View File

@ -44,8 +44,11 @@ import merge from "lodash/merge"
import { quotas } from "@budibase/pro"
import { db, roles, context } from "@budibase/backend-core"
datasourceDescribe(
{ name: "/v2/views (%s)", exclude: [DatabaseName.MONGODB] },
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
if (descriptions.length) {
describe.each(descriptions)(
"/v2/views ($dbName)",
({ config, isInternal, dsProvider }) => {
let table: Table
let rawDatasource: Datasource | undefined
@ -129,7 +132,8 @@ datasourceDescribe(
})
it("can persist views with all fields", async () => {
const newView: Required<Omit<CreateViewRequest, "query" | "type">> = {
const newView: Required<Omit<CreateViewRequest, "query" | "type">> =
{
name: generator.name(),
tableId: table._id!,
primaryDisplay: "id",
@ -194,8 +198,9 @@ datasourceDescribe(
})
it("can create a view with just a query field, no queryUI, for backwards compatibility", async () => {
const newView: Required<Omit<CreateViewRequest, "queryUI" | "type">> =
{
const newView: Required<
Omit<CreateViewRequest, "queryUI" | "type">
> = {
name: generator.name(),
tableId: table._id!,
primaryDisplay: "id",
@ -1162,7 +1167,8 @@ datasourceDescribe(
.expect(400)
expect(result.body).toEqual({
message: "View id does not match between the body and the uri path",
message:
"View id does not match between the body and the uri path",
status: 400,
})
})
@ -2016,7 +2022,10 @@ datasourceDescribe(
schema,
})
const renameColumn = async (table: Table, renaming: RenameColumn) => {
const renameColumn = async (
table: Table,
renaming: RenameColumn
) => {
const newSchema = { ...table.schema }
newSchema[renaming.updated] = {
...table.schema[renaming.old],
@ -2583,6 +2592,33 @@ datasourceDescribe(
})
})
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: {},
})
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()
})
})
describe("destroy", () => {
const getRowUsage = async () => {
const { total } = await config.doInContext(undefined, () =>
@ -2617,7 +2653,9 @@ datasourceDescribe(
])
const rowUsage = await getRowUsage()
await config.api.row.bulkDelete(view.id, { rows: [rows[0], rows[2]] })
await config.api.row.bulkDelete(view.id, {
rows: [rows[0], rows[2]],
})
await assertRowUsage(isInternal ? rowUsage - 2 : rowUsage)
@ -3470,7 +3508,10 @@ datasourceDescribe(
expect(response.rows).toEqual(
expect.arrayContaining([
expect.objectContaining({
"Quantity Sum": rows.reduce((acc, r) => acc + r.quantity, 0),
"Quantity Sum": rows.reduce(
(acc, r) => acc + r.quantity,
0
),
}),
])
)
@ -3511,7 +3552,9 @@ datasourceDescribe(
}
for (const row of response.rows) {
expect(row["Total Price"]).toEqual(priceByQuantity[row.quantity])
expect(row["Total Price"]).toEqual(
priceByQuantity[row.quantity]
)
}
})
@ -3701,9 +3744,12 @@ datasourceDescribe(
},
})
const apertureScience = await config.api.row.save(companies._id!, {
const apertureScience = await config.api.row.save(
companies._id!,
{
name: "Aperture Science Laboratories",
})
}
)
const blackMesa = await config.api.row.save(companies._id!, {
name: "Black Mesa",
@ -4402,7 +4448,9 @@ datasourceDescribe(
}),
expected: () => [
{
user: expect.objectContaining({ _id: config.getUser()._id }),
user: expect.objectContaining({
_id: config.getUser()._id,
}),
},
],
},
@ -4631,4 +4679,5 @@ datasourceDescribe(
})
})
}
)
)
}

View File

@ -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() {

View File

@ -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(

View File

@ -61,6 +61,9 @@ export async function run({
inputs: ServerLogStepInputs
appId: string
}): Promise<ServerLogStepOutputs> {
if (typeof inputs.text !== "string") {
inputs.text = JSON.stringify(inputs.text)
}
const message = `App ${appId} - ${inputs.text}`
console.log(message)
return {

View File

@ -7,11 +7,13 @@ import {
import { Knex } from "knex"
import { generator } from "@budibase/backend-core/tests"
datasourceDescribe(
{
name: "execute query action",
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
@ -74,4 +76,5 @@ datasourceDescribe(
expect(res.success).toEqual(false)
})
}
)
)
}

View File

@ -1,50 +1,123 @@
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import * as automation from "../index"
import * as setup from "./utilities"
import { Table } from "@budibase/types"
describe("test the execute script action", () => {
let config = getConfig()
describe("Execute Script Automations", () => {
let config = setup.getConfig(),
table: Table
beforeAll(async () => {
beforeEach(async () => {
await automation.init()
await config.init()
})
afterAll(_afterAll)
it("should be able to execute a script", async () => {
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, {
code: "return 1 + 1",
})
expect(res.value).toEqual(2)
expect(res.success).toEqual(true)
table = await config.createTable()
await config.createRow()
})
it("should handle a null value", async () => {
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, {
code: null,
})
expect(res.response.message).toEqual("Invalid inputs")
expect(res.success).toEqual(false)
afterAll(setup.afterAll)
it("should execute a basic script and return the result", async () => {
const builder = createAutomationBuilder({
name: "Basic Script Execution",
})
it("should be able to get a value from context", async () => {
const res = await runStep(
config,
actions.EXECUTE_SCRIPT.stepId,
const results = await builder
.appAction({ fields: {} })
.executeScript({ code: "return 2 + 2" })
.run()
expect(results.steps[0].outputs.value).toEqual(4)
})
it("should access bindings from previous steps", async () => {
const builder = createAutomationBuilder({
name: "Access Bindings",
})
const results = await builder
.appAction({ fields: { data: [1, 2, 3] } })
.executeScript(
{
code: "return steps.map(d => d.value)",
code: "return trigger.fields.data.map(x => x * 2)",
},
{
steps: [{ value: 0 }, { value: 1 }],
}
{ stepId: "binding-script-step" }
)
expect(res.value).toEqual([0, 1])
expect(res.response).toBeUndefined()
expect(res.success).toEqual(true)
.run()
expect(results.steps[0].outputs.value).toEqual([2, 4, 6])
})
it("should be able to handle an error gracefully", async () => {
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, {
code: "return something.map(x => x.name)",
it("should handle script execution errors gracefully", async () => {
const builder = createAutomationBuilder({
name: "Handle Script Errors",
})
expect(res.response).toEqual("ReferenceError: something is not defined")
expect(res.success).toEqual(false)
const results = await builder
.appAction({ fields: {} })
.executeScript({ code: "return nonexistentVariable.map(x => x)" })
.run()
expect(results.steps[0].outputs.response).toContain(
"ReferenceError: nonexistentVariable is not defined"
)
expect(results.steps[0].outputs.success).toEqual(false)
})
it("should handle conditional logic in scripts", async () => {
const builder = createAutomationBuilder({
name: "Conditional Script Logic",
})
const results = await builder
.appAction({ fields: { value: 10 } })
.executeScript({
code: `
if (trigger.fields.value > 5) {
return "Value is greater than 5";
} else {
return "Value is 5 or less";
}
`,
})
.run()
expect(results.steps[0].outputs.value).toEqual("Value is greater than 5")
})
it("should use multiple steps and validate script execution", async () => {
const builder = createAutomationBuilder({
name: "Multi-Step Script Execution",
})
const results = await builder
.appAction({ fields: {} })
.serverLog(
{ text: "Starting multi-step automation" },
{ stepId: "start-log-step" }
)
.createRow(
{ row: { name: "Test Row", value: 42, tableId: table._id } },
{ stepId: "abc123" }
)
.executeScript(
{
code: `
const createdRow = steps['abc123'];
return createdRow.row.value * 2;
`,
},
{ stepId: "ScriptingStep1" }
)
.serverLog({
text: `Final result is {{ steps.ScriptingStep1.value }}`,
})
.run()
expect(results.steps[0].outputs.message).toContain(
"Starting multi-step automation"
)
expect(results.steps[1].outputs.row.value).toEqual(42)
expect(results.steps[2].outputs.value).toEqual(84)
expect(results.steps[3].outputs.message).toContain("Final result is 84")
})
})

View File

@ -433,9 +433,10 @@ describe("Automation Scenarios", () => {
})
})
datasourceDescribe(
{ name: "", only: [DatabaseName.MYSQL] },
({ config, dsProvider }) => {
const descriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] })
if (descriptions.length) {
describe.each(descriptions)("/rows ($dbName)", ({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
@ -531,5 +532,5 @@ datasourceDescribe(
)
})
})
}
)
})
}

View File

@ -34,6 +34,7 @@ import {
SearchFilters,
Branch,
FilterStepInputs,
ExecuteScriptStepInputs,
} from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import * as setup from "../utilities"
@ -201,6 +202,18 @@ class BaseStepBuilder {
)
}
executeScript(
input: ExecuteScriptStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.EXECUTE_SCRIPT,
BUILTIN_ACTION_DEFINITIONS.EXECUTE_SCRIPT,
input,
opts
)
}
filter(input: FilterStepInputs): this {
return this.step(
AutomationActionStepId.FILTER,

View File

@ -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 {}

View File

@ -10,11 +10,11 @@ function uniqueTableName(length?: number): string {
.substring(0, length || 10)
}
datasourceDescribe(
{
name: "Integration compatibility with mysql search_path",
only: [DatabaseName.MYSQL],
},
const mainDescriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] })
if (mainDescriptions.length) {
describe.each(mainDescriptions)(
"/Integration compatibility with mysql search_path ($dbName)",
({ config, dsProvider }) => {
let rawDatasource: Datasource
let datasource: Datasource
@ -71,18 +71,20 @@ datasourceDescribe(
datasourceId: datasource._id!,
tablesFilter: [repeated_table_name],
})
expect(res.datasource.entities![repeated_table_name].schema).toBeDefined()
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"])
})
}
)
)
datasourceDescribe(
{
name: "POST /api/datasources/:datasourceId/schema",
only: [DatabaseName.MYSQL],
},
const descriptions = datasourceDescribe({ only: [DatabaseName.MYSQL] })
if (descriptions.length) {
describe.each(descriptions)(
"POST /api/datasources/:datasourceId/schema ($dbName)",
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
@ -125,4 +127,6 @@ datasourceDescribe(
expect(table.schema[enumColumnName].type).toEqual(FieldType.OPTIONS)
})
}
)
)
}
}

View File

@ -8,8 +8,11 @@ import {
} from "../integrations/tests/utils"
import { Knex } from "knex"
datasourceDescribe(
{ name: "postgres integrations", only: [DatabaseName.POSTGRES] },
const mainDescriptions = datasourceDescribe({ only: [DatabaseName.POSTGRES] })
if (mainDescriptions.length) {
describe.each(mainDescriptions)(
"/postgres integrations",
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
@ -199,18 +202,21 @@ datasourceDescribe(
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" })
row = await config.api.row.save(table._id!, {
...row,
price: "400.00",
})
expect(row.price).toBe("400.00")
})
})
}
)
)
datasourceDescribe(
{
name: "Integration compatibility with postgres search_path",
only: [DatabaseName.POSTGRES],
},
const descriptions = datasourceDescribe({ only: [DatabaseName.POSTGRES] })
if (descriptions.length) {
describe.each(descriptions)(
"Integration compatibility with postgres search_path",
({ config, dsProvider }) => {
let datasource: Datasource
let client: Knex
@ -283,8 +289,11 @@ datasourceDescribe(
expect(
response.datasource.entities?.[repeated_table_name].schema
).toBeDefined()
const schema = response.datasource.entities?.[repeated_table_name].schema
const schema =
response.datasource.entities?.[repeated_table_name].schema
expect(Object.keys(schema || {}).sort()).toEqual(["id", "val1"])
})
}
)
)
}
}

View File

@ -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,
},

View File

@ -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<ConnectionInfo> {

View File

@ -35,7 +35,6 @@ const providers: Record<DatabaseName, DatasourceProvider> = {
}
export interface DatasourceDescribeOpts {
name: string
only?: DatabaseName[]
exclude?: DatabaseName[]
}
@ -102,16 +101,12 @@ function createDummyTest() {
})
}
export function datasourceDescribe(
opts: DatasourceDescribeOpts,
cb: (args: DatasourceDescribeReturn) => void
) {
export function datasourceDescribe(opts: DatasourceDescribeOpts) {
if (process.env.DATASOURCE === "none") {
createDummyTest()
return
}
const { name, only, exclude } = opts
const { only, exclude } = opts
if (only && exclude) {
throw new Error("you can only supply one of 'only' or 'exclude'")
@ -130,36 +125,28 @@ export function datasourceDescribe(
if (databases.length === 0) {
createDummyTest()
return
}
describe.each(databases)(name, name => {
const config = new TestConfiguration()
afterAll(() => {
config.end()
})
cb({
name,
return databases.map(dbName => ({
dbName,
config,
dsProvider: () => createDatasources(config, name),
isInternal: name === DatabaseName.SQS,
isExternal: name !== DatabaseName.SQS,
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(name),
isMySQL: name === DatabaseName.MYSQL,
isPostgres: name === DatabaseName.POSTGRES,
isMongodb: name === DatabaseName.MONGODB,
isMSSQL: name === DatabaseName.SQL_SERVER,
isOracle: name === 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(

View File

@ -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(

View File

@ -19,8 +19,11 @@ 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.
datasourceDescribe(
{ name: "search sdk (%s)", exclude: [DatabaseName.MONGODB] },
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
if (descriptions.length) {
describe.each(descriptions)(
"search sdk ($dbName)",
({ config, dsProvider, isInternal }) => {
let datasource: Datasource | undefined
let table: Table
@ -217,4 +220,5 @@ datasourceDescribe(
}
)
}
)
)
}

View File

@ -78,8 +78,11 @@ export async function getAllInternalTables(db?: Database): Promise<Table[]> {
}
async function getAllExternalTables(): Promise<Table[]> {
// 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) {

View File

@ -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<ViewV2Enriched> {
return pickApi(tableId).getEnriched(viewId)
}
export async function getAllEnriched(): Promise<ViewV2Enriched[]> {
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<Table> {
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<ViewV2Enriched> {
async function populateRelTableSchema(
tableId: string,
viewFields: Record<string, RelationSchemaField>
) {
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<string, ViewV2ColumnEnriched> = {}
for (const relTableFieldName of Object.keys(relTable.schema)) {
const relTableField = relTable.schema[relTableFieldName]

View File

@ -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()
}

View File

@ -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<ViewFetchResponseEnriched>(`/api/v2/views`, {
expectations,
})
}
search = async (
viewId: string,
params?: SearchViewRowRequest,

View File

@ -385,7 +385,7 @@ class Orchestrator {
stepIdx: number,
pathIdx?: number
): Promise<number> {
await processObject(loopStep.inputs, this.processContext(this.context))
await processObject(loopStep.inputs, this.mergeContexts(this.context))
const iterations = getLoopIterations(loopStep)
let stepToLoopIndex = stepIdx + 1
let pathStepIdx = (pathIdx || stepIdx) + 1
@ -573,14 +573,14 @@ class Orchestrator {
for (const [field, value] of Object.entries(filters[filterKey])) {
const fromContext = processStringSync(
field,
this.processContext(this.context)
this.mergeContexts(this.context)
)
toFilter[field] = fromContext
if (typeof value === "string" && findHBSBlocks(value).length > 0) {
const processedVal = processStringSync(
value,
this.processContext(this.context)
this.mergeContexts(this.context)
)
filters[filterKey][field] = processedVal
@ -637,7 +637,7 @@ class Orchestrator {
const stepFn = await this.getStepFunctionality(step.stepId)
let inputs = await processObject(
originalStepInput,
this.processContext(this.context)
this.mergeContexts(this.context)
)
inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs)
@ -645,7 +645,7 @@ class Orchestrator {
inputs: inputs,
appId: this.appId,
emitter: this.emitter,
context: this.context,
context: this.mergeContexts(this.context),
})
this.handleStepOutput(step, outputs, loopIteration)
}
@ -665,8 +665,8 @@ class Orchestrator {
return null
}
private processContext(context: AutomationContext) {
const processContext = {
private mergeContexts(context: AutomationContext) {
const mergeContexts = {
...context,
steps: {
...context.steps,
@ -674,7 +674,7 @@ class Orchestrator {
...context.stepsByName,
},
}
return processContext
return mergeContexts
}
private handleStepOutput(

View File

@ -136,22 +136,24 @@ 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) {
const ctx = { data: rows, params: enrichedParameters }
rows = vm.withContext(ctx, () => vm.execute(transformer!))
}
}
// if the request fails we retry once, invalidating the cached value
if (info && info.code >= 400 && !this.hasRerun) {

View File

@ -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}`)
})
}

View File

@ -1,6 +1,7 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"lib": ["es2020", "dom"],
"composite": true,
"baseUrl": "."
},

View File

@ -0,0 +1,4 @@
*
!dist/**/*
dist/tsconfig.build.tsbuildinfo
!package.json

Some files were not shown because too many files have changed in this diff Show More