Merge branch 'master' of github.com:Budibase/budibase into new-datepicker
This commit is contained in:
commit
0aeddfa029
|
@ -42,7 +42,17 @@
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"local-rules/no-budibase-imports": "error",
|
||||||
|
"local-rules/no-console-error": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"local-rules/no-budibase-imports": "error"
|
"local-rules/no-budibase-imports": "error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -59,7 +69,15 @@
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"local-rules/no-test-com": "error",
|
"local-rules/no-test-com": "error",
|
||||||
"local-rules/email-domain-example-com": "error",
|
"local-rules/email-domain-example-com": "error",
|
||||||
"no-console": "warn",
|
"no-console": "warn",
|
||||||
|
@ -89,7 +107,8 @@
|
||||||
{
|
{
|
||||||
"varsIgnorePattern": "^_",
|
"varsIgnorePattern": "^_",
|
||||||
"argsIgnorePattern": "^_",
|
"argsIgnorePattern": "^_",
|
||||||
"destructuredArrayIgnorePattern": "^_"
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"import/no-relative-packages": "error",
|
"import/no-relative-packages": "error",
|
||||||
|
|
|
@ -24,5 +24,8 @@
|
||||||
},
|
},
|
||||||
"[svelte]": {
|
"[svelte]": {
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||||
|
},
|
||||||
|
"[handlebars]": {
|
||||||
|
"editor.formatOnSave": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,8 @@ spec:
|
||||||
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
- name: BACKUPS_BUCKET_NAME
|
- name: BACKUPS_BUCKET_NAME
|
||||||
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
||||||
|
- name: TEMP_BUCKET_NAME
|
||||||
|
value: {{ .Values.globals.tempBucketName | quote }}
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: {{ .Values.services.apps.port | quote }}
|
value: {{ .Values.services.apps.port | quote }}
|
||||||
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
||||||
|
|
|
@ -107,6 +107,8 @@ spec:
|
||||||
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
- name: BACKUPS_BUCKET_NAME
|
- name: BACKUPS_BUCKET_NAME
|
||||||
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
||||||
|
- name: TEMP_BUCKET_NAME
|
||||||
|
value: {{ .Values.globals.tempBucketName | quote }}
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: {{ .Values.services.automationWorkers.port | quote }}
|
value: {{ .Values.services.automationWorkers.port | quote }}
|
||||||
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
||||||
|
|
|
@ -106,6 +106,8 @@ spec:
|
||||||
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
- name: BACKUPS_BUCKET_NAME
|
- name: BACKUPS_BUCKET_NAME
|
||||||
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
||||||
|
- name: TEMP_BUCKET_NAME
|
||||||
|
value: {{ .Values.globals.tempBucketName | quote }}
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: {{ .Values.services.worker.port | quote }}
|
value: {{ .Values.services.worker.port | quote }}
|
||||||
- name: MULTI_TENANCY
|
- name: MULTI_TENANCY
|
||||||
|
|
|
@ -121,6 +121,9 @@ globals:
|
||||||
# to the old value for the duration of the rotation.
|
# to the old value for the duration of the rotation.
|
||||||
jwtSecretFallback: ""
|
jwtSecretFallback: ""
|
||||||
|
|
||||||
|
## -- If using S3 the bucket name to be used for storing temporary files
|
||||||
|
tempBucketName: ""
|
||||||
|
|
||||||
smtp:
|
smtp:
|
||||||
# -- Whether to enable SMTP or not.
|
# -- Whether to enable SMTP or not.
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
|
@ -1,4 +1,25 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
"no-console-error": {
|
||||||
|
create: function(context) {
|
||||||
|
return {
|
||||||
|
CallExpression(node) {
|
||||||
|
if (
|
||||||
|
node.callee.type === "MemberExpression" &&
|
||||||
|
node.callee.object.name === "console" &&
|
||||||
|
node.callee.property.name === "error" &&
|
||||||
|
node.arguments.length === 1 &&
|
||||||
|
node.arguments[0].name &&
|
||||||
|
node.arguments[0].name.startsWith("err")
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
message: 'Using console.error(err) on its own is not allowed. Either provide context to the error (console.error(msg, err)) or throw it.',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
"no-budibase-imports": {
|
"no-budibase-imports": {
|
||||||
create: function (context) {
|
create: function (context) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -17,6 +17,7 @@ APP_PORT=4002
|
||||||
WORKER_PORT=4003
|
WORKER_PORT=4003
|
||||||
MINIO_PORT=4004
|
MINIO_PORT=4004
|
||||||
COUCH_DB_PORT=4005
|
COUCH_DB_PORT=4005
|
||||||
|
COUCH_DB_SQS_PORT=4006
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
WATCHTOWER_PORT=6161
|
WATCHTOWER_PORT=6161
|
||||||
BUDIBASE_ENVIRONMENT=PRODUCTION
|
BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
|
@ -28,4 +29,4 @@ BB_ADMIN_USER_PASSWORD=
|
||||||
|
|
||||||
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
||||||
PLUGINS_DIR=
|
PLUGINS_DIR=
|
||||||
ROLLING_LOG_MAX_SIZE=
|
ROLLING_LOG_MAX_SIZE=
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.23.5",
|
"version": "2.23.11",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
5
nx.json
5
nx.json
|
@ -9,10 +9,7 @@
|
||||||
},
|
},
|
||||||
"targetDefaults": {
|
"targetDefaults": {
|
||||||
"build": {
|
"build": {
|
||||||
"inputs": [
|
"inputs": ["{workspaceRoot}/scripts/*", "{workspaceRoot}/lerna.json"]
|
||||||
"{workspaceRoot}/scripts/build.js",
|
|
||||||
"{workspaceRoot}/lerna.json"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
"dev:camunda": "./scripts/deploy-camunda.sh",
|
"dev:camunda": "./scripts/deploy-camunda.sh",
|
||||||
"dev:all": "yarn run kill-all && lerna run --stream dev",
|
"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:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||||
"dev:docker": "yarn build --scope @budibase/server --scope @budibase/worker && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
"dev:docker": "./scripts/devDocker.sh",
|
||||||
"test": "REUSE_CONTAINERS=1 lerna run --concurrency 1 --stream test --stream",
|
"test": "REUSE_CONTAINERS=1 lerna run --concurrency 1 --stream test --stream",
|
||||||
"lint:eslint": "eslint packages --max-warnings=0",
|
"lint:eslint": "eslint packages --max-warnings=0",
|
||||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit bd0e01d639ec3b2547e7c859a1c43b622dce8344
|
Subproject commit c167c331ff9b8161fc18e2ecbaaf1ea5815ba964
|
|
@ -64,7 +64,6 @@ async function refreshOIDCAccessToken(
|
||||||
}
|
}
|
||||||
strategy = await oidc.strategyFactory(enrichedConfig, ssoSaveUserNoOp)
|
strategy = await oidc.strategyFactory(enrichedConfig, ssoSaveUserNoOp)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
|
||||||
throw new Error("Could not refresh OAuth Token")
|
throw new Error("Could not refresh OAuth Token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +98,6 @@ async function refreshGoogleAccessToken(
|
||||||
ssoSaveUserNoOp
|
ssoSaveUserNoOp
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error constructing OIDC refresh strategy: message=${err.message}`
|
`Error constructing OIDC refresh strategy: message=${err.message}`
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,19 +8,9 @@ import {
|
||||||
SearchParams,
|
SearchParams,
|
||||||
WithRequired,
|
WithRequired,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
|
||||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
export const removeKeyNumbering = dataFilters.removeKeyNumbering
|
||||||
|
|
||||||
export function removeKeyNumbering(key: any): string {
|
|
||||||
if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) {
|
|
||||||
const parts = key.split(":")
|
|
||||||
// remove the number
|
|
||||||
parts.shift()
|
|
||||||
return parts.join(":")
|
|
||||||
} else {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to build lucene query URLs.
|
* Class to build lucene query URLs.
|
||||||
|
|
|
@ -29,6 +29,7 @@ const DefaultBucketName = {
|
||||||
TEMPLATES: "templates",
|
TEMPLATES: "templates",
|
||||||
GLOBAL: "global",
|
GLOBAL: "global",
|
||||||
PLUGINS: "plugins",
|
PLUGINS: "plugins",
|
||||||
|
TEMP: "tmp-file-attachments",
|
||||||
}
|
}
|
||||||
|
|
||||||
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
||||||
|
@ -107,7 +108,7 @@ const environment = {
|
||||||
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
||||||
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
API_ENCRYPTION_KEY: getAPIEncryptionKey(),
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||||
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || "http://localhost:4984",
|
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || "http://localhost:4006",
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
@ -146,6 +147,7 @@ const environment = {
|
||||||
process.env.GLOBAL_BUCKET_NAME || DefaultBucketName.GLOBAL,
|
process.env.GLOBAL_BUCKET_NAME || DefaultBucketName.GLOBAL,
|
||||||
PLUGIN_BUCKET_NAME:
|
PLUGIN_BUCKET_NAME:
|
||||||
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
|
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
|
||||||
|
TEMP_BUCKET_NAME: process.env.TEMP_BUCKET_NAME || DefaultBucketName.TEMP,
|
||||||
USE_COUCH: process.env.USE_COUCH || true,
|
USE_COUCH: process.env.USE_COUCH || true,
|
||||||
MOCK_REDIS: process.env.MOCK_REDIS,
|
MOCK_REDIS: process.env.MOCK_REDIS,
|
||||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||||
|
|
|
@ -138,7 +138,6 @@ export default function (
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
authenticated = false
|
authenticated = false
|
||||||
console.error(`Auth Error: ${err.message}`)
|
console.error(`Auth Error: ${err.message}`)
|
||||||
console.error(err)
|
|
||||||
// remove the cookie as the user does not exist anymore
|
// remove the cookie as the user does not exist anymore
|
||||||
clearCookie(ctx, Cookie.Auth)
|
clearCookie(ctx, Cookie.Auth)
|
||||||
}
|
}
|
||||||
|
@ -187,7 +186,6 @@ export default function (
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`Auth Error: ${err.message}`)
|
console.error(`Auth Error: ${err.message}`)
|
||||||
console.error(err)
|
|
||||||
// invalid token, clear the cookie
|
// invalid token, clear the cookie
|
||||||
if (err?.name === "JsonWebTokenError") {
|
if (err?.name === "JsonWebTokenError") {
|
||||||
clearCookie(ctx, Cookie.Auth)
|
clearCookie(ctx, Cookie.Auth)
|
||||||
|
|
|
@ -12,7 +12,7 @@ export async function errorHandling(ctx: any, next: any) {
|
||||||
if (status >= 400 && status < 500) {
|
if (status >= 400 && status < 500) {
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
} else {
|
} else {
|
||||||
console.error(err)
|
console.error("Got 400 response code", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
let error: APIError = {
|
let error: APIError = {
|
||||||
|
|
|
@ -68,7 +68,6 @@ export async function strategyFactory(
|
||||||
verify
|
verify
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
|
||||||
throw new Error(`Error constructing google authentication strategy: ${err}`)
|
throw new Error(`Error constructing google authentication strategy: ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,6 @@ export async function strategyFactory(
|
||||||
strategy.name = "oidc"
|
strategy.name = "oidc"
|
||||||
return strategy
|
return strategy
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
|
||||||
throw new Error(`Error constructing OIDC authentication strategy - ${err}`)
|
throw new Error(`Error constructing OIDC authentication strategy - ${err}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,7 +141,6 @@ export async function fetchStrategyConfig(
|
||||||
callbackURL: callbackUrl,
|
callbackURL: callbackUrl,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error constructing OIDC authentication configuration - ${err}`
|
`Error constructing OIDC authentication configuration - ${err}`
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,7 +26,6 @@ export const getMigrationsDoc = async (db: any) => {
|
||||||
if (err.status && err.status === 404) {
|
if (err.status && err.status === 404) {
|
||||||
return { _id: DocumentType.MIGRATIONS }
|
return { _id: DocumentType.MIGRATIONS }
|
||||||
} else {
|
} else {
|
||||||
console.error(err)
|
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,31 +7,41 @@ import tar from "tar-fs"
|
||||||
import zlib from "zlib"
|
import zlib from "zlib"
|
||||||
import { promisify } from "util"
|
import { promisify } from "util"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import fs, { ReadStream } from "fs"
|
import fs, { PathLike, ReadStream } from "fs"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { budibaseTempDir } from "./utils"
|
import { bucketTTLConfig, budibaseTempDir } from "./utils"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
||||||
|
import fsp from "fs/promises"
|
||||||
|
|
||||||
const streamPipeline = promisify(stream.pipeline)
|
const streamPipeline = promisify(stream.pipeline)
|
||||||
// use this as a temporary store of buckets that are being created
|
// use this as a temporary store of buckets that are being created
|
||||||
const STATE = {
|
const STATE = {
|
||||||
bucketCreationPromises: {},
|
bucketCreationPromises: {},
|
||||||
}
|
}
|
||||||
|
const signedFilePrefix = "/files/signed"
|
||||||
|
|
||||||
type ListParams = {
|
type ListParams = {
|
||||||
ContinuationToken?: string
|
ContinuationToken?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadParams = {
|
type BaseUploadParams = {
|
||||||
bucket: string
|
bucket: string
|
||||||
filename: string
|
filename: string
|
||||||
path: string
|
|
||||||
type?: string | null
|
type?: string | null
|
||||||
// can be undefined, we will remove it
|
metadata?: { [key: string]: string | undefined }
|
||||||
metadata?: {
|
body?: ReadableStream | Buffer
|
||||||
[key: string]: string | undefined
|
ttl?: number
|
||||||
}
|
addTTL?: boolean
|
||||||
|
extra?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadParams = BaseUploadParams & {
|
||||||
|
path?: string | PathLike
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamUploadParams = BaseUploadParams & {
|
||||||
|
stream: ReadStream
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONTENT_TYPE_MAP: any = {
|
const CONTENT_TYPE_MAP: any = {
|
||||||
|
@ -41,6 +51,8 @@ const CONTENT_TYPE_MAP: any = {
|
||||||
js: "application/javascript",
|
js: "application/javascript",
|
||||||
json: "application/json",
|
json: "application/json",
|
||||||
gz: "application/gzip",
|
gz: "application/gzip",
|
||||||
|
svg: "image/svg+xml",
|
||||||
|
form: "multipart/form-data",
|
||||||
}
|
}
|
||||||
|
|
||||||
const STRING_CONTENT_TYPES = [
|
const STRING_CONTENT_TYPES = [
|
||||||
|
@ -105,7 +117,10 @@ export function ObjectStore(
|
||||||
* Given an object store and a bucket name this will make sure the bucket exists,
|
* Given an object store and a bucket name this will make sure the bucket exists,
|
||||||
* if it does not exist then it will create it.
|
* if it does not exist then it will create it.
|
||||||
*/
|
*/
|
||||||
export async function makeSureBucketExists(client: any, bucketName: string) {
|
export async function createBucketIfNotExists(
|
||||||
|
client: any,
|
||||||
|
bucketName: string
|
||||||
|
): Promise<{ created: boolean; exists: boolean }> {
|
||||||
bucketName = sanitizeBucket(bucketName)
|
bucketName = sanitizeBucket(bucketName)
|
||||||
try {
|
try {
|
||||||
await client
|
await client
|
||||||
|
@ -113,15 +128,16 @@ export async function makeSureBucketExists(client: any, bucketName: string) {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
})
|
})
|
||||||
.promise()
|
.promise()
|
||||||
|
return { created: false, exists: true }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const promises: any = STATE.bucketCreationPromises
|
const promises: any = STATE.bucketCreationPromises
|
||||||
const doesntExist = err.statusCode === 404,
|
const doesntExist = err.statusCode === 404,
|
||||||
noAccess = err.statusCode === 403
|
noAccess = err.statusCode === 403
|
||||||
if (promises[bucketName]) {
|
if (promises[bucketName]) {
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
|
return { created: false, exists: true }
|
||||||
} else if (doesntExist || noAccess) {
|
} else if (doesntExist || noAccess) {
|
||||||
if (doesntExist) {
|
if (doesntExist) {
|
||||||
// bucket doesn't exist create it
|
|
||||||
promises[bucketName] = client
|
promises[bucketName] = client
|
||||||
.createBucket({
|
.createBucket({
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
|
@ -129,13 +145,15 @@ export async function makeSureBucketExists(client: any, bucketName: string) {
|
||||||
.promise()
|
.promise()
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
delete promises[bucketName]
|
delete promises[bucketName]
|
||||||
|
return { created: true, exists: false }
|
||||||
|
} else {
|
||||||
|
throw new Error("Access denied to object store bucket." + err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unable to write to object store bucket.")
|
throw new Error("Unable to write to object store bucket.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads the contents of a file given the required parameters, useful when
|
* Uploads the contents of a file given the required parameters, useful when
|
||||||
* temp files in use (for example file uploaded as an attachment).
|
* temp files in use (for example file uploaded as an attachment).
|
||||||
|
@ -146,12 +164,22 @@ export async function upload({
|
||||||
path,
|
path,
|
||||||
type,
|
type,
|
||||||
metadata,
|
metadata,
|
||||||
|
body,
|
||||||
|
ttl,
|
||||||
}: UploadParams) {
|
}: UploadParams) {
|
||||||
const extension = filename.split(".").pop()
|
const extension = filename.split(".").pop()
|
||||||
const fileBytes = fs.readFileSync(path)
|
|
||||||
|
const fileBytes = path ? (await fsp.open(path)).createReadStream() : body
|
||||||
|
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
|
||||||
|
if (ttl && (bucketCreated.created || bucketCreated.exists)) {
|
||||||
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||||
|
if (objectStore.putBucketLifecycleConfiguration) {
|
||||||
|
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let contentType = type
|
let contentType = type
|
||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
|
@ -174,6 +202,7 @@ export async function upload({
|
||||||
}
|
}
|
||||||
config.Metadata = metadata
|
config.Metadata = metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectStore.upload(config).promise()
|
return objectStore.upload(config).promise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,14 +210,24 @@ export async function upload({
|
||||||
* Similar to the upload function but can be used to send a file stream
|
* Similar to the upload function but can be used to send a file stream
|
||||||
* through to the object store.
|
* through to the object store.
|
||||||
*/
|
*/
|
||||||
export async function streamUpload(
|
export async function streamUpload({
|
||||||
bucketName: string,
|
bucket: bucketName,
|
||||||
filename: string,
|
stream,
|
||||||
stream: ReadStream | ReadableStream,
|
filename,
|
||||||
extra = {}
|
type,
|
||||||
) {
|
extra,
|
||||||
|
ttl,
|
||||||
|
}: StreamUploadParams) {
|
||||||
|
const extension = filename.split(".").pop()
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
||||||
|
|
||||||
|
if (ttl && (bucketCreated.created || bucketCreated.exists)) {
|
||||||
|
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
||||||
|
if (objectStore.putBucketLifecycleConfiguration) {
|
||||||
|
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set content type for certain known extensions
|
// Set content type for certain known extensions
|
||||||
if (filename?.endsWith(".js")) {
|
if (filename?.endsWith(".js")) {
|
||||||
|
@ -203,10 +242,18 @@ export async function streamUpload(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let contentType = type
|
||||||
|
if (!contentType) {
|
||||||
|
contentType = extension
|
||||||
|
? CONTENT_TYPE_MAP[extension.toLowerCase()]
|
||||||
|
: CONTENT_TYPE_MAP.txt
|
||||||
|
}
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: sanitizeBucket(bucketName),
|
Bucket: sanitizeBucket(bucketName),
|
||||||
Key: sanitizeKey(filename),
|
Key: sanitizeKey(filename),
|
||||||
Body: stream,
|
Body: stream,
|
||||||
|
ContentType: contentType,
|
||||||
...extra,
|
...extra,
|
||||||
}
|
}
|
||||||
return objectStore.upload(params).promise()
|
return objectStore.upload(params).promise()
|
||||||
|
@ -286,7 +333,7 @@ export function getPresignedUrl(
|
||||||
const signedUrl = new URL(url)
|
const signedUrl = new URL(url)
|
||||||
const path = signedUrl.pathname
|
const path = signedUrl.pathname
|
||||||
const query = signedUrl.search
|
const query = signedUrl.search
|
||||||
return `/files/signed${path}${query}`
|
return `${signedFilePrefix}${path}${query}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,7 +388,7 @@ export async function retrieveDirectory(bucketName: string, path: string) {
|
||||||
*/
|
*/
|
||||||
export async function deleteFile(bucketName: string, filepath: string) {
|
export async function deleteFile(bucketName: string, filepath: string) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
await createBucketIfNotExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Key: sanitizeKey(filepath),
|
Key: sanitizeKey(filepath),
|
||||||
|
@ -351,7 +398,7 @@ export async function deleteFile(bucketName: string, filepath: string) {
|
||||||
|
|
||||||
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
await makeSureBucketExists(objectStore, bucketName)
|
await createBucketIfNotExists(objectStore, bucketName)
|
||||||
const params = {
|
const params = {
|
||||||
Bucket: bucketName,
|
Bucket: bucketName,
|
||||||
Delete: {
|
Delete: {
|
||||||
|
@ -412,7 +459,13 @@ export async function uploadDirectory(
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
uploads.push(uploadDirectory(bucketName, local, path))
|
uploads.push(uploadDirectory(bucketName, local, path))
|
||||||
} else {
|
} else {
|
||||||
uploads.push(streamUpload(bucketName, path, fs.createReadStream(local)))
|
uploads.push(
|
||||||
|
streamUpload({
|
||||||
|
bucket: bucketName,
|
||||||
|
filename: path,
|
||||||
|
stream: fs.createReadStream(local),
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(uploads)
|
await Promise.all(uploads)
|
||||||
|
@ -467,3 +520,23 @@ export async function getReadStream(
|
||||||
}
|
}
|
||||||
return client.getObject(params).createReadStream()
|
return client.getObject(params).createReadStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Given a signed url like '/files/signed/tmp-files-attachments/app_123456/myfile.txt' extract
|
||||||
|
the bucket and the path from it
|
||||||
|
*/
|
||||||
|
export function extractBucketAndPath(
|
||||||
|
url: string
|
||||||
|
): { bucket: string; path: string } | null {
|
||||||
|
const baseUrl = url.split("?")[0]
|
||||||
|
|
||||||
|
const regex = new RegExp(`^${signedFilePrefix}/(?<bucket>[^/]+)/(?<path>.+)$`)
|
||||||
|
const match = baseUrl.match(regex)
|
||||||
|
|
||||||
|
if (match && match.groups) {
|
||||||
|
const { bucket, path } = match.groups
|
||||||
|
return { bucket, path }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { join } from "path"
|
||||||
import { tmpdir } from "os"
|
import { tmpdir } from "os"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"
|
||||||
|
|
||||||
/****************************************************
|
/****************************************************
|
||||||
* NOTE: When adding a new bucket - name *
|
* NOTE: When adding a new bucket - name *
|
||||||
|
@ -15,6 +16,7 @@ export const ObjectStoreBuckets = {
|
||||||
TEMPLATES: env.TEMPLATES_BUCKET_NAME,
|
TEMPLATES: env.TEMPLATES_BUCKET_NAME,
|
||||||
GLOBAL: env.GLOBAL_BUCKET_NAME,
|
GLOBAL: env.GLOBAL_BUCKET_NAME,
|
||||||
PLUGINS: env.PLUGIN_BUCKET_NAME,
|
PLUGINS: env.PLUGIN_BUCKET_NAME,
|
||||||
|
TEMP: env.TEMP_BUCKET_NAME,
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbTmp = join(tmpdir(), ".budibase")
|
const bbTmp = join(tmpdir(), ".budibase")
|
||||||
|
@ -29,3 +31,27 @@ try {
|
||||||
export function budibaseTempDir() {
|
export function budibaseTempDir() {
|
||||||
return bbTmp
|
return bbTmp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const bucketTTLConfig = (
|
||||||
|
bucketName: string,
|
||||||
|
days: number
|
||||||
|
): PutBucketLifecycleConfigurationRequest => {
|
||||||
|
const lifecycleRule = {
|
||||||
|
ID: `${bucketName}-ExpireAfter${days}days`,
|
||||||
|
Prefix: "",
|
||||||
|
Status: "Enabled",
|
||||||
|
Expiration: {
|
||||||
|
Days: days,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const lifecycleConfiguration = {
|
||||||
|
Rules: [lifecycleRule],
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Bucket: bucketName,
|
||||||
|
LifecycleConfiguration: lifecycleConfiguration,
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
|
@ -115,7 +115,6 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
* a JSON message as this is required by Bull.
|
* a JSON message as this is required by Bull.
|
||||||
* @param repeat serves no purpose for the import queue.
|
* @param repeat serves no purpose for the import queue.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
async add(data: any, opts?: JobOptions) {
|
async add(data: any, opts?: JobOptions) {
|
||||||
const jobId = opts?.jobId?.toString()
|
const jobId = opts?.jobId?.toString()
|
||||||
if (jobId && this._queuedJobIds.has(jobId)) {
|
if (jobId && this._queuedJobIds.has(jobId)) {
|
||||||
|
@ -166,8 +165,7 @@ class InMemoryQueue implements Partial<Queue> {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
async removeJobs(_pattern: string) {
|
||||||
async removeJobs(pattern: string) {
|
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,8 @@ type CreateAdminUserOpts = {
|
||||||
hashPassword?: boolean
|
hashPassword?: boolean
|
||||||
requirePassword?: boolean
|
requirePassword?: boolean
|
||||||
skipPasswordValidation?: boolean
|
skipPasswordValidation?: boolean
|
||||||
|
firstName?: string
|
||||||
|
lastName?: string
|
||||||
}
|
}
|
||||||
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
|
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
|
||||||
|
|
||||||
|
@ -517,6 +519,8 @@ export class UserDB {
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
tenantId,
|
tenantId,
|
||||||
|
firstName: opts?.firstName,
|
||||||
|
lastName: opts?.lastName,
|
||||||
}
|
}
|
||||||
if (opts?.ssoId) {
|
if (opts?.ssoId) {
|
||||||
user.ssoId = opts.ssoId
|
user.ssoId = opts.ssoId
|
||||||
|
|
|
@ -17,8 +17,8 @@ import {
|
||||||
ContextUser,
|
ContextUser,
|
||||||
CouchFindOptions,
|
CouchFindOptions,
|
||||||
DatabaseQueryOpts,
|
DatabaseQueryOpts,
|
||||||
SearchQuery,
|
SearchFilters,
|
||||||
SearchQueryOperators,
|
SearchFilterOperator,
|
||||||
SearchUsersRequest,
|
SearchUsersRequest,
|
||||||
User,
|
User,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -44,11 +44,11 @@ function removeUserPassword(users: User | User[]) {
|
||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSupportedUserSearch(query: SearchQuery) {
|
export function isSupportedUserSearch(query: SearchFilters) {
|
||||||
const allowed = [
|
const allowed = [
|
||||||
{ op: SearchQueryOperators.STRING, key: "email" },
|
{ op: SearchFilterOperator.STRING, key: "email" },
|
||||||
{ op: SearchQueryOperators.EQUAL, key: "_id" },
|
{ op: SearchFilterOperator.EQUAL, key: "_id" },
|
||||||
{ op: SearchQueryOperators.ONE_OF, key: "_id" },
|
{ op: SearchFilterOperator.ONE_OF, key: "_id" },
|
||||||
]
|
]
|
||||||
for (let [key, operation] of Object.entries(query)) {
|
for (let [key, operation] of Object.entries(query)) {
|
||||||
if (typeof operation !== "object") {
|
if (typeof operation !== "object") {
|
||||||
|
|
|
@ -4,3 +4,6 @@ export { generator } from "./structures"
|
||||||
export * as testContainerUtils from "./testContainerUtils"
|
export * as testContainerUtils from "./testContainerUtils"
|
||||||
export * as utils from "./utils"
|
export * as utils from "./utils"
|
||||||
export * from "./jestUtils"
|
export * from "./jestUtils"
|
||||||
|
import * as minio from "./minio"
|
||||||
|
|
||||||
|
export const objectStoreTestProviders = { minio }
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
|
||||||
|
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
||||||
|
import env from "../../../src/environment"
|
||||||
|
|
||||||
|
let container: StartedTestContainer | undefined
|
||||||
|
|
||||||
|
class ObjectStoreWaitStrategy extends AbstractWaitStrategy {
|
||||||
|
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
|
||||||
|
const logs = Wait.forListeningPorts()
|
||||||
|
await logs.waitUntilReady(container, boundPorts, startTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function start(): Promise<void> {
|
||||||
|
container = await new GenericContainer("minio/minio")
|
||||||
|
.withExposedPorts(9000)
|
||||||
|
.withCommand(["server", "/data"])
|
||||||
|
.withEnvironment({
|
||||||
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
|
MINIO_SECRET_KEY: "budibase",
|
||||||
|
})
|
||||||
|
.withWaitStrategy(new ObjectStoreWaitStrategy().withStartupTimeout(30000))
|
||||||
|
.start()
|
||||||
|
|
||||||
|
const port = container.getMappedPort(9000)
|
||||||
|
env._set("MINIO_URL", `http://0.0.0.0:${port}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stop() {
|
||||||
|
if (container) {
|
||||||
|
await container.stop()
|
||||||
|
container = undefined
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
|
DrawerContent,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||||
|
@ -31,13 +32,14 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||||
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
|
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
|
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
|
||||||
import {
|
import {
|
||||||
bindingsToCompletions,
|
bindingsToCompletions,
|
||||||
hbAutocomplete,
|
hbAutocomplete,
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "components/common/CodeEditor"
|
} from "components/common/CodeEditor"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
||||||
import {
|
import {
|
||||||
getSchemaForDatasourcePlus,
|
getSchemaForDatasourcePlus,
|
||||||
|
@ -355,7 +357,8 @@
|
||||||
value.customType !== "queryParams" &&
|
value.customType !== "queryParams" &&
|
||||||
value.customType !== "cron" &&
|
value.customType !== "cron" &&
|
||||||
value.customType !== "triggerSchema" &&
|
value.customType !== "triggerSchema" &&
|
||||||
value.customType !== "automationFields"
|
value.customType !== "automationFields" &&
|
||||||
|
value.type !== "attachment"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,6 +374,15 @@
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const handleAttachmentParams = keyValuObj => {
|
||||||
|
let params = {}
|
||||||
|
if (keyValuObj?.length) {
|
||||||
|
for (let param of keyValuObj) {
|
||||||
|
params[param.url] = param.filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
|
@ -436,21 +448,49 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
options={Object.keys(table?.schema || {})}
|
options={Object.keys(table?.schema || {})}
|
||||||
/>
|
/>
|
||||||
|
{:else if value.type === "attachment"}
|
||||||
|
<div class="attachment-field-wrapper">
|
||||||
|
<div class="label-wrapper">
|
||||||
|
<Label>{label}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="attachment-field-width">
|
||||||
|
<KeyValueBuilder
|
||||||
|
on:change={e =>
|
||||||
|
onChange(
|
||||||
|
{
|
||||||
|
detail: e.detail.map(({ name, value }) => ({
|
||||||
|
url: name,
|
||||||
|
filename: value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
key
|
||||||
|
)}
|
||||||
|
object={handleAttachmentParams(inputData[key])}
|
||||||
|
allowJS
|
||||||
|
{bindings}
|
||||||
|
keyBindings
|
||||||
|
customButtonText={"Add attachment"}
|
||||||
|
keyPlaceholder={"URL"}
|
||||||
|
valuePlaceholder={"Filename"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else if value.customType === "filters"}
|
{:else if value.customType === "filters"}
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||||
<Drawer bind:this={drawer} title="Filtering">
|
<Drawer bind:this={drawer} title="Filtering">
|
||||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<FilterDrawer
|
<DrawerContent slot="body">
|
||||||
slot="body"
|
<FilterBuilder
|
||||||
{filters}
|
{filters}
|
||||||
{bindings}
|
{bindings}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
datasource={{ type: "table", tableId }}
|
datasource={{ type: "table", tableId }}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
on:change={e => (tempFilters = e.detail)}
|
on:change={e => (tempFilters = e.detail)}
|
||||||
/>
|
/>
|
||||||
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
{:else if value.customType === "password"}
|
{:else if value.customType === "password"}
|
||||||
<Input
|
<Input
|
||||||
|
@ -649,14 +689,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-field {
|
.block-field {
|
||||||
display: flex; /* Use Flexbox */
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: row; /* Arrange label and field side by side */
|
flex-direction: row;
|
||||||
align-items: center; /* Align vertically in the center */
|
align-items: center;
|
||||||
gap: 10px; /* Add some space between label and field */
|
gap: 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment-field-width {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-wrapper {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.test :global(.drawer) {
|
.test :global(.drawer) {
|
||||||
width: 10000px !important;
|
width: 10000px !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let filters
|
export let filters
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
onConfirm={() => dispatch("change", tempValue)}
|
onConfirm={() => dispatch("change", tempValue)}
|
||||||
>
|
>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<FilterDrawer
|
<FilterBuilder
|
||||||
allowBindings={false}
|
allowBindings={false}
|
||||||
{filters}
|
{filters}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
Layout,
|
Layout,
|
||||||
AbsTooltip,
|
AbsTooltip,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
import { SWITCHABLE_TYPES, ValidColumnNameRegex } from "@budibase/shared-core"
|
||||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables, datasources } from "stores/builder"
|
import { tables, datasources } from "stores/builder"
|
||||||
|
@ -20,11 +21,6 @@
|
||||||
import {
|
import {
|
||||||
FIELDS,
|
FIELDS,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
ALLOWABLE_STRING_OPTIONS,
|
|
||||||
ALLOWABLE_NUMBER_OPTIONS,
|
|
||||||
ALLOWABLE_STRING_TYPES,
|
|
||||||
ALLOWABLE_NUMBER_TYPES,
|
|
||||||
SWITCHABLE_TYPES,
|
|
||||||
PrettyRelationshipDefinitions,
|
PrettyRelationshipDefinitions,
|
||||||
DB_TYPE_EXTERNAL,
|
DB_TYPE_EXTERNAL,
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
|
@ -33,21 +29,20 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import { getBindings } from "components/backend/DataTable/formula"
|
import { getBindings } from "components/backend/DataTable/formula"
|
||||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||||
import { ValidColumnNameRegex } from "@budibase/shared-core"
|
|
||||||
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
|
import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
|
||||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||||
import { RowUtils } from "@budibase/frontend-core"
|
import { RowUtils } from "@budibase/frontend-core"
|
||||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
const AUTO_TYPE = FIELDS.AUTO.type
|
const AUTO_TYPE = FieldType.AUTO
|
||||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
const FORMULA_TYPE = FieldType.FORMULA
|
||||||
const LINK_TYPE = FIELDS.LINK.type
|
const LINK_TYPE = FieldType.LINK
|
||||||
const STRING_TYPE = FIELDS.STRING.type
|
const STRING_TYPE = FieldType.STRING
|
||||||
const NUMBER_TYPE = FIELDS.NUMBER.type
|
const NUMBER_TYPE = FieldType.NUMBER
|
||||||
const JSON_TYPE = FIELDS.JSON.type
|
const JSON_TYPE = FieldType.JSON
|
||||||
const DATE_TYPE = FIELDS.DATETIME.type
|
const DATE_TYPE = FieldType.DATETIME
|
||||||
const USER_TYPE = FIELDS.USER.subtype
|
const USER_TYPE = FieldSubtype.USER
|
||||||
const USERS_TYPE = FIELDS.USERS.subtype
|
const USERS_TYPE = FieldSubtype.USERS
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
||||||
|
@ -61,8 +56,8 @@
|
||||||
let primaryDisplay
|
let primaryDisplay
|
||||||
let indexes = [...($tables.selected.indexes || [])]
|
let indexes = [...($tables.selected.indexes || [])]
|
||||||
let isCreating = undefined
|
let isCreating = undefined
|
||||||
let relationshipPart1 = PrettyRelationshipDefinitions.Many
|
let relationshipPart1 = PrettyRelationshipDefinitions.MANY
|
||||||
let relationshipPart2 = PrettyRelationshipDefinitions.One
|
let relationshipPart2 = PrettyRelationshipDefinitions.ONE
|
||||||
let relationshipTableIdPrimary = null
|
let relationshipTableIdPrimary = null
|
||||||
let relationshipTableIdSecondary = null
|
let relationshipTableIdSecondary = null
|
||||||
let table = $tables.selected
|
let table = $tables.selected
|
||||||
|
@ -175,7 +170,7 @@
|
||||||
$: typeEnabled =
|
$: typeEnabled =
|
||||||
!originalName ||
|
!originalName ||
|
||||||
(originalName &&
|
(originalName &&
|
||||||
SWITCHABLE_TYPES.indexOf(editableColumn.type) !== -1 &&
|
SWITCHABLE_TYPES[field.type] &&
|
||||||
!editableColumn?.autocolumn)
|
!editableColumn?.autocolumn)
|
||||||
|
|
||||||
const fieldDefinitions = Object.values(FIELDS).reduce(
|
const fieldDefinitions = Object.values(FIELDS).reduce(
|
||||||
|
@ -367,16 +362,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllowedTypes() {
|
function getAllowedTypes() {
|
||||||
if (
|
if (originalName) {
|
||||||
originalName &&
|
const possibleTypes = (
|
||||||
ALLOWABLE_STRING_TYPES.indexOf(editableColumn.type) !== -1
|
SWITCHABLE_TYPES[field.type] || [editableColumn.type]
|
||||||
) {
|
).map(t => t.toLowerCase())
|
||||||
return ALLOWABLE_STRING_OPTIONS
|
return Object.entries(FIELDS)
|
||||||
} else if (
|
.filter(([fieldType]) =>
|
||||||
originalName &&
|
possibleTypes.includes(fieldType.toLowerCase())
|
||||||
ALLOWABLE_NUMBER_TYPES.indexOf(editableColumn.type) !== -1
|
)
|
||||||
) {
|
.map(([_, fieldDefinition]) => fieldDefinition)
|
||||||
return ALLOWABLE_NUMBER_OPTIONS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUsers =
|
const isUsers =
|
||||||
|
@ -633,7 +627,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if editableColumn.type === FieldType.LINK}
|
{:else if editableColumn.type === FieldType.LINK && !editableColumn.autocolumn}
|
||||||
<RelationshipSelector
|
<RelationshipSelector
|
||||||
bind:relationshipPart1
|
bind:relationshipPart1
|
||||||
bind:relationshipPart2
|
bind:relationshipPart2
|
||||||
|
|
|
@ -27,14 +27,6 @@
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAttachments(fileList) {
|
|
||||||
try {
|
|
||||||
return await API.deleteBuilderAttachments(fileList)
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
|
@ -42,6 +34,5 @@
|
||||||
{label}
|
{label}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
"",
|
"",
|
||||||
requiredValidator
|
requiredValidator
|
||||||
)
|
)
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
||||||
"",
|
"",
|
||||||
requiredValidator
|
requiredValidator
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<script>
|
||||||
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||||
|
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
import { FilterBuilder } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
|
||||||
|
export let schemaFields
|
||||||
|
export let filters = []
|
||||||
|
export let bindings = []
|
||||||
|
export let panel = ClientBindingPanel
|
||||||
|
export let allowBindings = true
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let rawFilters
|
||||||
|
|
||||||
|
$: parseFilters(rawFilters)
|
||||||
|
$: dispatch("change", enrichFilters(rawFilters))
|
||||||
|
|
||||||
|
// Remove field key prefixes and determine which behaviours to use
|
||||||
|
const parseFilters = filters => {
|
||||||
|
rawFilters = (filters || []).map(filter => {
|
||||||
|
const { field } = filter
|
||||||
|
let newFilter = { ...filter }
|
||||||
|
delete newFilter.allOr
|
||||||
|
newFilter.field = dataFilters.removeKeyNumbering(field)
|
||||||
|
return newFilter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
parseFilters(filters)
|
||||||
|
rawFilters.forEach(filter => {
|
||||||
|
filter.type =
|
||||||
|
schemaFields.find(field => field.name === filter.field)?.type ||
|
||||||
|
filter.type
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add field key prefixes and a special metadata filter object to indicate
|
||||||
|
// how to handle filter behaviour
|
||||||
|
const enrichFilters = rawFilters => {
|
||||||
|
let count = 1
|
||||||
|
return rawFilters
|
||||||
|
.filter(filter => filter.field)
|
||||||
|
.map(filter => ({
|
||||||
|
...filter,
|
||||||
|
field: `${count++}:${filter.field}`,
|
||||||
|
}))
|
||||||
|
.concat(...rawFilters.filter(filter => !filter.field))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FilterBuilder
|
||||||
|
bind:filters={rawFilters}
|
||||||
|
behaviourFilters={true}
|
||||||
|
{schemaFields}
|
||||||
|
{datasource}
|
||||||
|
{allowBindings}
|
||||||
|
>
|
||||||
|
<div slot="filtering-hero-content" />
|
||||||
|
|
||||||
|
<DrawerBindableInput
|
||||||
|
let:filter
|
||||||
|
slot="binding"
|
||||||
|
disabled={filter.noValue}
|
||||||
|
title={filter.field}
|
||||||
|
value={filter.value}
|
||||||
|
placeholder="Value"
|
||||||
|
{panel}
|
||||||
|
{bindings}
|
||||||
|
on:change={event => {
|
||||||
|
const indexToUpdate = rawFilters.findIndex(f => f.id === filter.id)
|
||||||
|
rawFilters[indexToUpdate] = {
|
||||||
|
...rawFilters[indexToUpdate],
|
||||||
|
value: event.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FilterBuilder>
|
|
@ -1,8 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifications, ActionButton, Button, Drawer } from "@budibase/bbui"
|
import {
|
||||||
|
notifications,
|
||||||
|
ActionButton,
|
||||||
|
Button,
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||||
import FilterDrawer from "./FilterDrawer.svelte"
|
import FilterBuilder from "./FilterBuilder.svelte"
|
||||||
import { selectedScreen } from "stores/builder"
|
import { selectedScreen } from "stores/builder"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
@ -40,14 +46,15 @@
|
||||||
</div>
|
</div>
|
||||||
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
|
<Drawer bind:this={drawer} title="Filtering" on:drawerHide on:drawerShow>
|
||||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||||
<FilterDrawer
|
<DrawerContent slot="body">
|
||||||
slot="body"
|
<FilterBuilder
|
||||||
filters={value}
|
filters={value}
|
||||||
{bindings}
|
{bindings}
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
{datasource}
|
{datasource}
|
||||||
on:change={e => (tempValue = e.detail)}
|
on:change={e => (tempValue = e.detail)}
|
||||||
/>
|
/>
|
||||||
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
export let bindingDrawerLeft
|
export let bindingDrawerLeft
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
export let customButtonText = null
|
export let customButtonText = null
|
||||||
|
export let keyBindings = false
|
||||||
|
export let allowJS = false
|
||||||
export let compare = (option, value) => option === value
|
export let compare = (option, value) => option === value
|
||||||
|
|
||||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||||
|
@ -116,12 +118,23 @@
|
||||||
class:readOnly-menu={readOnly && showMenu}
|
class:readOnly-menu={readOnly && showMenu}
|
||||||
>
|
>
|
||||||
{#each fields as field, idx}
|
{#each fields as field, idx}
|
||||||
<Input
|
{#if keyBindings}
|
||||||
placeholder={keyPlaceholder}
|
<DrawerBindableInput
|
||||||
readonly={readOnly}
|
{bindings}
|
||||||
bind:value={field.name}
|
placeholder={keyPlaceholder}
|
||||||
on:blur={changed}
|
on:blur={e => {
|
||||||
/>
|
field.name = e.detail
|
||||||
|
changed()
|
||||||
|
}}
|
||||||
|
disabled={readOnly}
|
||||||
|
value={field.name}
|
||||||
|
{allowJS}
|
||||||
|
{allowHelpers}
|
||||||
|
drawerLeft={bindingDrawerLeft}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Input readonly={readOnly} bind:value={field.name} on:blur={changed} />
|
||||||
|
{/if}
|
||||||
{#if isJsonArray(field.value)}
|
{#if isJsonArray(field.value)}
|
||||||
<Select readonly={true} value="Array" options={["Array"]} />
|
<Select readonly={true} value="Array" options={["Array"]} />
|
||||||
{:else if options}
|
{:else if options}
|
||||||
|
@ -134,14 +147,14 @@
|
||||||
{:else if bindings && bindings.length}
|
{:else if bindings && bindings.length}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
{bindings}
|
{bindings}
|
||||||
placeholder="Value"
|
placeholder={valuePlaceholder}
|
||||||
on:blur={e => {
|
on:blur={e => {
|
||||||
field.value = e.detail
|
field.value = e.detail
|
||||||
changed()
|
changed()
|
||||||
}}
|
}}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
allowJS={false}
|
{allowJS}
|
||||||
{allowHelpers}
|
{allowHelpers}
|
||||||
drawerLeft={bindingDrawerLeft}
|
drawerLeft={bindingDrawerLeft}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -202,26 +202,6 @@ export const PrettyRelationshipDefinitions = {
|
||||||
ONE: "One row",
|
ONE: "One row",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ALLOWABLE_STRING_OPTIONS = [
|
|
||||||
FIELDS.STRING,
|
|
||||||
FIELDS.OPTIONS,
|
|
||||||
FIELDS.LONGFORM,
|
|
||||||
FIELDS.BARCODEQR,
|
|
||||||
]
|
|
||||||
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
|
|
||||||
opt => opt.type
|
|
||||||
)
|
|
||||||
|
|
||||||
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
|
|
||||||
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
|
||||||
opt => opt.type
|
|
||||||
)
|
|
||||||
|
|
||||||
export const SWITCHABLE_TYPES = [
|
|
||||||
...ALLOWABLE_STRING_TYPES,
|
|
||||||
...ALLOWABLE_NUMBER_TYPES,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const BUDIBASE_INTERNAL_DB_ID = INTERNAL_TABLE_SOURCE_ID
|
export const BUDIBASE_INTERNAL_DB_ID = INTERNAL_TABLE_SOURCE_ID
|
||||||
export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
|
export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
|
||||||
export const BUDIBASE_DATASOURCE_TYPE = "budibase"
|
export const BUDIBASE_DATASOURCE_TYPE = "budibase"
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
isJSBinding,
|
isJSBinding,
|
||||||
decodeJSBinding,
|
decodeJSBinding,
|
||||||
encodeJSBinding,
|
encodeJSBinding,
|
||||||
|
getJsHelperList,
|
||||||
} from "@budibase/string-templates"
|
} from "@budibase/string-templates"
|
||||||
import { TableNames } from "./constants"
|
import { TableNames } from "./constants"
|
||||||
import { JSONUtils, Constants } from "@budibase/frontend-core"
|
import { JSONUtils, Constants } from "@budibase/frontend-core"
|
||||||
|
@ -1210,9 +1211,32 @@ const shouldReplaceBinding = (currentValue, from, convertTo, binding) => {
|
||||||
if (!currentValue?.includes(from)) {
|
if (!currentValue?.includes(from)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (convertTo === "readableBinding") {
|
// some cases we have the same binding for readable/runtime, specific logic for this
|
||||||
// Dont replace if the value already matches the readable binding
|
const sameBindings = binding.runtimeBinding.includes(binding.readableBinding)
|
||||||
|
const convertingToReadable = convertTo === "readableBinding"
|
||||||
|
const helperNames = Object.keys(getJsHelperList())
|
||||||
|
const matchedHelperNames = helperNames.filter(
|
||||||
|
name => name.includes(from) && currentValue.includes(name)
|
||||||
|
)
|
||||||
|
// edge case - if the binding is part of a helper it may accidentally replace it
|
||||||
|
if (matchedHelperNames.length > 0) {
|
||||||
|
const indexStart = currentValue.indexOf(from),
|
||||||
|
indexEnd = indexStart + from.length
|
||||||
|
for (let helperName of matchedHelperNames) {
|
||||||
|
const helperIndexStart = currentValue.indexOf(helperName),
|
||||||
|
helperIndexEnd = helperIndexStart + helperName.length
|
||||||
|
if (indexStart >= helperIndexStart && indexEnd <= helperIndexEnd) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convertingToReadable && !sameBindings) {
|
||||||
|
// Don't replace if the value already matches the readable binding
|
||||||
return currentValue.indexOf(binding.readableBinding) === -1
|
return currentValue.indexOf(binding.readableBinding) === -1
|
||||||
|
} else if (convertingToReadable) {
|
||||||
|
// if the runtime and readable bindings are very similar we have to assume it should be replaced
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
|
// remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then
|
||||||
// this makes sure it is detected
|
// this makes sure it is detected
|
||||||
|
|
|
@ -4,8 +4,6 @@ import {
|
||||||
createDatasourceCreationStore,
|
createDatasourceCreationStore,
|
||||||
} from "./datasourceCreation"
|
} from "./datasourceCreation"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
import { shouldIntegrationFetchTableNames } from "stores/selectors"
|
|
||||||
|
|
||||||
vi.mock("stores/selectors", () => ({
|
vi.mock("stores/selectors", () => ({
|
||||||
shouldIntegrationFetchTableNames: vi.fn(),
|
shouldIntegrationFetchTableNames: vi.fn(),
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||||
import { createOnGoogleAuthStore } from "./onGoogleAuth"
|
import { createOnGoogleAuthStore } from "./onGoogleAuth"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line
|
||||||
import { params } from "@roxi/routify"
|
import { params } from "@roxi/routify"
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line
|
||||||
import { integrations } from "stores/builder"
|
import { integrations } from "stores/builder"
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
|
|
||||||
|
|
|
@ -189,6 +189,7 @@
|
||||||
<Select
|
<Select
|
||||||
options={settingOptions}
|
options={settingOptions}
|
||||||
bind:value={condition.setting}
|
bind:value={condition.setting}
|
||||||
|
on:change={() => delete condition.settingValue}
|
||||||
/>
|
/>
|
||||||
<div>TO</div>
|
<div>TO</div>
|
||||||
{#if definition}
|
{#if definition}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { FieldType } from "@budibase/types"
|
import { FieldType } from "@budibase/types"
|
||||||
|
import { SWITCHABLE_TYPES } from "@budibase/shared-core"
|
||||||
import { get, writable, derived } from "svelte/store"
|
import { get, writable, derived } from "svelte/store"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { SWITCHABLE_TYPES } from "constants/backend"
|
|
||||||
|
|
||||||
export function createTablesStore() {
|
export function createTablesStore() {
|
||||||
const store = writable({
|
const store = writable({
|
||||||
|
@ -64,7 +64,7 @@ export function createTablesStore() {
|
||||||
if (
|
if (
|
||||||
oldField != null &&
|
oldField != null &&
|
||||||
oldField?.type !== field.type &&
|
oldField?.type !== field.type &&
|
||||||
SWITCHABLE_TYPES.indexOf(oldField?.type) === -1
|
!SWITCHABLE_TYPES[oldField?.type]?.includes(field.type)
|
||||||
) {
|
) {
|
||||||
updatedTable.schema[key] = oldField
|
updatedTable.schema[key] = oldField
|
||||||
}
|
}
|
||||||
|
@ -148,12 +148,6 @@ export function createTablesStore() {
|
||||||
if (indexes) {
|
if (indexes) {
|
||||||
draft.indexes = indexes
|
draft.indexes = indexes
|
||||||
}
|
}
|
||||||
// Add object to indicate if column is being added
|
|
||||||
if (draft.schema[field.name] === undefined) {
|
|
||||||
draft._add = {
|
|
||||||
name: field.name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
draft.schema = {
|
draft.schema = {
|
||||||
...draft.schema,
|
...draft.schema,
|
||||||
[field.name]: cloneDeep(field),
|
[field.name]: cloneDeep(field),
|
||||||
|
|
|
@ -9,7 +9,7 @@ const {
|
||||||
ObjectStore,
|
ObjectStore,
|
||||||
retrieve,
|
retrieve,
|
||||||
uploadDirectory,
|
uploadDirectory,
|
||||||
makeSureBucketExists,
|
createBucketIfNotExists,
|
||||||
} = objectStore
|
} = objectStore
|
||||||
|
|
||||||
const bucketList = Object.values(ObjectStoreBuckets)
|
const bucketList = Object.values(ObjectStoreBuckets)
|
||||||
|
@ -61,7 +61,7 @@ export async function importObjects() {
|
||||||
let count = 0
|
let count = 0
|
||||||
for (let bucket of buckets) {
|
for (let bucket of buckets) {
|
||||||
const client = ObjectStore(bucket)
|
const client = ObjectStore(bucket)
|
||||||
await makeSureBucketExists(client, bucket)
|
await createBucketIfNotExists(client, bucket)
|
||||||
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
const files = await uploadDirectory(bucket, join(path, bucket), "/")
|
||||||
count += files.length
|
count += files.length
|
||||||
bar.update(count)
|
bar.update(count)
|
||||||
|
|
|
@ -54,11 +54,9 @@ export async function downloadDockerCompose() {
|
||||||
|
|
||||||
export async function checkDockerConfigured() {
|
export async function checkDockerConfigured() {
|
||||||
const error =
|
const error =
|
||||||
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
"docker has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
||||||
const docker = await lookpath("docker")
|
const docker = await lookpath("docker")
|
||||||
const compose = await lookpath("docker-compose")
|
if (!docker) {
|
||||||
const composeV2 = await lookpath("docker compose")
|
|
||||||
if (!docker || (!compose && !composeV2)) {
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +103,6 @@ export function getAppService(path: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateDockerComposeService(
|
export function updateDockerComposeService(
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
updateFn: (service: DockerCompose) => void
|
updateFn: (service: DockerCompose) => void
|
||||||
) {
|
) {
|
||||||
const opts = ["docker-compose.yaml", "docker-compose.yml"]
|
const opts = ["docker-compose.yaml", "docker-compose.yml"]
|
||||||
|
|
|
@ -6967,6 +6967,12 @@
|
||||||
"key": "stripeRows",
|
"key": "stripeRows",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Quiet",
|
||||||
|
"key": "quiet",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": true,
|
"section": true,
|
||||||
"name": "Columns",
|
"name": "Columns",
|
||||||
|
|
|
@ -119,140 +119,142 @@
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div
|
{#if dataLoaded}
|
||||||
id="spectrum-root"
|
<div
|
||||||
lang="en"
|
id="spectrum-root"
|
||||||
dir="ltr"
|
lang="en"
|
||||||
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
dir="ltr"
|
||||||
class:builder={$builderStore.inBuilder}
|
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
|
||||||
class:show={fontsLoaded && dataLoaded}
|
class:builder={$builderStore.inBuilder}
|
||||||
>
|
class:show={fontsLoaded && dataLoaded}
|
||||||
{#if $environmentStore.maintenance.length > 0}
|
>
|
||||||
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
|
{#if $environmentStore.maintenance.length > 0}
|
||||||
{:else}
|
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
|
||||||
<DeviceBindingsProvider>
|
{:else}
|
||||||
<UserBindingsProvider>
|
<DeviceBindingsProvider>
|
||||||
<StateBindingsProvider>
|
<UserBindingsProvider>
|
||||||
<RowSelectionProvider>
|
<StateBindingsProvider>
|
||||||
<QueryParamsProvider>
|
<RowSelectionProvider>
|
||||||
<SnippetsProvider>
|
<QueryParamsProvider>
|
||||||
<!-- Settings bar can be rendered outside of device preview -->
|
<SnippetsProvider>
|
||||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
<!-- Settings bar can be rendered outside of device preview -->
|
||||||
{#key $builderStore.selectedComponentId}
|
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||||
{#if $builderStore.inBuilder}
|
{#key $builderStore.selectedComponentId}
|
||||||
<SettingsBar />
|
{#if $builderStore.inBuilder}
|
||||||
{/if}
|
<SettingsBar />
|
||||||
{/key}
|
|
||||||
|
|
||||||
<!-- Clip boundary for selection indicators -->
|
|
||||||
<div
|
|
||||||
id="clip-root"
|
|
||||||
class:preview={$builderStore.inBuilder}
|
|
||||||
class:tablet-preview={$builderStore.previewDevice ===
|
|
||||||
"tablet"}
|
|
||||||
class:mobile-preview={$builderStore.previewDevice ===
|
|
||||||
"mobile"}
|
|
||||||
>
|
|
||||||
<!-- Actual app -->
|
|
||||||
<div id="app-root">
|
|
||||||
{#if showDevTools}
|
|
||||||
<DevToolsHeader />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|
||||||
<div id="app-body">
|
<!-- Clip boundary for selection indicators -->
|
||||||
{#if permissionError}
|
<div
|
||||||
<div class="error">
|
id="clip-root"
|
||||||
<Layout justifyItems="center" gap="S">
|
class:preview={$builderStore.inBuilder}
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
class:tablet-preview={$builderStore.previewDevice ===
|
||||||
{@html ErrorSVG}
|
"tablet"}
|
||||||
<Heading size="L">
|
class:mobile-preview={$builderStore.previewDevice ===
|
||||||
You don't have permission to use this app
|
"mobile"}
|
||||||
</Heading>
|
>
|
||||||
<Body size="S">
|
<!-- Actual app -->
|
||||||
Ask your administrator to grant you access
|
<div id="app-root">
|
||||||
</Body>
|
{#if showDevTools}
|
||||||
</Layout>
|
<DevToolsHeader />
|
||||||
</div>
|
{/if}
|
||||||
{:else if !$screenStore.activeLayout}
|
|
||||||
<div class="error">
|
|
||||||
<Layout justifyItems="center" gap="S">
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
||||||
{@html ErrorSVG}
|
|
||||||
<Heading size="L">
|
|
||||||
Something went wrong rendering your app
|
|
||||||
</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
Get in touch with support if this issue persists
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{:else if embedNoScreens}
|
|
||||||
<div class="error">
|
|
||||||
<Layout justifyItems="center" gap="S">
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
||||||
{@html ErrorSVG}
|
|
||||||
<Heading size="L">
|
|
||||||
This Budibase app is not publicly accessible
|
|
||||||
</Heading>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<CustomThemeWrapper>
|
|
||||||
{#key $screenStore.activeLayout._id}
|
|
||||||
<Component
|
|
||||||
isLayout
|
|
||||||
instance={$screenStore.activeLayout.props}
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
<!--
|
<div id="app-body">
|
||||||
|
{#if permissionError}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
You don't have permission to use this app
|
||||||
|
</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Ask your administrator to grant you access
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else if !$screenStore.activeLayout}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
Something went wrong rendering your app
|
||||||
|
</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
Get in touch with support if this issue persists
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else if embedNoScreens}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
This Budibase app is not publicly accessible
|
||||||
|
</Heading>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<CustomThemeWrapper>
|
||||||
|
{#key $screenStore.activeLayout._id}
|
||||||
|
<Component
|
||||||
|
isLayout
|
||||||
|
instance={$screenStore.activeLayout.props}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<!--
|
||||||
Flatpickr needs to be inside the theme wrapper.
|
Flatpickr needs to be inside the theme wrapper.
|
||||||
It also needs its own container because otherwise it hijacks
|
It also needs its own container because otherwise it hijacks
|
||||||
key events on the whole page. It is painful to work with.
|
key events on the whole page. It is painful to work with.
|
||||||
-->
|
-->
|
||||||
<div id="flatpickr-root" />
|
<div id="flatpickr-root" />
|
||||||
|
|
||||||
<!-- Modal container to ensure they sit on top -->
|
<!-- Modal container to ensure they sit on top -->
|
||||||
<div class="modal-container" />
|
<div class="modal-container" />
|
||||||
|
|
||||||
<!-- Layers on top of app -->
|
<!-- Layers on top of app -->
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
<ConfirmationDisplay />
|
<ConfirmationDisplay />
|
||||||
<PeekScreenDisplay />
|
<PeekScreenDisplay />
|
||||||
</CustomThemeWrapper>
|
</CustomThemeWrapper>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showDevTools}
|
{#if showDevTools}
|
||||||
<DevTools />
|
<DevTools />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
|
||||||
|
<FreeFooter />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
|
<!-- Preview and dev tools utilities -->
|
||||||
<FreeFooter />
|
{#if $appStore.isDevApp}
|
||||||
|
<SelectionIndicator />
|
||||||
|
{/if}
|
||||||
|
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
|
||||||
|
<HoverIndicator />
|
||||||
|
{/if}
|
||||||
|
{#if $builderStore.inBuilder}
|
||||||
|
<DNDHandler />
|
||||||
|
<GridDNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</SnippetsProvider>
|
||||||
<!-- Preview and dev tools utilities -->
|
</QueryParamsProvider>
|
||||||
{#if $appStore.isDevApp}
|
</RowSelectionProvider>
|
||||||
<SelectionIndicator />
|
</StateBindingsProvider>
|
||||||
{/if}
|
</UserBindingsProvider>
|
||||||
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
|
</DeviceBindingsProvider>
|
||||||
<HoverIndicator />
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
{#if $builderStore.inBuilder}
|
<KeyboardManager />
|
||||||
<DNDHandler />
|
{/if}
|
||||||
<GridDNDHandler />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</SnippetsProvider>
|
|
||||||
</QueryParamsProvider>
|
|
||||||
</RowSelectionProvider>
|
|
||||||
</StateBindingsProvider>
|
|
||||||
</UserBindingsProvider>
|
|
||||||
</DeviceBindingsProvider>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<KeyboardManager />
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#spectrum-root {
|
#spectrum-root {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let allowEditRows = true
|
export let allowEditRows = true
|
||||||
export let allowDeleteRows = true
|
export let allowDeleteRows = true
|
||||||
export let stripeRows = false
|
export let stripeRows = false
|
||||||
|
export let quiet = false
|
||||||
export let initialFilter = null
|
export let initialFilter = null
|
||||||
export let initialSortColumn = null
|
export let initialSortColumn = null
|
||||||
export let initialSortOrder = null
|
export let initialSortOrder = null
|
||||||
|
@ -117,6 +118,7 @@
|
||||||
datasource={table}
|
datasource={table}
|
||||||
{API}
|
{API}
|
||||||
{stripeRows}
|
{stripeRows}
|
||||||
|
{quiet}
|
||||||
{initialFilter}
|
{initialFilter}
|
||||||
{initialSortColumn}
|
{initialSortColumn}
|
||||||
{initialSortOrder}
|
{initialSortOrder}
|
||||||
|
|
|
@ -157,6 +157,11 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Use normal theme colors for links when using a top nav */
|
||||||
|
.dropdown:not(.left) .sublinks a {
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* Left dropdowns */
|
/* Left dropdowns */
|
||||||
.dropdown.left .sublinks-wrapper {
|
.dropdown.left .sublinks-wrapper {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -1,216 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { FilterBuilder } from "@budibase/frontend-core"
|
||||||
Body,
|
|
||||||
Button,
|
|
||||||
Combobox,
|
|
||||||
DatePicker,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Layout,
|
|
||||||
Select,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { generate } from "shortid"
|
|
||||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let datasource
|
export let datasource
|
||||||
|
|
||||||
const context = getContext("context")
|
|
||||||
const BannedTypes = ["link", "attachment", "json"]
|
|
||||||
|
|
||||||
$: fieldOptions = (schemaFields ?? [])
|
|
||||||
.filter(
|
|
||||||
field =>
|
|
||||||
!BannedTypes.includes(field.type) ||
|
|
||||||
(field.type === "formula" && field.formulaType === "static")
|
|
||||||
)
|
|
||||||
.map(field => ({
|
|
||||||
label: field.displayName || field.name,
|
|
||||||
value: field.name,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const addFilter = () => {
|
|
||||||
filters = [
|
|
||||||
...filters,
|
|
||||||
{
|
|
||||||
id: generate(),
|
|
||||||
field: null,
|
|
||||||
operator: Constants.OperatorOptions.Equals.value,
|
|
||||||
value: null,
|
|
||||||
valueType: "Value",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeFilter = id => {
|
|
||||||
filters = filters.filter(field => field.id !== id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicateFilter = id => {
|
|
||||||
const existingFilter = filters.find(filter => filter.id === id)
|
|
||||||
const duplicate = { ...existingFilter, id: generate() }
|
|
||||||
filters = [...filters, duplicate]
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFieldChange = (expression, field) => {
|
|
||||||
// Update the field type
|
|
||||||
expression.type = schemaFields.find(x => x.name === field)?.type
|
|
||||||
expression.externalType = schemaFields.find(
|
|
||||||
x => x.name === field
|
|
||||||
)?.externalType
|
|
||||||
|
|
||||||
// Ensure a valid operator is set
|
|
||||||
const validOperators = LuceneUtils.getValidOperatorsForType(
|
|
||||||
{ type: expression.type },
|
|
||||||
expression.field,
|
|
||||||
datasource
|
|
||||||
).map(x => x.value)
|
|
||||||
if (!validOperators.includes(expression.operator)) {
|
|
||||||
expression.operator =
|
|
||||||
validOperators[0] ?? Constants.OperatorOptions.Equals.value
|
|
||||||
onOperatorChange(expression, expression.operator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if changed to an array, change default value to empty array
|
|
||||||
const idx = filters.findIndex(x => x.field === field)
|
|
||||||
if (expression.type === "array") {
|
|
||||||
filters[idx].value = []
|
|
||||||
} else {
|
|
||||||
filters[idx].value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOperatorChange = (expression, operator) => {
|
|
||||||
const noValueOptions = [
|
|
||||||
Constants.OperatorOptions.Empty.value,
|
|
||||||
Constants.OperatorOptions.NotEmpty.value,
|
|
||||||
]
|
|
||||||
expression.noValue = noValueOptions.includes(operator)
|
|
||||||
if (expression.noValue) {
|
|
||||||
expression.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
|
||||||
const schema = schemaFields.find(x => x.name === field)
|
|
||||||
return schema?.constraints?.inclusion || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSchema = filter => {
|
|
||||||
return schemaFields.find(field => field.name === filter.field)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container" class:mobile={$context.device.mobile}>
|
<FilterBuilder bind:filters {schemaFields} {datasource} filtersLabel={null}>
|
||||||
<Layout noPadding>
|
<div slot="filtering-hero-content">
|
||||||
<Body size="S">
|
Results are filtered to only those which match all of the following
|
||||||
{#if !filters?.length}
|
constraints.
|
||||||
Add your first filter expression.
|
</div>
|
||||||
{:else}
|
</FilterBuilder>
|
||||||
Results are filtered to only those which match all of the following
|
|
||||||
constraints.
|
|
||||||
{/if}
|
|
||||||
</Body>
|
|
||||||
{#if filters?.length}
|
|
||||||
<div class="fields">
|
|
||||||
{#each filters as filter}
|
|
||||||
<Select
|
|
||||||
bind:value={filter.field}
|
|
||||||
options={fieldOptions}
|
|
||||||
on:change={e => onFieldChange(filter, e.detail)}
|
|
||||||
placeholder="Column"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
disabled={!filter.field}
|
|
||||||
options={LuceneUtils.getValidOperatorsForType(
|
|
||||||
{ type: filter.type, subtype: filter.subtype },
|
|
||||||
filter.field,
|
|
||||||
datasource
|
|
||||||
)}
|
|
||||||
bind:value={filter.operator}
|
|
||||||
on:change={e => onOperatorChange(filter, e.detail)}
|
|
||||||
placeholder={null}
|
|
||||||
/>
|
|
||||||
{#if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
|
||||||
{:else if ["options", "array"].includes(filter.type)}
|
|
||||||
<Combobox
|
|
||||||
disabled={filter.noValue}
|
|
||||||
options={getFieldOptions(filter.field)}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else if filter.type === "boolean"}
|
|
||||||
<Combobox
|
|
||||||
disabled={filter.noValue}
|
|
||||||
options={[
|
|
||||||
{ label: "True", value: "true" },
|
|
||||||
{ label: "False", value: "false" },
|
|
||||||
]}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else if filter.type === "datetime"}
|
|
||||||
<DatePicker
|
|
||||||
disabled={filter.noValue}
|
|
||||||
enableTime={!getSchema(filter).dateOnly}
|
|
||||||
timeOnly={getSchema(filter).timeOnly}
|
|
||||||
bind:value={filter.value}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Input disabled />
|
|
||||||
{/if}
|
|
||||||
<div class="controls">
|
|
||||||
<Icon
|
|
||||||
name="Duplicate"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => duplicateFilter(filter.id)}
|
|
||||||
/>
|
|
||||||
<Icon
|
|
||||||
name="Close"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => removeFilter(filter.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div>
|
|
||||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
|
||||||
Add filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1000px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.fields {
|
|
||||||
display: grid;
|
|
||||||
column-gap: var(--spacing-l);
|
|
||||||
row-gap: var(--spacing-s);
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: 1fr 120px 1fr auto auto;
|
|
||||||
}
|
|
||||||
.controls {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container.mobile .fields {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
.container.mobile .controls {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--spacing-s) 0;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -58,17 +58,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAttachments = async fileList => {
|
|
||||||
try {
|
|
||||||
return await API.deleteAttachments({
|
|
||||||
keys: fileList,
|
|
||||||
tableId: formContext?.dataSource?.tableId,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = e => {
|
const handleChange = e => {
|
||||||
const value = fieldApiMapper.set(e.detail)
|
const value = fieldApiMapper.set(e.detail)
|
||||||
const changed = fieldApi.setValue(value)
|
const changed = fieldApi.setValue(value)
|
||||||
|
@ -98,7 +87,6 @@
|
||||||
error={fieldState.error}
|
error={fieldState.error}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
{handleTooManyFiles}
|
{handleTooManyFiles}
|
||||||
{maximum}
|
{maximum}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@budibase/types": "0.0.0",
|
"@budibase/types": "0.0.0",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"shortid": "2.2.15",
|
||||||
"socket.io-client": "^4.6.1"
|
"socket.io-client": "^4.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,34 +61,6 @@ export const buildAttachmentEndpoints = API => {
|
||||||
})
|
})
|
||||||
return { publicUrl }
|
return { publicUrl }
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes attachments from the bucket.
|
|
||||||
* @param keys the attachments to delete
|
|
||||||
* @param tableId the associated table ID
|
|
||||||
*/
|
|
||||||
deleteAttachments: async ({ keys, tableId }) => {
|
|
||||||
return await API.post({
|
|
||||||
url: `/api/attachments/${tableId}/delete`,
|
|
||||||
body: {
|
|
||||||
keys,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes attachments from the builder bucket.
|
|
||||||
* @param keys the attachments to delete
|
|
||||||
*/
|
|
||||||
deleteBuilderAttachments: async keys => {
|
|
||||||
return await API.post({
|
|
||||||
url: `/api/attachments/delete`,
|
|
||||||
body: {
|
|
||||||
keys,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download an attachment from a row given its column name.
|
* Download an attachment from a row given its column name.
|
||||||
* @param datasourceId the ID of the datasource to download from
|
* @param datasourceId the ID of the datasource to download from
|
||||||
|
|
|
@ -4,33 +4,36 @@
|
||||||
Button,
|
Button,
|
||||||
Combobox,
|
Combobox,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
DrawerContent,
|
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
|
||||||
Layout,
|
Layout,
|
||||||
Multiselect,
|
|
||||||
Select,
|
Select,
|
||||||
|
Label,
|
||||||
|
Multiselect,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import { FieldType, SearchFilterOperator } from "@budibase/types"
|
||||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||||
import { getFields } from "helpers/searchFields"
|
import { getContext } from "svelte"
|
||||||
import { FieldType } from "@budibase/types"
|
|
||||||
import { createEventDispatcher, onMount } from "svelte"
|
|
||||||
import FilterUsers from "./FilterUsers.svelte"
|
import FilterUsers from "./FilterUsers.svelte"
|
||||||
|
|
||||||
|
const { OperatorOptions } = Constants
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let bindings = []
|
|
||||||
export let panel = ClientBindingPanel
|
|
||||||
export let allowBindings = true
|
|
||||||
export let datasource
|
export let datasource
|
||||||
|
export let behaviourFilters = false
|
||||||
|
export let allowBindings = false
|
||||||
|
export let filtersLabel = "Filters"
|
||||||
|
|
||||||
|
$: matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
||||||
|
$: onEmptyFilter =
|
||||||
|
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
||||||
|
|
||||||
|
$: fieldFilters = filters.filter(
|
||||||
|
filter => filter.operator !== "allOr" && !filter.onEmptyFilter
|
||||||
|
)
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
const { OperatorOptions } = Constants
|
|
||||||
const KeyedFieldRegex = /\d[0-9]*:/g
|
|
||||||
const behaviourOptions = [
|
const behaviourOptions = [
|
||||||
{ value: "and", label: "Match all filters" },
|
{ value: "and", label: "Match all filters" },
|
||||||
{ value: "or", label: "Match any filter" },
|
{ value: "or", label: "Match any filter" },
|
||||||
|
@ -40,62 +43,18 @@
|
||||||
{ value: "none", label: "Return no rows" },
|
{ value: "none", label: "Return no rows" },
|
||||||
]
|
]
|
||||||
|
|
||||||
let rawFilters
|
const context = getContext("context")
|
||||||
let matchAny = false
|
|
||||||
let onEmptyFilter = "all"
|
|
||||||
|
|
||||||
$: parseFilters(filters)
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
|
.filter(field => getValidOperatorsForType(field).length)
|
||||||
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
.map(field => ({
|
||||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
label: field.displayName || field.name,
|
||||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
value: field.name,
|
||||||
|
}))
|
||||||
// Remove field key prefixes and determine which behaviours to use
|
|
||||||
const parseFilters = filters => {
|
|
||||||
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
|
||||||
onEmptyFilter =
|
|
||||||
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
|
||||||
rawFilters = (filters || [])
|
|
||||||
.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
|
|
||||||
.map(filter => {
|
|
||||||
const { field } = filter
|
|
||||||
let newFilter = { ...filter }
|
|
||||||
delete newFilter.allOr
|
|
||||||
if (typeof field === "string" && field.match(KeyedFieldRegex) != null) {
|
|
||||||
const parts = field.split(":")
|
|
||||||
parts.shift()
|
|
||||||
newFilter.field = parts.join(":")
|
|
||||||
}
|
|
||||||
return newFilter
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
parseFilters(filters)
|
|
||||||
rawFilters.forEach(filter => {
|
|
||||||
filter.type =
|
|
||||||
schemaFields.find(field => field.name === filter.field)?.type ||
|
|
||||||
filter.type
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add field key prefixes and a special metadata filter object to indicate
|
|
||||||
// how to handle filter behaviour
|
|
||||||
const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
|
|
||||||
let count = 1
|
|
||||||
return rawFilters
|
|
||||||
.filter(filter => filter.field)
|
|
||||||
.map(filter => ({
|
|
||||||
...filter,
|
|
||||||
field: `${count++}:${filter.field}`,
|
|
||||||
}))
|
|
||||||
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
|
||||||
.concat([{ onEmptyFilter }])
|
|
||||||
}
|
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
rawFilters = [
|
filters = [
|
||||||
...rawFilters,
|
...(filters || []),
|
||||||
{
|
{
|
||||||
id: generate(),
|
id: generate(),
|
||||||
field: null,
|
field: null,
|
||||||
|
@ -107,22 +66,62 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFilter = id => {
|
const removeFilter = id => {
|
||||||
rawFilters = rawFilters.filter(field => field.id !== id)
|
filters = filters.filter(field => field.id !== id)
|
||||||
|
|
||||||
|
// Clear all filters when no fields are specified
|
||||||
|
if (filters.length === 1 && filters[0].onEmptyFilter) {
|
||||||
|
filters = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicateFilter = id => {
|
const duplicateFilter = id => {
|
||||||
const existingFilter = rawFilters.find(filter => filter.id === id)
|
const existingFilter = filters.find(filter => filter.id === id)
|
||||||
const duplicate = { ...existingFilter, id: generate() }
|
const duplicate = { ...existingFilter, id: generate() }
|
||||||
rawFilters = [...rawFilters, duplicate]
|
filters = [...filters, duplicate]
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFieldChange = filter => {
|
||||||
|
const previousType = filter.type
|
||||||
|
sanitizeTypes(filter)
|
||||||
|
sanitizeOperator(filter)
|
||||||
|
sanitizeValue(filter, previousType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOperatorChange = filter => {
|
||||||
|
sanitizeOperator(filter)
|
||||||
|
sanitizeValue(filter, filter.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onValueTypeChange = filter => {
|
||||||
|
sanitizeValue(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFieldOptions = field => {
|
||||||
|
const schema = schemaFields.find(x => x.name === field)
|
||||||
|
return schema?.constraints?.inclusion || []
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSchema = filter => {
|
const getSchema = filter => {
|
||||||
return enrichedSchemaFields.find(field => field.name === filter.field)
|
return schemaFields.find(field => field.name === filter.field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getValidOperatorsForType = filter => {
|
||||||
|
if (!filter?.field && !filter?.name) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return LuceneUtils.getValidOperatorsForType(
|
||||||
|
filter,
|
||||||
|
filter.field || filter.name,
|
||||||
|
datasource
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||||
|
|
||||||
const sanitizeTypes = filter => {
|
const sanitizeTypes = filter => {
|
||||||
// Update type based on field
|
// Update type based on field
|
||||||
const fieldSchema = enrichedSchemaFields.find(x => x.name === filter.field)
|
const fieldSchema = schemaFields.find(x => x.name === filter.field)
|
||||||
filter.type = fieldSchema?.type
|
filter.type = fieldSchema?.type
|
||||||
filter.subtype = fieldSchema?.subtype
|
filter.subtype = fieldSchema?.subtype
|
||||||
|
|
||||||
|
@ -154,88 +153,79 @@
|
||||||
|
|
||||||
// Ensure array values are properly set and cleared
|
// Ensure array values are properly set and cleared
|
||||||
if (Array.isArray(filter.value)) {
|
if (Array.isArray(filter.value)) {
|
||||||
if (filter.valueType !== "Value" || filter.type !== "array") {
|
if (filter.valueType !== "Value" || filter.type !== FieldType.ARRAY) {
|
||||||
filter.value = null
|
filter.value = null
|
||||||
}
|
}
|
||||||
} else if (filter.type === "array" && filter.valueType === "Value") {
|
} else if (
|
||||||
|
filter.type === FieldType.ARRAY &&
|
||||||
|
filter.valueType === "Value"
|
||||||
|
) {
|
||||||
filter.value = []
|
filter.value = []
|
||||||
} else if (
|
} else if (
|
||||||
previousType !== filter.type &&
|
previousType !== filter.type &&
|
||||||
(previousType === FieldType.BB_REFERENCE ||
|
(previousType === FieldType.BB_REFERENCE ||
|
||||||
filter.type === FieldType.BB_REFERENCE)
|
filter.type === FieldType.BB_REFERENCE)
|
||||||
) {
|
) {
|
||||||
filter.value = filter.type === "array" ? [] : null
|
filter.value = filter.type === FieldType.ARRAY ? [] : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFieldChange = filter => {
|
function handleAllOr(option) {
|
||||||
const previousType = filter.type
|
filters = filters.filter(f => f.operator !== "allOr")
|
||||||
sanitizeTypes(filter)
|
if (option === "or") {
|
||||||
sanitizeOperator(filter)
|
filters.push({ operator: "allOr" })
|
||||||
sanitizeValue(filter, previousType)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOperatorChange = filter => {
|
|
||||||
sanitizeOperator(filter)
|
|
||||||
sanitizeValue(filter, filter.type)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onValueTypeChange = filter => {
|
|
||||||
sanitizeValue(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFieldOptions = field => {
|
|
||||||
const schema = enrichedSchemaFields.find(x => x.name === field)
|
|
||||||
return schema?.constraints?.inclusion || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const getValidOperatorsForType = filter => {
|
|
||||||
if (!filter?.field) {
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return LuceneUtils.getValidOperatorsForType(
|
function handleOnEmptyFilter(value) {
|
||||||
{ type: filter.type, subtype: filter.subtype },
|
filters = filters?.filter(filter => !filter.onEmptyFilter)
|
||||||
filter.field,
|
filters.push({ onEmptyFilter: value })
|
||||||
datasource
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DrawerContent>
|
<div class="container" class:mobile={$context?.device?.mobile}>
|
||||||
<div class="container">
|
<Layout noPadding>
|
||||||
<Layout noPadding>
|
{#if fieldOptions?.length}
|
||||||
{#if !rawFilters?.length}
|
<Body size="S">
|
||||||
<Body size="S">Add your first filter expression.</Body>
|
{#if !fieldFilters?.length}
|
||||||
{:else}
|
Add your first filter expression.
|
||||||
<div class="fields">
|
{:else}
|
||||||
<Select
|
<slot name="filtering-hero-content" />
|
||||||
label="Behaviour"
|
{#if behaviourFilters}
|
||||||
value={matchAny ? "or" : "and"}
|
<div class="behaviour-filters">
|
||||||
options={behaviourOptions}
|
<Select
|
||||||
getOptionLabel={opt => opt.label}
|
label="Behaviour"
|
||||||
getOptionValue={opt => opt.value}
|
value={matchAny ? "or" : "and"}
|
||||||
on:change={e => (matchAny = e.detail === "or")}
|
options={behaviourOptions}
|
||||||
placeholder={null}
|
getOptionLabel={opt => opt.label}
|
||||||
/>
|
getOptionValue={opt => opt.value}
|
||||||
{#if datasource?.type === "table"}
|
on:change={e => handleAllOr(e.detail)}
|
||||||
<Select
|
placeholder={null}
|
||||||
label="When filter empty"
|
/>
|
||||||
value={onEmptyFilter}
|
{#if datasource?.type === "table"}
|
||||||
options={onEmptyOptions}
|
<Select
|
||||||
getOptionLabel={opt => opt.label}
|
label="When filter empty"
|
||||||
getOptionValue={opt => opt.value}
|
value={onEmptyFilter}
|
||||||
on:change={e => (onEmptyFilter = e.detail)}
|
options={onEmptyOptions}
|
||||||
placeholder={null}
|
getOptionLabel={opt => opt.label}
|
||||||
/>
|
getOptionValue={opt => opt.value}
|
||||||
|
on:change={e => handleOnEmptyFilter(e.detail)}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/if}
|
||||||
|
</Body>
|
||||||
|
{#if fieldFilters?.length}
|
||||||
<div>
|
<div>
|
||||||
<div class="filter-label">
|
{#if filtersLabel}
|
||||||
<Label>Filters</Label>
|
<div class="filter-label">
|
||||||
</div>
|
<Label>{filtersLabel}</Label>
|
||||||
<div class="fields">
|
</div>
|
||||||
{#each rawFilters as filter}
|
{/if}
|
||||||
|
<div class="fields" class:with-bindings={allowBindings}>
|
||||||
|
{#each fieldFilters as filter}
|
||||||
<Select
|
<Select
|
||||||
bind:value={filter.field}
|
bind:value={filter.field}
|
||||||
options={fieldOptions}
|
options={fieldOptions}
|
||||||
|
@ -249,38 +239,32 @@
|
||||||
on:change={() => onOperatorChange(filter)}
|
on:change={() => onOperatorChange(filter)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
<Select
|
{#if allowBindings}
|
||||||
disabled={filter.noValue || !filter.field}
|
<Select
|
||||||
options={valueTypeOptions}
|
disabled={filter.noValue || !filter.field}
|
||||||
bind:value={filter.valueType}
|
options={valueTypeOptions}
|
||||||
on:change={() => onValueTypeChange(filter)}
|
bind:value={filter.valueType}
|
||||||
placeholder={null}
|
on:change={() => onValueTypeChange(filter)}
|
||||||
/>
|
placeholder={null}
|
||||||
{#if filter.field && filter.valueType === "Binding"}
|
|
||||||
<DrawerBindableInput
|
|
||||||
disabled={filter.noValue}
|
|
||||||
title={filter.field}
|
|
||||||
value={filter.value}
|
|
||||||
placeholder="Value"
|
|
||||||
{panel}
|
|
||||||
{bindings}
|
|
||||||
on:change={event => (filter.value = event.detail)}
|
|
||||||
/>
|
/>
|
||||||
{:else if ["string", "longform", "number", "bigint", "formula"].includes(filter.type)}
|
{/if}
|
||||||
|
{#if allowBindings && filter.field && filter.valueType === "Binding"}
|
||||||
|
<slot name="binding" {filter} />
|
||||||
|
{:else if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)}
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||||
{:else if filter.type === "array" || (filter.type === "options" && filter.operator === "oneOf")}
|
{:else if filter.type === FieldType.ARRAY || (filter.type === FieldType.OPTIONS && filter.operator === SearchFilterOperator.ONE_OF)}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={getFieldOptions(filter.field)}
|
options={getFieldOptions(filter.field)}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "options"}
|
{:else if filter.type === FieldType.OPTIONS}
|
||||||
<Combobox
|
<Combobox
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={getFieldOptions(filter.field)}
|
options={getFieldOptions(filter.field)}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "boolean"}
|
{:else if filter.type === FieldType.BOOLEAN}
|
||||||
<Combobox
|
<Combobox
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
options={[
|
options={[
|
||||||
|
@ -289,7 +273,7 @@
|
||||||
]}
|
]}
|
||||||
bind:value={filter.value}
|
bind:value={filter.value}
|
||||||
/>
|
/>
|
||||||
{:else if filter.type === "datetime"}
|
{:else if filter.type === FieldType.DATETIME}
|
||||||
<DatePicker
|
<DatePicker
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
enableTime={!getSchema(filter)?.dateOnly}
|
enableTime={!getSchema(filter)?.dateOnly}
|
||||||
|
@ -306,32 +290,36 @@
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableInput disabled />
|
<Input disabled />
|
||||||
{/if}
|
{/if}
|
||||||
<Icon
|
<div class="controls">
|
||||||
name="Duplicate"
|
<Icon
|
||||||
hoverable
|
name="Duplicate"
|
||||||
size="S"
|
hoverable
|
||||||
on:click={() => duplicateFilter(filter.id)}
|
size="S"
|
||||||
/>
|
on:click={() => duplicateFilter(filter.id)}
|
||||||
<Icon
|
/>
|
||||||
name="Close"
|
<Icon
|
||||||
hoverable
|
name="Close"
|
||||||
size="S"
|
hoverable
|
||||||
on:click={() => removeFilter(filter.id)}
|
size="S"
|
||||||
/>
|
on:click={() => removeFilter(filter.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="bottom">
|
<div>
|
||||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||||
Add filter
|
Add filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
{:else}
|
||||||
</div>
|
<Body size="S">None of the table column can be used for filtering.</Body>
|
||||||
</DrawerContent>
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
@ -339,22 +327,42 @@
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
column-gap: var(--spacing-l);
|
column-gap: var(--spacing-l);
|
||||||
row-gap: var(--spacing-s);
|
row-gap: var(--spacing-s);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
grid-template-columns: 1fr 120px 1fr auto auto;
|
||||||
|
}
|
||||||
|
.fields.with-bindings {
|
||||||
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.mobile .fields {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.container.mobile .controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-s) 0;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.filter-label {
|
.filter-label {
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom {
|
.behaviour-filters {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
grid-template-columns: minmax(150px, 1fr) 170px 120px minmax(150px, 1fr) 16px 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, Multiselect } from "@budibase/bbui"
|
import { Select, Multiselect } from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
|
import { createAPIClient } from "../api"
|
||||||
|
|
||||||
import { API } from "api"
|
export let API = createAPIClient()
|
||||||
|
|
||||||
export let value = null
|
export let value = null
|
||||||
export let disabled
|
export let disabled
|
||||||
export let multiselect = false
|
export let multiselect = false
|
|
@ -61,14 +61,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAttachments = async fileList => {
|
|
||||||
try {
|
|
||||||
return await API.deleteBuilderAttachments(fileList)
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
api = {
|
api = {
|
||||||
focus: () => open(),
|
focus: () => open(),
|
||||||
|
@ -101,7 +93,6 @@
|
||||||
on:change={e => onChange(e.detail)}
|
on:change={e => onChange(e.detail)}
|
||||||
maximum={maximum || schema.constraints?.length?.maximum}
|
maximum={maximum || schema.constraints?.length?.maximum}
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
export let canEditColumns = true
|
export let canEditColumns = true
|
||||||
export let canSaveSchema = true
|
export let canSaveSchema = true
|
||||||
export let stripeRows = false
|
export let stripeRows = false
|
||||||
|
export let quiet = false
|
||||||
export let collaboration = true
|
export let collaboration = true
|
||||||
export let showAvatars = true
|
export let showAvatars = true
|
||||||
export let showControls = true
|
export let showControls = true
|
||||||
|
@ -91,6 +92,7 @@
|
||||||
canEditColumns,
|
canEditColumns,
|
||||||
canSaveSchema,
|
canSaveSchema,
|
||||||
stripeRows,
|
stripeRows,
|
||||||
|
quiet,
|
||||||
collaboration,
|
collaboration,
|
||||||
showAvatars,
|
showAvatars,
|
||||||
showControls,
|
showControls,
|
||||||
|
@ -124,6 +126,7 @@
|
||||||
class:is-resizing={$isResizing}
|
class:is-resizing={$isResizing}
|
||||||
class:is-reordering={$isReordering}
|
class:is-reordering={$isReordering}
|
||||||
class:stripe={stripeRows}
|
class:stripe={stripeRows}
|
||||||
|
class:quiet
|
||||||
on:mouseenter={() => gridFocused.set(true)}
|
on:mouseenter={() => gridFocused.set(true)}
|
||||||
on:mouseleave={() => gridFocused.set(false)}
|
on:mouseleave={() => gridFocused.set(false)}
|
||||||
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
|
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
|
||||||
|
@ -331,4 +334,9 @@
|
||||||
.grid-data-outer :global(.spectrum-Checkbox-partialCheckmark) {
|
.grid-data-outer :global(.spectrum-Checkbox-partialCheckmark) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Overrides */
|
||||||
|
.grid.quiet :global(.grid-data-content .row > .cell:not(:last-child)) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,3 +6,4 @@ export { default as UserAvatars } from "./UserAvatars.svelte"
|
||||||
export { default as Updating } from "./Updating.svelte"
|
export { default as Updating } from "./Updating.svelte"
|
||||||
export { Grid } from "./grid"
|
export { Grid } from "./grid"
|
||||||
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
||||||
|
export { default as FilterBuilder } from "./FilterBuilder.svelte"
|
||||||
|
|
|
@ -348,8 +348,7 @@ export default class DataFetch {
|
||||||
* Determine the feature flag for this datasource definition
|
* Determine the feature flag for this datasource definition
|
||||||
* @param definition
|
* @param definition
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars
|
determineFeatureFlags(_definition) {
|
||||||
determineFeatureFlags(definition) {
|
|
||||||
return {
|
return {
|
||||||
supportsSearch: false,
|
supportsSearch: false,
|
||||||
supportsSort: false,
|
supportsSort: false,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit ef186d00241f96037f9fd34d7a3826041977ab3a
|
Subproject commit dff7b5a9dd1fd770f8a48fb8e6df1740be605f18
|
|
@ -61,14 +61,17 @@
|
||||||
"@google-cloud/firestore": "6.8.0",
|
"@google-cloud/firestore": "6.8.0",
|
||||||
"@koa/router": "8.0.8",
|
"@koa/router": "8.0.8",
|
||||||
"@socket.io/redis-adapter": "^8.2.1",
|
"@socket.io/redis-adapter": "^8.2.1",
|
||||||
|
"@types/xml2js": "^0.4.14",
|
||||||
"airtable": "0.10.1",
|
"airtable": "0.10.1",
|
||||||
"arangojs": "7.2.0",
|
"arangojs": "7.2.0",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.1.0",
|
"bcrypt": "5.1.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
|
"bl": "^6.0.12",
|
||||||
"bull": "4.10.1",
|
"bull": "4.10.1",
|
||||||
"chokidar": "3.5.3",
|
"chokidar": "3.5.3",
|
||||||
|
"content-disposition": "^0.5.4",
|
||||||
"cookies": "0.8.0",
|
"cookies": "0.8.0",
|
||||||
"csvtojson": "2.0.10",
|
"csvtojson": "2.0.10",
|
||||||
"curlconverter": "3.21.0",
|
"curlconverter": "3.21.0",
|
||||||
|
@ -125,7 +128,7 @@
|
||||||
"@babel/preset-env": "7.16.11",
|
"@babel/preset-env": "7.16.11",
|
||||||
"@swc/core": "1.3.71",
|
"@swc/core": "1.3.71",
|
||||||
"@swc/jest": "0.2.27",
|
"@swc/jest": "0.2.27",
|
||||||
"@types/archiver": "^6.0.2",
|
"@types/archiver": "6.0.2",
|
||||||
"@types/global-agent": "2.1.1",
|
"@types/global-agent": "2.1.1",
|
||||||
"@types/google-spreadsheet": "3.1.5",
|
"@types/google-spreadsheet": "3.1.5",
|
||||||
"@types/jest": "29.5.5",
|
"@types/jest": "29.5.5",
|
||||||
|
|
|
@ -4,6 +4,7 @@ services:
|
||||||
# user: sa
|
# user: sa
|
||||||
# database: master
|
# database: master
|
||||||
mssql:
|
mssql:
|
||||||
|
# platform: linux/amd64
|
||||||
image: bb/mssql
|
image: bb/mssql
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
|
@ -76,7 +76,7 @@ function writeFile(output: any, filename: string) {
|
||||||
console.log(`Wrote spec to ${path}`)
|
console.log(`Wrote spec to ${path}`)
|
||||||
return path
|
return path
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error("Error writing spec file", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
|
||||||
import { processObjectSync } from "@budibase/string-templates"
|
import { processObjectSync } from "@budibase/string-templates"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
import AliasTables from "./alias"
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
|
|
||||||
|
@ -120,6 +119,9 @@ async function removeManyToManyRelationships(
|
||||||
endpoint: getEndpoint(tableId, Operation.DELETE),
|
endpoint: getEndpoint(tableId, Operation.DELETE),
|
||||||
body: { [colName]: null },
|
body: { [colName]: null },
|
||||||
filters,
|
filters,
|
||||||
|
meta: {
|
||||||
|
table,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
|
@ -134,6 +136,9 @@ async function removeOneToManyRelationships(rowId: string, table: Table) {
|
||||||
return getDatasourceAndQuery({
|
return getDatasourceAndQuery({
|
||||||
endpoint: getEndpoint(tableId, Operation.UPDATE),
|
endpoint: getEndpoint(tableId, Operation.UPDATE),
|
||||||
filters,
|
filters,
|
||||||
|
meta: {
|
||||||
|
table,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
|
@ -249,6 +254,9 @@ export class ExternalRequest<T extends Operation> {
|
||||||
const response = await getDatasourceAndQuery({
|
const response = await getDatasourceAndQuery({
|
||||||
endpoint: getEndpoint(table._id!, Operation.READ),
|
endpoint: getEndpoint(table._id!, Operation.READ),
|
||||||
filters: buildFilters(rowId, {}, table),
|
filters: buildFilters(rowId, {}, table),
|
||||||
|
meta: {
|
||||||
|
table,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if (Array.isArray(response) && response.length > 0) {
|
if (Array.isArray(response) && response.length > 0) {
|
||||||
return response[0]
|
return response[0]
|
||||||
|
@ -366,36 +374,45 @@ export class ExternalRequest<T extends Operation> {
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let tableId: string | undefined,
|
let relatedTableId: string | undefined,
|
||||||
lookupField: string | undefined,
|
lookupField: string | undefined,
|
||||||
fieldName: string | undefined
|
fieldName: string | undefined
|
||||||
if (isManyToMany(field)) {
|
if (isManyToMany(field)) {
|
||||||
tableId = field.through
|
relatedTableId = field.through
|
||||||
lookupField = primaryKey
|
lookupField = primaryKey
|
||||||
fieldName = field.throughTo || primaryKey
|
fieldName = field.throughTo || primaryKey
|
||||||
} else if (isManyToOne(field)) {
|
} else if (isManyToOne(field)) {
|
||||||
tableId = field.tableId
|
relatedTableId = field.tableId
|
||||||
lookupField = field.foreignKey
|
lookupField = field.foreignKey
|
||||||
fieldName = field.fieldName
|
fieldName = field.fieldName
|
||||||
}
|
}
|
||||||
if (!tableId || !lookupField || !fieldName) {
|
if (!relatedTableId || !lookupField || !fieldName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Unable to lookup relationships - undefined column properties."
|
"Unable to lookup relationships - undefined column properties."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const { tableName: relatedTableName } = breakExternalTableId(tableId)
|
const { tableName: relatedTableName } =
|
||||||
|
breakExternalTableId(relatedTableId)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const linkPrimaryKey = this.tables[relatedTableName].primary[0]
|
const linkPrimaryKey = this.tables[relatedTableName].primary[0]
|
||||||
if (!lookupField || !row[lookupField]) {
|
if (!lookupField || !row[lookupField]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
const endpoint = getEndpoint(relatedTableId, Operation.READ)
|
||||||
|
const relatedTable = this.tables[endpoint.entityId]
|
||||||
|
if (!relatedTable) {
|
||||||
|
throw new Error("unable to find related table")
|
||||||
|
}
|
||||||
const response = await getDatasourceAndQuery({
|
const response = await getDatasourceAndQuery({
|
||||||
endpoint: getEndpoint(tableId, Operation.READ),
|
endpoint: endpoint,
|
||||||
filters: {
|
filters: {
|
||||||
equal: {
|
equal: {
|
||||||
[fieldName]: row[lookupField],
|
[fieldName]: row[lookupField],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
table: relatedTable,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
// this is the response from knex if no rows found
|
// this is the response from knex if no rows found
|
||||||
const rows: Row[] =
|
const rows: Row[] =
|
||||||
|
@ -403,7 +420,11 @@ export class ExternalRequest<T extends Operation> {
|
||||||
const storeTo = isManyToMany(field)
|
const storeTo = isManyToMany(field)
|
||||||
? field.throughFrom || linkPrimaryKey
|
? field.throughFrom || linkPrimaryKey
|
||||||
: fieldName
|
: fieldName
|
||||||
related[storeTo] = { rows, isMany: isManyToMany(field), tableId }
|
related[storeTo] = {
|
||||||
|
rows,
|
||||||
|
isMany: isManyToMany(field),
|
||||||
|
tableId: relatedTableId,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return related
|
return related
|
||||||
}
|
}
|
||||||
|
@ -471,6 +492,9 @@ export class ExternalRequest<T extends Operation> {
|
||||||
// if we're doing many relationships then we're writing, only one response
|
// if we're doing many relationships then we're writing, only one response
|
||||||
body,
|
body,
|
||||||
filters: buildFilters(id, {}, linkTable),
|
filters: buildFilters(id, {}, linkTable),
|
||||||
|
meta: {
|
||||||
|
table: linkTable,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -618,7 +642,7 @@ export class ExternalRequest<T extends Operation> {
|
||||||
if (env.SQL_ALIASING_DISABLE) {
|
if (env.SQL_ALIASING_DISABLE) {
|
||||||
response = await getDatasourceAndQuery(json)
|
response = await getDatasourceAndQuery(json)
|
||||||
} else {
|
} else {
|
||||||
const aliasing = new AliasTables(Object.keys(this.tables))
|
const aliasing = new sdk.rows.AliasTables(Object.keys(this.tables))
|
||||||
response = await aliasing.queryWithAliasing(json)
|
response = await aliasing.queryWithAliasing(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,12 +62,12 @@ export function basicProcessing({
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
isLinked,
|
isLinked,
|
||||||
internal,
|
sqs,
|
||||||
}: {
|
}: {
|
||||||
row: Row
|
row: Row
|
||||||
table: Table
|
table: Table
|
||||||
isLinked: boolean
|
isLinked: boolean
|
||||||
internal?: boolean
|
sqs?: boolean
|
||||||
}): Row {
|
}): Row {
|
||||||
const thisRow: Row = {}
|
const thisRow: Row = {}
|
||||||
// filter the row down to what is actually the row (not joined)
|
// filter the row down to what is actually the row (not joined)
|
||||||
|
@ -84,12 +84,13 @@ export function basicProcessing({
|
||||||
thisRow[fieldName] = value
|
thisRow[fieldName] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!internal) {
|
if (!sqs) {
|
||||||
thisRow._id = generateIdForRow(row, table, isLinked)
|
thisRow._id = generateIdForRow(row, table, isLinked)
|
||||||
thisRow.tableId = table._id
|
thisRow.tableId = table._id
|
||||||
thisRow._rev = "rev"
|
thisRow._rev = "rev"
|
||||||
} else {
|
} else {
|
||||||
for (let internalColumn of CONSTANT_INTERNAL_ROW_COLS) {
|
const columns = Object.keys(table.schema)
|
||||||
|
for (let internalColumn of [...CONSTANT_INTERNAL_ROW_COLS, ...columns]) {
|
||||||
thisRow[internalColumn] = extractFieldValue({
|
thisRow[internalColumn] = extractFieldValue({
|
||||||
row,
|
row,
|
||||||
tableName: table._id!,
|
tableName: table._id!,
|
||||||
|
|
|
@ -51,11 +51,11 @@ export async function updateRelationshipColumns(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let linked = await basicProcessing({
|
let linked = basicProcessing({
|
||||||
row,
|
row,
|
||||||
table: linkedTable,
|
table: linkedTable,
|
||||||
isLinked: true,
|
isLinked: true,
|
||||||
internal: opts?.sqs,
|
sqs: opts?.sqs,
|
||||||
})
|
})
|
||||||
if (!linked._id) {
|
if (!linked._id) {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -132,6 +132,7 @@ export async function sqlOutputProcessing(
|
||||||
let rowId = row._id
|
let rowId = row._id
|
||||||
if (opts?.sqs) {
|
if (opts?.sqs) {
|
||||||
rowId = getInternalRowId(row, table)
|
rowId = getInternalRowId(row, table)
|
||||||
|
row._id = rowId
|
||||||
} else if (!rowId) {
|
} else if (!rowId) {
|
||||||
rowId = generateIdForRow(row, table)
|
rowId = generateIdForRow(row, table)
|
||||||
row._id = rowId
|
row._id = rowId
|
||||||
|
@ -153,7 +154,7 @@ export async function sqlOutputProcessing(
|
||||||
row,
|
row,
|
||||||
table,
|
table,
|
||||||
isLinked: false,
|
isLinked: false,
|
||||||
internal: opts?.sqs,
|
sqs: opts?.sqs,
|
||||||
}),
|
}),
|
||||||
table
|
table
|
||||||
)
|
)
|
||||||
|
@ -167,7 +168,8 @@ export async function sqlOutputProcessing(
|
||||||
tables,
|
tables,
|
||||||
row,
|
row,
|
||||||
finalRows,
|
finalRows,
|
||||||
relationships
|
relationships,
|
||||||
|
opts
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -127,13 +127,6 @@ export const uploadFile = async function (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteObjects = async function (ctx: Ctx) {
|
|
||||||
ctx.body = await objectStore.deleteFiles(
|
|
||||||
ObjectStoreBuckets.APPS,
|
|
||||||
ctx.request.body.keys
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiresMigration = async (ctx: Ctx) => {
|
const requiresMigration = async (ctx: Ctx) => {
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ export async function makeTableRequest(
|
||||||
operation,
|
operation,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
|
table,
|
||||||
tables,
|
tables,
|
||||||
},
|
},
|
||||||
table,
|
table,
|
||||||
|
|
|
@ -31,7 +31,6 @@ export async function save(
|
||||||
renaming?: RenameColumn
|
renaming?: RenameColumn
|
||||||
) {
|
) {
|
||||||
const inputs = ctx.request.body
|
const inputs = ctx.request.body
|
||||||
const adding = inputs?._add
|
|
||||||
// can't do this right now
|
// can't do this right now
|
||||||
delete inputs.rows
|
delete inputs.rows
|
||||||
const tableId = ctx.request.body._id
|
const tableId = ctx.request.body._id
|
||||||
|
@ -44,7 +43,7 @@ export async function save(
|
||||||
const { datasource, table } = await sdk.tables.external.save(
|
const { datasource, table } = await sdk.tables.external.save(
|
||||||
datasourceId!,
|
datasourceId!,
|
||||||
inputs,
|
inputs,
|
||||||
{ tableId, renaming, adding }
|
{ tableId, renaming }
|
||||||
)
|
)
|
||||||
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
||||||
return table
|
return table
|
||||||
|
|
|
@ -77,11 +77,6 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
||||||
const renaming = ctx.request.body._rename
|
const renaming = ctx.request.body._rename
|
||||||
|
|
||||||
const api = pickApi({ table })
|
const api = pickApi({ table })
|
||||||
// do not pass _rename or _add if saving to CouchDB
|
|
||||||
if (api === internal) {
|
|
||||||
delete ctx.request.body._add
|
|
||||||
delete ctx.request.body._rename
|
|
||||||
}
|
|
||||||
let savedTable = await api.save(ctx, renaming)
|
let savedTable = await api.save(ctx, renaming)
|
||||||
if (!table._id) {
|
if (!table._id) {
|
||||||
savedTable = sdk.tables.enrichViewSchemas(savedTable)
|
savedTable = sdk.tables.enrichViewSchemas(savedTable)
|
||||||
|
|
|
@ -16,7 +16,7 @@ export async function save(
|
||||||
ctx: UserCtx<SaveTableRequest, SaveTableResponse>,
|
ctx: UserCtx<SaveTableRequest, SaveTableResponse>,
|
||||||
renaming?: RenameColumn
|
renaming?: RenameColumn
|
||||||
) {
|
) {
|
||||||
const { rows, ...rest } = ctx.request.body
|
const { _rename, rows, ...rest } = ctx.request.body
|
||||||
let tableToSave: Table = {
|
let tableToSave: Table = {
|
||||||
_id: generateTableID(),
|
_id: generateTableID(),
|
||||||
...rest,
|
...rest,
|
||||||
|
|
|
@ -32,11 +32,6 @@ router
|
||||||
.get("/builder/:file*", controller.serveBuilder)
|
.get("/builder/:file*", controller.serveBuilder)
|
||||||
.get("/api/assets/client", controller.serveClientLibrary)
|
.get("/api/assets/client", controller.serveClientLibrary)
|
||||||
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
|
.post("/api/attachments/process", authorized(BUILDER), controller.uploadFile)
|
||||||
.post(
|
|
||||||
"/api/attachments/delete",
|
|
||||||
authorized(BUILDER),
|
|
||||||
controller.deleteObjects
|
|
||||||
)
|
|
||||||
.post("/api/beta/:feature", controller.toggleBetaUiFeature)
|
.post("/api/beta/:feature", controller.toggleBetaUiFeature)
|
||||||
.post(
|
.post(
|
||||||
"/api/attachments/:tableId/upload",
|
"/api/attachments/:tableId/upload",
|
||||||
|
@ -44,12 +39,6 @@ router
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||||
controller.uploadFile
|
controller.uploadFile
|
||||||
)
|
)
|
||||||
.post(
|
|
||||||
"/api/attachments/:tableId/delete",
|
|
||||||
paramResource("tableId"),
|
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
|
||||||
controller.deleteObjects
|
|
||||||
)
|
|
||||||
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
|
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
|
||||||
.get("/app/:appUrl/:path*", controller.serveApp)
|
.get("/app/:appUrl/:path*", controller.serveApp)
|
||||||
.get("/:appId/:path*", controller.serveApp)
|
.get("/:appId/:path*", controller.serveApp)
|
||||||
|
|
|
@ -6,7 +6,19 @@ import sdk from "../../../sdk"
|
||||||
|
|
||||||
import tk from "timekeeper"
|
import tk from "timekeeper"
|
||||||
import { mocks } from "@budibase/backend-core/tests"
|
import { mocks } from "@budibase/backend-core/tests"
|
||||||
import { QueryPreview, SourceName } from "@budibase/types"
|
import {
|
||||||
|
Datasource,
|
||||||
|
FieldSchema,
|
||||||
|
FieldSubtype,
|
||||||
|
FieldType,
|
||||||
|
QueryPreview,
|
||||||
|
RelationshipType,
|
||||||
|
SourceName,
|
||||||
|
Table,
|
||||||
|
TableSchema,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
|
||||||
|
import { tableForDatasource } from "../../../tests/utilities/structures"
|
||||||
|
|
||||||
tk.freeze(mocks.date.MOCK_DATE)
|
tk.freeze(mocks.date.MOCK_DATE)
|
||||||
|
|
||||||
|
@ -223,4 +235,152 @@ describe("/datasources", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
|
||||||
|
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
|
||||||
|
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
|
||||||
|
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
|
||||||
|
])("fetch schema (%s)", (_, dsProvider) => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
datasource = await config.api.datasource.create(await dsProvider)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fetching schema will not drop tables or columns", async () => {
|
||||||
|
const datasourceId = datasource!._id!
|
||||||
|
|
||||||
|
const simpleTable = await config.api.table.save(
|
||||||
|
tableForDatasource(datasource, {
|
||||||
|
name: "simple",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type SupportedSqlTypes =
|
||||||
|
| FieldType.STRING
|
||||||
|
| FieldType.BARCODEQR
|
||||||
|
| FieldType.LONGFORM
|
||||||
|
| FieldType.OPTIONS
|
||||||
|
| FieldType.DATETIME
|
||||||
|
| FieldType.NUMBER
|
||||||
|
| FieldType.BOOLEAN
|
||||||
|
| FieldType.FORMULA
|
||||||
|
| FieldType.BIGINT
|
||||||
|
| FieldType.BB_REFERENCE
|
||||||
|
| FieldType.LINK
|
||||||
|
| FieldType.ARRAY
|
||||||
|
|
||||||
|
const fullSchema: {
|
||||||
|
[type in SupportedSqlTypes]: FieldSchema & { type: type }
|
||||||
|
} = {
|
||||||
|
[FieldType.STRING]: {
|
||||||
|
name: "string",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[FieldType.LONGFORM]: {
|
||||||
|
name: "longform",
|
||||||
|
type: FieldType.LONGFORM,
|
||||||
|
},
|
||||||
|
[FieldType.OPTIONS]: {
|
||||||
|
name: "options",
|
||||||
|
type: FieldType.OPTIONS,
|
||||||
|
constraints: {
|
||||||
|
presence: { allowEmpty: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[FieldType.NUMBER]: {
|
||||||
|
name: "number",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
},
|
||||||
|
[FieldType.BOOLEAN]: {
|
||||||
|
name: "boolean",
|
||||||
|
type: FieldType.BOOLEAN,
|
||||||
|
},
|
||||||
|
[FieldType.ARRAY]: {
|
||||||
|
name: "array",
|
||||||
|
type: FieldType.ARRAY,
|
||||||
|
},
|
||||||
|
[FieldType.DATETIME]: {
|
||||||
|
name: "datetime",
|
||||||
|
type: FieldType.DATETIME,
|
||||||
|
dateOnly: true,
|
||||||
|
timeOnly: false,
|
||||||
|
},
|
||||||
|
[FieldType.LINK]: {
|
||||||
|
name: "link",
|
||||||
|
type: FieldType.LINK,
|
||||||
|
tableId: simpleTable._id!,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "link",
|
||||||
|
},
|
||||||
|
[FieldType.FORMULA]: {
|
||||||
|
name: "formula",
|
||||||
|
type: FieldType.FORMULA,
|
||||||
|
formula: "any formula",
|
||||||
|
},
|
||||||
|
[FieldType.BARCODEQR]: {
|
||||||
|
name: "barcodeqr",
|
||||||
|
type: FieldType.BARCODEQR,
|
||||||
|
},
|
||||||
|
[FieldType.BIGINT]: {
|
||||||
|
name: "bigint",
|
||||||
|
type: FieldType.BIGINT,
|
||||||
|
},
|
||||||
|
[FieldType.BB_REFERENCE]: {
|
||||||
|
name: "bb_reference",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: FieldSubtype.USERS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.api.table.save(
|
||||||
|
tableForDatasource(datasource, {
|
||||||
|
name: "full",
|
||||||
|
schema: fullSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const persisted = await config.api.datasource.get(datasourceId)
|
||||||
|
await config.api.datasource.fetchSchema(datasourceId)
|
||||||
|
|
||||||
|
const updated = await config.api.datasource.get(datasourceId)
|
||||||
|
const expected: Datasource = {
|
||||||
|
...persisted,
|
||||||
|
entities:
|
||||||
|
persisted?.entities &&
|
||||||
|
Object.entries(persisted.entities).reduce<Record<string, Table>>(
|
||||||
|
(acc, [tableName, table]) => {
|
||||||
|
acc[tableName] = {
|
||||||
|
...table,
|
||||||
|
primaryDisplay: expect.not.stringMatching(
|
||||||
|
new RegExp(`^${table.primaryDisplay || ""}$`)
|
||||||
|
),
|
||||||
|
schema: Object.entries(table.schema).reduce<TableSchema>(
|
||||||
|
(acc, [fieldName, field]) => {
|
||||||
|
acc[fieldName] = expect.objectContaining({
|
||||||
|
...field,
|
||||||
|
})
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
|
||||||
|
_rev: expect.any(String),
|
||||||
|
}
|
||||||
|
expect(updated).toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
Query,
|
Query,
|
||||||
QueryPreview,
|
QueryPreview,
|
||||||
SourceName,
|
SourceName,
|
||||||
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as setup from "../utilities"
|
import * as setup from "../utilities"
|
||||||
import {
|
import {
|
||||||
|
@ -740,12 +741,25 @@ describe.each(
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("query through datasource", () => {
|
describe("query through datasource", () => {
|
||||||
it("should be able to query a pg datasource", async () => {
|
it("should be able to query the datasource", async () => {
|
||||||
|
const entityId = "test_table"
|
||||||
|
await config.api.datasource.update({
|
||||||
|
...datasource,
|
||||||
|
entities: {
|
||||||
|
[entityId]: {
|
||||||
|
name: entityId,
|
||||||
|
schema: {},
|
||||||
|
type: "table",
|
||||||
|
sourceId: datasource._id!,
|
||||||
|
sourceType: TableSourceType.EXTERNAL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
const res = await config.api.datasource.query({
|
const res = await config.api.datasource.query({
|
||||||
endpoint: {
|
endpoint: {
|
||||||
datasourceId: datasource._id!,
|
datasourceId: datasource._id!,
|
||||||
operation: Operation.READ,
|
operation: Operation.READ,
|
||||||
entityId: "test_table",
|
entityId,
|
||||||
},
|
},
|
||||||
resource: {
|
resource: {
|
||||||
fields: ["id", "name"],
|
fields: ["id", "name"],
|
||||||
|
|
|
@ -26,6 +26,7 @@ describe.each([
|
||||||
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
|
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
|
||||||
])("/api/:sourceId/search (%s)", (name, dsProvider) => {
|
])("/api/:sourceId/search (%s)", (name, dsProvider) => {
|
||||||
const isSqs = name === "internal-sqs"
|
const isSqs = name === "internal-sqs"
|
||||||
|
const isInternal = name === "internal"
|
||||||
const config = setup.getConfig()
|
const config = setup.getConfig()
|
||||||
|
|
||||||
let envCleanup: (() => void) | undefined
|
let envCleanup: (() => void) | undefined
|
||||||
|
@ -336,6 +337,20 @@ describe.each([
|
||||||
expectQuery({
|
expectQuery({
|
||||||
range: { age: { low: 5, high: 9 } },
|
range: { age: { low: 5, high: 9 } },
|
||||||
}).toFindNothing())
|
}).toFindNothing())
|
||||||
|
|
||||||
|
// We never implemented half-open ranges in Lucene.
|
||||||
|
!isInternal &&
|
||||||
|
it("can search using just a low value", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { age: { low: 5 } },
|
||||||
|
}).toContainExactly([{ age: 10 }]))
|
||||||
|
|
||||||
|
// We never implemented half-open ranges in Lucene.
|
||||||
|
!isInternal &&
|
||||||
|
it("can search using just a high value", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { age: { high: 5 } },
|
||||||
|
}).toContainExactly([{ age: 1 }]))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("sort", () => {
|
describe("sort", () => {
|
||||||
|
@ -440,6 +455,20 @@ describe.each([
|
||||||
expectQuery({
|
expectQuery({
|
||||||
range: { dob: { low: JAN_5TH, high: JAN_9TH } },
|
range: { dob: { low: JAN_5TH, high: JAN_9TH } },
|
||||||
}).toFindNothing())
|
}).toFindNothing())
|
||||||
|
|
||||||
|
// We never implemented half-open ranges in Lucene.
|
||||||
|
!isInternal &&
|
||||||
|
it("can search using just a low value", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { dob: { low: JAN_5TH } },
|
||||||
|
}).toContainExactly([{ dob: JAN_10TH }]))
|
||||||
|
|
||||||
|
// We never implemented half-open ranges in Lucene.
|
||||||
|
!isInternal &&
|
||||||
|
it("can search using just a high value", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { dob: { high: JAN_5TH } },
|
||||||
|
}).toContainExactly([{ dob: JAN_1ST }]))
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("sort", () => {
|
describe("sort", () => {
|
||||||
|
@ -550,4 +579,100 @@ describe.each([
|
||||||
]))
|
]))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("bigints", () => {
|
||||||
|
const SMALL = "1"
|
||||||
|
const MEDIUM = "10000000"
|
||||||
|
|
||||||
|
// Our bigints are int64s in most datasources.
|
||||||
|
const BIG = "9223372036854775807"
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await createTable({
|
||||||
|
num: { name: "num", type: FieldType.BIGINT },
|
||||||
|
})
|
||||||
|
await createRows([{ num: SMALL }, { num: MEDIUM }, { num: BIG }])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("equal", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({ equal: { num: SMALL } }).toContainExactly([
|
||||||
|
{ num: SMALL },
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("successfully finds a big value", () =>
|
||||||
|
expectQuery({ equal: { num: BIG } }).toContainExactly([{ num: BIG }]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ equal: { num: "2" } }).toFindNothing())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("notEqual", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({ notEqual: { num: SMALL } }).toContainExactly([
|
||||||
|
{ num: MEDIUM },
|
||||||
|
{ num: BIG },
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ notEqual: { num: 10 } }).toContainExactly([
|
||||||
|
{ num: SMALL },
|
||||||
|
{ num: MEDIUM },
|
||||||
|
{ num: BIG },
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("oneOf", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({ oneOf: { num: [SMALL] } }).toContainExactly([
|
||||||
|
{ num: SMALL },
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("successfully finds all rows", () =>
|
||||||
|
expectQuery({ oneOf: { num: [SMALL, MEDIUM, BIG] } }).toContainExactly([
|
||||||
|
{ num: SMALL },
|
||||||
|
{ num: MEDIUM },
|
||||||
|
{ num: BIG },
|
||||||
|
]))
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", () =>
|
||||||
|
expectQuery({ oneOf: { num: [2] } }).toFindNothing())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Range searches against bigints don't seem to work at all in Lucene, and I
|
||||||
|
// couldn't figure out why. Given that we're replacing Lucene with SQS,
|
||||||
|
// we've decided not to spend time on it.
|
||||||
|
!isInternal &&
|
||||||
|
describe("range", () => {
|
||||||
|
it("successfully finds a row", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { num: { low: SMALL, high: "5" } },
|
||||||
|
}).toContainExactly([{ num: SMALL }]))
|
||||||
|
|
||||||
|
it("successfully finds multiple rows", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { num: { low: SMALL, high: MEDIUM } },
|
||||||
|
}).toContainExactly([{ num: SMALL }, { num: MEDIUM }]))
|
||||||
|
|
||||||
|
it("successfully finds a row with a high bound", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { num: { low: MEDIUM, high: BIG } },
|
||||||
|
}).toContainExactly([{ num: MEDIUM }, { num: BIG }]))
|
||||||
|
|
||||||
|
it("successfully finds no rows", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { num: { low: "5", high: "5" } },
|
||||||
|
}).toFindNothing())
|
||||||
|
|
||||||
|
it("can search using just a low value", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { num: { low: MEDIUM } },
|
||||||
|
}).toContainExactly([{ num: MEDIUM }, { num: BIG }]))
|
||||||
|
|
||||||
|
it("can search using just a high value", () =>
|
||||||
|
expectQuery({
|
||||||
|
range: { num: { high: MEDIUM } },
|
||||||
|
}).toContainExactly([{ num: SMALL }, { num: MEDIUM }]))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,7 +34,7 @@ describe.each([
|
||||||
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
|
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
|
||||||
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
|
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
|
||||||
])("/tables (%s)", (_, dsProvider) => {
|
])("/tables (%s)", (_, dsProvider) => {
|
||||||
let isInternal: boolean
|
const isInternal: boolean = !dsProvider
|
||||||
let datasource: Datasource | undefined
|
let datasource: Datasource | undefined
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
@ -44,9 +44,6 @@ describe.each([
|
||||||
await config.init()
|
await config.init()
|
||||||
if (dsProvider) {
|
if (dsProvider) {
|
||||||
datasource = await config.api.datasource.create(await dsProvider)
|
datasource = await config.api.datasource.create(await dsProvider)
|
||||||
isInternal = false
|
|
||||||
} else {
|
|
||||||
isInternal = true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -219,9 +216,6 @@ describe.each([
|
||||||
|
|
||||||
it("should add a new column for an internal DB table", async () => {
|
it("should add a new column for an internal DB table", async () => {
|
||||||
const saveTableRequest: SaveTableRequest = {
|
const saveTableRequest: SaveTableRequest = {
|
||||||
_add: {
|
|
||||||
name: "NEW_COLUMN",
|
|
||||||
},
|
|
||||||
...basicTable(),
|
...basicTable(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +229,6 @@ describe.each([
|
||||||
updatedAt: expect.stringMatching(ISO_REGEX_PATTERN),
|
updatedAt: expect.stringMatching(ISO_REGEX_PATTERN),
|
||||||
views: {},
|
views: {},
|
||||||
}
|
}
|
||||||
delete expectedResponse._add
|
|
||||||
expect(response).toEqual(expectedResponse)
|
expect(response).toEqual(expectedResponse)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
QuotaUsageType,
|
QuotaUsageType,
|
||||||
SaveTableRequest,
|
SaveTableRequest,
|
||||||
SearchQueryOperators,
|
SearchFilterOperator,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
SortType,
|
SortType,
|
||||||
StaticQuotaName,
|
StaticQuotaName,
|
||||||
|
@ -132,7 +132,7 @@ describe.each([
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "field",
|
field: "field",
|
||||||
value: "value",
|
value: "value",
|
||||||
},
|
},
|
||||||
|
@ -236,7 +236,7 @@ describe.each([
|
||||||
...view,
|
...view,
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "newField",
|
field: "newField",
|
||||||
value: "thatValue",
|
value: "thatValue",
|
||||||
},
|
},
|
||||||
|
@ -263,7 +263,7 @@ describe.each([
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: generator.word(),
|
field: generator.word(),
|
||||||
value: generator.word(),
|
value: generator.word(),
|
||||||
},
|
},
|
||||||
|
@ -341,7 +341,7 @@ describe.each([
|
||||||
tableId: generator.guid(),
|
tableId: generator.guid(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "newField",
|
field: "newField",
|
||||||
value: "thatValue",
|
value: "thatValue",
|
||||||
},
|
},
|
||||||
|
@ -671,7 +671,7 @@ describe.each([
|
||||||
name: generator.guid(),
|
name: generator.guid(),
|
||||||
query: [
|
query: [
|
||||||
{
|
{
|
||||||
operator: SearchQueryOperators.EQUAL,
|
operator: SearchFilterOperator.EQUAL,
|
||||||
field: "two",
|
field: "two",
|
||||||
value: "bar2",
|
value: "bar2",
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
AutomationStepType,
|
AutomationStepType,
|
||||||
AutomationIOType,
|
AutomationIOType,
|
||||||
AutomationFeature,
|
AutomationFeature,
|
||||||
|
AutomationCustomIOType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
export const definition: AutomationStepSchema = {
|
export const definition: AutomationStepSchema = {
|
||||||
|
@ -72,10 +73,10 @@ export const definition: AutomationStepSchema = {
|
||||||
title: "Location",
|
title: "Location",
|
||||||
dependsOn: "addInvite",
|
dependsOn: "addInvite",
|
||||||
},
|
},
|
||||||
url: {
|
attachments: {
|
||||||
type: AutomationIOType.STRING,
|
type: AutomationIOType.ATTACHMENT,
|
||||||
title: "URL",
|
customType: AutomationCustomIOType.MULTI_ATTACHMENTS,
|
||||||
dependsOn: "addInvite",
|
title: "Attachments",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ["to", "from", "subject", "contents"],
|
required: ["to", "from", "subject", "contents"],
|
||||||
|
@ -110,11 +111,13 @@ export async function run({ inputs }: AutomationStepInput) {
|
||||||
summary,
|
summary,
|
||||||
location,
|
location,
|
||||||
url,
|
url,
|
||||||
|
attachments,
|
||||||
} = inputs
|
} = inputs
|
||||||
if (!contents) {
|
if (!contents) {
|
||||||
contents = "<h1>No content</h1>"
|
contents = "<h1>No content</h1>"
|
||||||
}
|
}
|
||||||
to = to || undefined
|
to = to || undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response = await sendSmtpEmail({
|
let response = await sendSmtpEmail({
|
||||||
to,
|
to,
|
||||||
|
@ -124,6 +127,7 @@ export async function run({ inputs }: AutomationStepInput) {
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
automation: true,
|
automation: true,
|
||||||
|
attachments,
|
||||||
invite: addInvite
|
invite: addInvite
|
||||||
? {
|
? {
|
||||||
startTime,
|
startTime,
|
||||||
|
|
|
@ -62,7 +62,6 @@ export const definition: AutomationStepSchema = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run({ inputs }: AutomationStepInput) {
|
export async function run({ inputs }: AutomationStepInput) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { automationId, ...fieldParams } = inputs.automation
|
const { automationId, ...fieldParams } = inputs.automation
|
||||||
|
|
||||||
if (await features.isTriggerAutomationRunEnabled()) {
|
if (await features.isTriggerAutomationRunEnabled()) {
|
||||||
|
|
|
@ -50,6 +50,10 @@ describe("test the outgoing webhook action", () => {
|
||||||
cc: "cc",
|
cc: "cc",
|
||||||
bcc: "bcc",
|
bcc: "bcc",
|
||||||
addInvite: true,
|
addInvite: true,
|
||||||
|
attachments: [
|
||||||
|
{ url: "attachment1", filename: "attachment1.txt" },
|
||||||
|
{ url: "attachment2", filename: "attachment2.txt" },
|
||||||
|
],
|
||||||
...invite,
|
...invite,
|
||||||
}
|
}
|
||||||
let resp = generateResponse(inputs.to, inputs.from)
|
let resp = generateResponse(inputs.to, inputs.from)
|
||||||
|
@ -69,6 +73,10 @@ describe("test the outgoing webhook action", () => {
|
||||||
bcc: "bcc",
|
bcc: "bcc",
|
||||||
invite,
|
invite,
|
||||||
automation: true,
|
automation: true,
|
||||||
|
attachments: [
|
||||||
|
{ url: "attachment1", filename: "attachment1.txt" },
|
||||||
|
{ url: "attachment2", filename: "attachment2.txt" },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -40,6 +40,7 @@ export const USER_METDATA_PREFIX = `${DocumentType.ROW}${SEPARATOR}${dbCore.Inte
|
||||||
export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
|
export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
|
||||||
export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}`
|
export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}`
|
||||||
export const AUTOMATION_LOG_PREFIX = `${DocumentType.AUTOMATION_LOG}${SEPARATOR}`
|
export const AUTOMATION_LOG_PREFIX = `${DocumentType.AUTOMATION_LOG}${SEPARATOR}`
|
||||||
|
export const SQS_DATASOURCE_INTERNAL = "internal"
|
||||||
export const ViewName = dbCore.ViewName
|
export const ViewName = dbCore.ViewName
|
||||||
export const InternalTables = dbCore.InternalTable
|
export const InternalTables = dbCore.InternalTable
|
||||||
export const UNICODE_MAX = dbCore.UNICODE_MAX
|
export const UNICODE_MAX = dbCore.UNICODE_MAX
|
||||||
|
|
|
@ -28,6 +28,7 @@ const DEFAULTS = {
|
||||||
PLUGINS_DIR: "/plugins",
|
PLUGINS_DIR: "/plugins",
|
||||||
FORKED_PROCESS_NAME: "main",
|
FORKED_PROCESS_NAME: "main",
|
||||||
JS_RUNNER_MEMORY_LIMIT: 64,
|
JS_RUNNER_MEMORY_LIMIT: 64,
|
||||||
|
COUCH_DB_SQL_URL: "http://localhost:4006",
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUERY_THREAD_TIMEOUT =
|
const QUERY_THREAD_TIMEOUT =
|
||||||
|
@ -39,6 +40,7 @@ const environment = {
|
||||||
// important - prefer app port to generic port
|
// important - prefer app port to generic port
|
||||||
PORT: process.env.APP_PORT || process.env.PORT,
|
PORT: process.env.APP_PORT || process.env.PORT,
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||||
|
COUCH_DB_SQL_URL: process.env.COUCH_DB_SQL_URL || DEFAULTS.COUCH_DB_SQL_URL,
|
||||||
MINIO_URL: process.env.MINIO_URL,
|
MINIO_URL: process.env.MINIO_URL,
|
||||||
WORKER_URL: process.env.WORKER_URL,
|
WORKER_URL: process.env.WORKER_URL,
|
||||||
AWS_REGION: process.env.AWS_REGION,
|
AWS_REGION: process.env.AWS_REGION,
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import { features } from "@budibase/backend-core"
|
import { features } from "@budibase/backend-core"
|
||||||
import env from "./environment"
|
import env from "./environment"
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
enum AppFeature {
|
enum AppFeature {
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
API = "api",
|
API = "api",
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
AUTOMATIONS = "automations",
|
AUTOMATIONS = "automations",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
getDatasource,
|
getDatasource,
|
||||||
rawQuery,
|
rawQuery,
|
||||||
} from "../integrations/tests/utils"
|
} from "../integrations/tests/utils"
|
||||||
import { builderSocket } from "../websockets"
|
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
fetch.mockSearch()
|
fetch.mockSearch()
|
||||||
|
@ -233,72 +232,6 @@ describe("mysql integrations", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /api/tables/", () => {
|
describe("POST /api/tables/", () => {
|
||||||
const emitDatasourceUpdateMock = jest.fn()
|
|
||||||
|
|
||||||
it("will emit the datasource entity schema with externalType to the front-end when adding a new column", async () => {
|
|
||||||
const addColumnToTable: TableRequest = {
|
|
||||||
type: "table",
|
|
||||||
sourceType: TableSourceType.EXTERNAL,
|
|
||||||
name: uniqueTableName(),
|
|
||||||
sourceId: datasource._id!,
|
|
||||||
primary: ["id"],
|
|
||||||
schema: {
|
|
||||||
id: {
|
|
||||||
type: FieldType.AUTO,
|
|
||||||
name: "id",
|
|
||||||
autocolumn: true,
|
|
||||||
},
|
|
||||||
new_column: {
|
|
||||||
type: FieldType.NUMBER,
|
|
||||||
name: "new_column",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_add: {
|
|
||||||
name: "new_column",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(builderSocket!, "emitDatasourceUpdate")
|
|
||||||
.mockImplementation(emitDatasourceUpdateMock)
|
|
||||||
|
|
||||||
await makeRequest("post", "/api/tables/", addColumnToTable)
|
|
||||||
|
|
||||||
const expectedTable: TableRequest = {
|
|
||||||
...addColumnToTable,
|
|
||||||
schema: {
|
|
||||||
id: {
|
|
||||||
type: FieldType.NUMBER,
|
|
||||||
name: "id",
|
|
||||||
autocolumn: true,
|
|
||||||
constraints: {
|
|
||||||
presence: false,
|
|
||||||
},
|
|
||||||
externalType: "int unsigned",
|
|
||||||
},
|
|
||||||
new_column: {
|
|
||||||
type: FieldType.NUMBER,
|
|
||||||
name: "new_column",
|
|
||||||
autocolumn: false,
|
|
||||||
constraints: {
|
|
||||||
presence: false,
|
|
||||||
},
|
|
||||||
externalType: "float(8,2)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
created: true,
|
|
||||||
_id: `${datasource._id}__${addColumnToTable.name}`,
|
|
||||||
}
|
|
||||||
delete expectedTable._add
|
|
||||||
|
|
||||||
expect(emitDatasourceUpdateMock).toHaveBeenCalledTimes(1)
|
|
||||||
const emittedDatasource: Datasource =
|
|
||||||
emitDatasourceUpdateMock.mock.calls[0][1]
|
|
||||||
expect(emittedDatasource.entities![expectedTable.name]).toEqual(
|
|
||||||
expectedTable
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("will rename a column", async () => {
|
it("will rename a column", async () => {
|
||||||
await makeRequest("post", "/api/tables/", primaryMySqlTable)
|
await makeRequest("post", "/api/tables/", primaryMySqlTable)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
QueryJson,
|
QueryJson,
|
||||||
Datasource,
|
Datasource,
|
||||||
DatasourcePlusQueryResponse,
|
DatasourcePlusQueryResponse,
|
||||||
|
RowOperations,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { getIntegration } from "../index"
|
import { getIntegration } from "../index"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
@ -10,6 +11,17 @@ export async function makeExternalQuery(
|
||||||
datasource: Datasource,
|
datasource: Datasource,
|
||||||
json: QueryJson
|
json: QueryJson
|
||||||
): Promise<DatasourcePlusQueryResponse> {
|
): Promise<DatasourcePlusQueryResponse> {
|
||||||
|
const entityId = json.endpoint.entityId,
|
||||||
|
tableName = json.meta.table.name,
|
||||||
|
tableId = json.meta.table._id
|
||||||
|
// case found during testing - make sure this doesn't happen again
|
||||||
|
if (
|
||||||
|
RowOperations.includes(json.endpoint.operation) &&
|
||||||
|
entityId !== tableId &&
|
||||||
|
entityId !== tableName
|
||||||
|
) {
|
||||||
|
throw new Error("Entity ID and table metadata do not align")
|
||||||
|
}
|
||||||
datasource = await sdk.datasources.enrich(datasource)
|
datasource = await sdk.datasources.enrich(datasource)
|
||||||
const Integration = await getIntegration(datasource.source)
|
const Integration = await getIntegration(datasource.source)
|
||||||
// query is the opinionated function
|
// query is the opinionated function
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
SqlClient,
|
SqlClient,
|
||||||
isValidFilter,
|
isValidFilter,
|
||||||
getNativeSql,
|
getNativeSql,
|
||||||
|
SqlStatements,
|
||||||
} from "../utils"
|
} from "../utils"
|
||||||
import SqlTableQueryBuilder from "./sqlTable"
|
import SqlTableQueryBuilder from "./sqlTable"
|
||||||
import {
|
import {
|
||||||
|
@ -22,6 +23,8 @@ import {
|
||||||
SortDirection,
|
SortDirection,
|
||||||
SqlQueryBinding,
|
SqlQueryBinding,
|
||||||
Table,
|
Table,
|
||||||
|
TableSourceType,
|
||||||
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import environment from "../../environment"
|
import environment from "../../environment"
|
||||||
|
|
||||||
|
@ -135,6 +138,18 @@ function generateSelectStatement(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTableName(table?: Table): string | undefined {
|
||||||
|
// SQS uses the table ID rather than the table name
|
||||||
|
if (
|
||||||
|
table?.sourceType === TableSourceType.INTERNAL ||
|
||||||
|
table?.sourceId === INTERNAL_TABLE_SOURCE_ID
|
||||||
|
) {
|
||||||
|
return table?._id
|
||||||
|
} else {
|
||||||
|
return table?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class InternalBuilder {
|
class InternalBuilder {
|
||||||
private readonly client: string
|
private readonly client: string
|
||||||
|
|
||||||
|
@ -146,10 +161,20 @@ class InternalBuilder {
|
||||||
addFilters(
|
addFilters(
|
||||||
query: Knex.QueryBuilder,
|
query: Knex.QueryBuilder,
|
||||||
filters: SearchFilters | undefined,
|
filters: SearchFilters | undefined,
|
||||||
tableName: string,
|
table: Table,
|
||||||
opts: { aliases?: Record<string, string>; relationship?: boolean }
|
opts: { aliases?: Record<string, string>; relationship?: boolean }
|
||||||
): Knex.QueryBuilder {
|
): Knex.QueryBuilder {
|
||||||
function getTableName(name: string) {
|
if (!filters) {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
filters = parseFilters(filters)
|
||||||
|
// if all or specified in filters, then everything is an or
|
||||||
|
const allOr = filters.allOr
|
||||||
|
const sqlStatements = new SqlStatements(this.client, table, { allOr })
|
||||||
|
const tableName =
|
||||||
|
this.client === SqlClient.SQL_LITE ? table._id! : table.name
|
||||||
|
|
||||||
|
function getTableAlias(name: string) {
|
||||||
const alias = opts.aliases?.[name]
|
const alias = opts.aliases?.[name]
|
||||||
return alias || name
|
return alias || name
|
||||||
}
|
}
|
||||||
|
@ -161,11 +186,11 @@ class InternalBuilder {
|
||||||
const updatedKey = dbCore.removeKeyNumbering(key)
|
const updatedKey = dbCore.removeKeyNumbering(key)
|
||||||
const isRelationshipField = updatedKey.includes(".")
|
const isRelationshipField = updatedKey.includes(".")
|
||||||
if (!opts.relationship && !isRelationshipField) {
|
if (!opts.relationship && !isRelationshipField) {
|
||||||
fn(`${getTableName(tableName)}.${updatedKey}`, value)
|
fn(`${getTableAlias(tableName)}.${updatedKey}`, value)
|
||||||
}
|
}
|
||||||
if (opts.relationship && isRelationshipField) {
|
if (opts.relationship && isRelationshipField) {
|
||||||
const [filterTableName, property] = updatedKey.split(".")
|
const [filterTableName, property] = updatedKey.split(".")
|
||||||
fn(`${getTableName(filterTableName)}.${property}`, value)
|
fn(`${getTableAlias(filterTableName)}.${property}`, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,12 +269,6 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!filters) {
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
filters = parseFilters(filters)
|
|
||||||
// if all or specified in filters, then everything is an or
|
|
||||||
const allOr = filters.allOr
|
|
||||||
if (filters.oneOf) {
|
if (filters.oneOf) {
|
||||||
iterate(filters.oneOf, (key, array) => {
|
iterate(filters.oneOf, (key, array) => {
|
||||||
const fnc = allOr ? "orWhereIn" : "whereIn"
|
const fnc = allOr ? "orWhereIn" : "whereIn"
|
||||||
|
@ -292,17 +311,11 @@ class InternalBuilder {
|
||||||
const lowValid = isValidFilter(value.low),
|
const lowValid = isValidFilter(value.low),
|
||||||
highValid = isValidFilter(value.high)
|
highValid = isValidFilter(value.high)
|
||||||
if (lowValid && highValid) {
|
if (lowValid && highValid) {
|
||||||
// Use a between operator if we have 2 valid range values
|
query = sqlStatements.between(query, key, value.low, value.high)
|
||||||
const fnc = allOr ? "orWhereBetween" : "whereBetween"
|
|
||||||
query = query[fnc](key, [value.low, value.high])
|
|
||||||
} else if (lowValid) {
|
} else if (lowValid) {
|
||||||
// Use just a single greater than operator if we only have a low
|
query = sqlStatements.lte(query, key, value.low)
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
|
||||||
query = query[fnc](key, ">", value.low)
|
|
||||||
} else if (highValid) {
|
} else if (highValid) {
|
||||||
// Use just a single less than operator if we only have a high
|
query = sqlStatements.gte(query, key, value.high)
|
||||||
const fnc = allOr ? "orWhere" : "where"
|
|
||||||
query = query[fnc](key, "<", value.high)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -345,10 +358,11 @@ class InternalBuilder {
|
||||||
|
|
||||||
addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
|
addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
|
||||||
let { sort, paginate } = json
|
let { sort, paginate } = json
|
||||||
const table = json.meta?.table
|
const table = json.meta.table
|
||||||
|
const tableName = getTableName(table)
|
||||||
const aliases = json.tableAliases
|
const aliases = json.tableAliases
|
||||||
const aliased =
|
const aliased =
|
||||||
table?.name && aliases?.[table.name] ? aliases[table.name] : table?.name
|
tableName && aliases?.[tableName] ? aliases[tableName] : table?.name
|
||||||
if (sort && Object.keys(sort || {}).length > 0) {
|
if (sort && Object.keys(sort || {}).length > 0) {
|
||||||
for (let [key, value] of Object.entries(sort)) {
|
for (let [key, value] of Object.entries(sort)) {
|
||||||
const direction =
|
const direction =
|
||||||
|
@ -458,14 +472,13 @@ class InternalBuilder {
|
||||||
): Knex.QueryBuilder {
|
): Knex.QueryBuilder {
|
||||||
const tableName = endpoint.entityId
|
const tableName = endpoint.entityId
|
||||||
const tableAlias = aliases?.[tableName]
|
const tableAlias = aliases?.[tableName]
|
||||||
let table: string | Record<string, string> = tableName
|
|
||||||
if (tableAlias) {
|
const query = knex(
|
||||||
table = { [tableAlias]: tableName }
|
this.tableNameWithSchema(tableName, {
|
||||||
}
|
alias: tableAlias,
|
||||||
let query = knex(table)
|
schema: endpoint.schema,
|
||||||
if (endpoint.schema) {
|
})
|
||||||
query = query.withSchema(endpoint.schema)
|
)
|
||||||
}
|
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,7 +545,7 @@ class InternalBuilder {
|
||||||
if (foundOffset) {
|
if (foundOffset) {
|
||||||
query = query.offset(foundOffset)
|
query = query.offset(foundOffset)
|
||||||
}
|
}
|
||||||
query = this.addFilters(query, filters, tableName, {
|
query = this.addFilters(query, filters, json.meta.table, {
|
||||||
aliases: tableAliases,
|
aliases: tableAliases,
|
||||||
})
|
})
|
||||||
// add sorting to pre-query
|
// add sorting to pre-query
|
||||||
|
@ -553,7 +566,7 @@ class InternalBuilder {
|
||||||
endpoint.schema,
|
endpoint.schema,
|
||||||
tableAliases
|
tableAliases
|
||||||
)
|
)
|
||||||
return this.addFilters(query, filters, tableName, {
|
return this.addFilters(query, filters, json.meta.table, {
|
||||||
relationship: true,
|
relationship: true,
|
||||||
aliases: tableAliases,
|
aliases: tableAliases,
|
||||||
})
|
})
|
||||||
|
@ -563,7 +576,7 @@ class InternalBuilder {
|
||||||
const { endpoint, body, filters, tableAliases } = json
|
const { endpoint, body, filters, tableAliases } = json
|
||||||
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
||||||
const parsedBody = parseBody(body)
|
const parsedBody = parseBody(body)
|
||||||
query = this.addFilters(query, filters, endpoint.entityId, {
|
query = this.addFilters(query, filters, json.meta.table, {
|
||||||
aliases: tableAliases,
|
aliases: tableAliases,
|
||||||
})
|
})
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
|
@ -577,7 +590,7 @@ class InternalBuilder {
|
||||||
delete(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
|
delete(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
|
||||||
const { endpoint, filters, tableAliases } = json
|
const { endpoint, filters, tableAliases } = json
|
||||||
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
let query = this.knexWithAlias(knex, endpoint, tableAliases)
|
||||||
query = this.addFilters(query, filters, endpoint.entityId, {
|
query = this.addFilters(query, filters, json.meta.table, {
|
||||||
aliases: tableAliases,
|
aliases: tableAliases,
|
||||||
})
|
})
|
||||||
// mysql can't use returning
|
// mysql can't use returning
|
||||||
|
@ -669,7 +682,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
// when creating if an ID has been inserted need to make sure
|
// when creating if an ID has been inserted need to make sure
|
||||||
// the id filter is enriched with it before trying to retrieve the row
|
// the id filter is enriched with it before trying to retrieve the row
|
||||||
checkLookupKeys(id: any, json: QueryJson) {
|
checkLookupKeys(id: any, json: QueryJson) {
|
||||||
if (!id || !json.meta?.table || !json.meta.table.primary) {
|
if (!id || !json.meta.table || !json.meta.table.primary) {
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
const primaryKey = json.meta.table.primary?.[0]
|
const primaryKey = json.meta.table.primary?.[0]
|
||||||
|
@ -729,12 +742,13 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
|
||||||
results: Record<string, any>[],
|
results: Record<string, any>[],
|
||||||
aliases?: Record<string, string>
|
aliases?: Record<string, string>
|
||||||
): Record<string, any>[] {
|
): Record<string, any>[] {
|
||||||
|
const tableName = getTableName(table)
|
||||||
for (const [name, field] of Object.entries(table.schema)) {
|
for (const [name, field] of Object.entries(table.schema)) {
|
||||||
if (!this._isJsonColumn(field)) {
|
if (!this._isJsonColumn(field)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const tableName = aliases?.[table.name] || table.name
|
const aliasedTableName = (tableName && aliases?.[tableName]) || tableName
|
||||||
const fullName = `${tableName}.${name}`
|
const fullName = `${aliasedTableName}.${name}`
|
||||||
for (let row of results) {
|
for (let row of results) {
|
||||||
if (typeof row[fullName] === "string") {
|
if (typeof row[fullName] === "string") {
|
||||||
row[fullName] = JSON.parse(row[fullName])
|
row[fullName] = JSON.parse(row[fullName])
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import { Knex, knex } from "knex"
|
import { Knex, knex } from "knex"
|
||||||
import {
|
import {
|
||||||
RelationshipType,
|
|
||||||
FieldSubtype,
|
FieldSubtype,
|
||||||
|
FieldType,
|
||||||
NumberFieldMetadata,
|
NumberFieldMetadata,
|
||||||
Operation,
|
Operation,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
|
RelationshipType,
|
||||||
RenameColumn,
|
RenameColumn,
|
||||||
Table,
|
|
||||||
FieldType,
|
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
|
Table,
|
||||||
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { breakExternalTableId, getNativeSql, SqlClient } from "../utils"
|
import { breakExternalTableId, getNativeSql, SqlClient } from "../utils"
|
||||||
|
import { utils } from "@budibase/shared-core"
|
||||||
import SchemaBuilder = Knex.SchemaBuilder
|
import SchemaBuilder = Knex.SchemaBuilder
|
||||||
import CreateTableBuilder = Knex.CreateTableBuilder
|
import CreateTableBuilder = Knex.CreateTableBuilder
|
||||||
import { utils } from "@budibase/shared-core"
|
|
||||||
|
|
||||||
function isIgnoredType(type: FieldType) {
|
function isIgnoredType(type: FieldType) {
|
||||||
const ignored = [FieldType.LINK, FieldType.FORMULA]
|
const ignored = [FieldType.LINK, FieldType.FORMULA]
|
||||||
|
@ -105,13 +106,13 @@ function generateSchema(
|
||||||
column.relationshipType !== RelationshipType.MANY_TO_MANY
|
column.relationshipType !== RelationshipType.MANY_TO_MANY
|
||||||
) {
|
) {
|
||||||
if (!column.foreignKey || !column.tableId) {
|
if (!column.foreignKey || !column.tableId) {
|
||||||
throw "Invalid relationship schema"
|
throw new Error("Invalid relationship schema")
|
||||||
}
|
}
|
||||||
const { tableName } = breakExternalTableId(column.tableId)
|
const { tableName } = breakExternalTableId(column.tableId)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const relatedTable = tables[tableName]
|
const relatedTable = tables[tableName]
|
||||||
if (!relatedTable) {
|
if (!relatedTable) {
|
||||||
throw "Referenced table doesn't exist"
|
throw new Error("Referenced table doesn't exist")
|
||||||
}
|
}
|
||||||
const relatedPrimary = relatedTable.primary[0]
|
const relatedPrimary = relatedTable.primary[0]
|
||||||
const externalType = relatedTable.schema[relatedPrimary].externalType
|
const externalType = relatedTable.schema[relatedPrimary].externalType
|
||||||
|
@ -209,15 +210,19 @@ class SqlTableQueryBuilder {
|
||||||
|
|
||||||
let query: Knex.SchemaBuilder
|
let query: Knex.SchemaBuilder
|
||||||
if (!json.table || !json.meta || !json.meta.tables) {
|
if (!json.table || !json.meta || !json.meta.tables) {
|
||||||
throw "Cannot execute without table being specified"
|
throw new Error("Cannot execute without table being specified")
|
||||||
}
|
}
|
||||||
|
if (json.table.sourceType === TableSourceType.INTERNAL) {
|
||||||
|
throw new Error("Cannot perform table actions for SQS.")
|
||||||
|
}
|
||||||
|
|
||||||
switch (this._operation(json)) {
|
switch (this._operation(json)) {
|
||||||
case Operation.CREATE_TABLE:
|
case Operation.CREATE_TABLE:
|
||||||
query = buildCreateTable(client, json.table, json.meta.tables)
|
query = buildCreateTable(client, json.table, json.meta.tables)
|
||||||
break
|
break
|
||||||
case Operation.UPDATE_TABLE:
|
case Operation.UPDATE_TABLE:
|
||||||
if (!json.meta || !json.meta.table) {
|
if (!json.meta || !json.meta.table) {
|
||||||
throw "Must specify old table for update"
|
throw new Error("Must specify old table for update")
|
||||||
}
|
}
|
||||||
// renameColumn does not work for MySQL, so return a raw query
|
// renameColumn does not work for MySQL, so return a raw query
|
||||||
if (this.sqlClient === SqlClient.MY_SQL && json.meta.renamed) {
|
if (this.sqlClient === SqlClient.MY_SQL && json.meta.renamed) {
|
||||||
|
@ -264,7 +269,7 @@ class SqlTableQueryBuilder {
|
||||||
query = buildDeleteTable(client, json.table)
|
query = buildDeleteTable(client, json.table)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw "Table operation is of unknown type"
|
throw new Error("Table operation is of unknown type")
|
||||||
}
|
}
|
||||||
return getNativeSql(query)
|
return getNativeSql(query)
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,8 +168,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
getStringConcat(_parts: string[]) {
|
||||||
getStringConcat(parts: string[]) {
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -378,7 +378,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
|
||||||
try {
|
try {
|
||||||
await connection.close()
|
await connection.close()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error("Error connecting to Oracle", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,10 @@ import { performance } from "perf_hooks"
|
||||||
import FormData from "form-data"
|
import FormData from "form-data"
|
||||||
import { URLSearchParams } from "url"
|
import { URLSearchParams } from "url"
|
||||||
import { blacklist } from "@budibase/backend-core"
|
import { blacklist } from "@budibase/backend-core"
|
||||||
|
import { handleFileResponse, handleXml } from "./utils"
|
||||||
|
import { parse } from "content-disposition"
|
||||||
|
import path from "path"
|
||||||
|
import { Builder as XmlBuilder } from "xml2js"
|
||||||
|
|
||||||
const BodyTypes = {
|
const BodyTypes = {
|
||||||
NONE: "none",
|
NONE: "none",
|
||||||
|
@ -57,8 +61,6 @@ const coreFields = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const { parseStringPromise: xmlParser, Builder: XmlBuilder } = require("xml2js")
|
|
||||||
|
|
||||||
const SCHEMA: Integration = {
|
const SCHEMA: Integration = {
|
||||||
docs: "https://github.com/node-fetch/node-fetch",
|
docs: "https://github.com/node-fetch/node-fetch",
|
||||||
description:
|
description:
|
||||||
|
@ -129,42 +131,44 @@ class RestIntegration implements IntegrationBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseResponse(response: any, pagination: PaginationConfig | null) {
|
async parseResponse(response: any, pagination: PaginationConfig | null) {
|
||||||
let data, raw, headers
|
let data, raw, headers, filename
|
||||||
|
|
||||||
const contentType = response.headers.get("content-type") || ""
|
const contentType = response.headers.get("content-type") || ""
|
||||||
|
const contentDisposition = response.headers.get("content-disposition") || ""
|
||||||
|
if (
|
||||||
|
contentDisposition.includes("attachment") ||
|
||||||
|
contentDisposition.includes("form-data")
|
||||||
|
) {
|
||||||
|
filename =
|
||||||
|
path.basename(parse(contentDisposition).parameters?.filename) || ""
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (response.status === 204) {
|
if (filename) {
|
||||||
data = []
|
return handleFileResponse(response, filename, this.startTimeMs)
|
||||||
raw = []
|
|
||||||
} else if (contentType.includes("application/json")) {
|
|
||||||
data = await response.json()
|
|
||||||
raw = JSON.stringify(data)
|
|
||||||
} else if (
|
|
||||||
contentType.includes("text/xml") ||
|
|
||||||
contentType.includes("application/xml")
|
|
||||||
) {
|
|
||||||
const rawXml = await response.text()
|
|
||||||
data =
|
|
||||||
(await xmlParser(rawXml, {
|
|
||||||
explicitArray: false,
|
|
||||||
trim: true,
|
|
||||||
explicitRoot: false,
|
|
||||||
})) || {}
|
|
||||||
// there is only one structure, its an array, return the array so it appears as rows
|
|
||||||
const keys = Object.keys(data)
|
|
||||||
if (keys.length === 1 && Array.isArray(data[keys[0]])) {
|
|
||||||
data = data[keys[0]]
|
|
||||||
}
|
|
||||||
raw = rawXml
|
|
||||||
} else if (contentType.includes("application/pdf")) {
|
|
||||||
data = await response.arrayBuffer() // Save PDF as ArrayBuffer
|
|
||||||
raw = Buffer.from(data)
|
|
||||||
} else {
|
} else {
|
||||||
data = await response.text()
|
if (response.status === 204) {
|
||||||
raw = data
|
data = []
|
||||||
|
raw = []
|
||||||
|
} else if (contentType.includes("application/json")) {
|
||||||
|
data = await response.json()
|
||||||
|
raw = JSON.stringify(data)
|
||||||
|
} else if (
|
||||||
|
contentType.includes("text/xml") ||
|
||||||
|
contentType.includes("application/xml")
|
||||||
|
) {
|
||||||
|
let xmlResponse = await handleXml(response)
|
||||||
|
data = xmlResponse.data
|
||||||
|
raw = xmlResponse.rawXml
|
||||||
|
} else {
|
||||||
|
data = await response.text()
|
||||||
|
raw = data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw "Failed to parse response body."
|
throw `Failed to parse response body: ${err}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = formatBytes(
|
const size = formatBytes(
|
||||||
response.headers.get("content-length") || Buffer.byteLength(raw, "utf8")
|
response.headers.get("content-length") || Buffer.byteLength(raw, "utf8")
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,9 +13,23 @@ jest.mock("node-fetch", () => {
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
import fetch from "node-fetch"
|
jest.mock("@budibase/backend-core", () => {
|
||||||
|
const core = jest.requireActual("@budibase/backend-core")
|
||||||
|
return {
|
||||||
|
...core,
|
||||||
|
context: {
|
||||||
|
...core.context,
|
||||||
|
getProdAppId: jest.fn(() => "app-id"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
jest.mock("uuid", () => ({ v4: () => "00000000-0000-0000-0000-000000000000" }))
|
||||||
|
|
||||||
import { default as RestIntegration } from "../rest"
|
import { default as RestIntegration } from "../rest"
|
||||||
import { RestAuthType } from "@budibase/types"
|
import { RestAuthType } from "@budibase/types"
|
||||||
|
import fetch from "node-fetch"
|
||||||
|
import { objectStoreTestProviders } from "@budibase/backend-core/tests"
|
||||||
|
import { Readable } from "stream"
|
||||||
|
|
||||||
const FormData = require("form-data")
|
const FormData = require("form-data")
|
||||||
const { URLSearchParams } = require("url")
|
const { URLSearchParams } = require("url")
|
||||||
|
@ -611,4 +625,104 @@ describe("REST Integration", () => {
|
||||||
expect(calledConfig.headers).toEqual({})
|
expect(calledConfig.headers).toEqual({})
|
||||||
expect(calledConfig.agent.options.rejectUnauthorized).toBe(false)
|
expect(calledConfig.agent.options.rejectUnauthorized).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("File Handling", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.unmock("aws-sdk")
|
||||||
|
await objectStoreTestProviders.minio.start()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await objectStoreTestProviders.minio.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uploads file to object store and returns signed URL", async () => {
|
||||||
|
const responseData = Buffer.from("teest file contnt")
|
||||||
|
const filename = "test.tar.gz"
|
||||||
|
const contentType = "application/gzip"
|
||||||
|
const mockReadable = new Readable()
|
||||||
|
mockReadable.push(responseData)
|
||||||
|
mockReadable.push(null)
|
||||||
|
;(fetch as unknown as jest.Mock).mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
headers: {
|
||||||
|
raw: () => ({
|
||||||
|
"content-type": [contentType],
|
||||||
|
"content-disposition": [`attachment; filename="${filename}"`],
|
||||||
|
}),
|
||||||
|
get: (header: any) => {
|
||||||
|
if (header === "content-type") return contentType
|
||||||
|
if (header === "content-disposition")
|
||||||
|
return `attachment; filename="${filename}"`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
body: mockReadable,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
path: "api",
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await config.integration.read(query)
|
||||||
|
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
size: responseData.byteLength,
|
||||||
|
name: "00000000-0000-0000-0000-000000000000.tar.gz",
|
||||||
|
url: expect.stringContaining(
|
||||||
|
"/files/signed/tmp-file-attachments/app-id/00000000-0000-0000-0000-000000000000.tar.gz"
|
||||||
|
),
|
||||||
|
extension: "tar.gz",
|
||||||
|
key: expect.stringContaining(
|
||||||
|
"app-id/00000000-0000-0000-0000-000000000000.tar.gz"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uploads file with non ascii filename to object store and returns signed URL", async () => {
|
||||||
|
const responseData = Buffer.from("teest file contnt")
|
||||||
|
const contentType = "text/plain"
|
||||||
|
const mockReadable = new Readable()
|
||||||
|
mockReadable.push(responseData)
|
||||||
|
mockReadable.push(null)
|
||||||
|
;(fetch as unknown as jest.Mock).mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
headers: {
|
||||||
|
raw: () => ({
|
||||||
|
"content-type": [contentType],
|
||||||
|
"content-disposition": [
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
`attachment; filename="£ and ? rates.pdf"; filename*=UTF-8'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf`,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
get: (header: any) => {
|
||||||
|
if (header === "content-type") return contentType
|
||||||
|
if (header === "content-disposition")
|
||||||
|
// eslint-disable-next-line no-useless-escape
|
||||||
|
return `attachment; filename="£ and ? rates.pdf"; filename*=UTF-8'\'%C2%A3%20and%20%E2%82%AC%20rates.pdf`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
body: mockReadable,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
path: "api",
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await config.integration.read(query)
|
||||||
|
|
||||||
|
expect(response.data).toEqual({
|
||||||
|
size: responseData.byteLength,
|
||||||
|
name: "00000000-0000-0000-0000-000000000000.pdf",
|
||||||
|
url: expect.stringContaining(
|
||||||
|
"/files/signed/tmp-file-attachments/app-id/00000000-0000-0000-0000-000000000000.pdf"
|
||||||
|
),
|
||||||
|
extension: "pdf",
|
||||||
|
key: expect.stringContaining(
|
||||||
|
"app-id/00000000-0000-0000-0000-000000000000.pdf"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
import { SqlClient } from "../utils"
|
import { SqlClient } from "../utils"
|
||||||
import Sql from "../base/sql"
|
import Sql from "../base/sql"
|
||||||
import {
|
import {
|
||||||
|
FieldType,
|
||||||
Operation,
|
Operation,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
TableSourceType,
|
|
||||||
Table,
|
Table,
|
||||||
FieldType,
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
const TABLE_NAME = "test"
|
const TABLE_NAME = "test"
|
||||||
|
const TABLE: Table = {
|
||||||
|
type: "table",
|
||||||
|
sourceType: TableSourceType.EXTERNAL,
|
||||||
|
sourceId: "SOURCE_ID",
|
||||||
|
schema: {
|
||||||
|
id: {
|
||||||
|
name: "id",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: TABLE_NAME,
|
||||||
|
primary: ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
function endpoint(table: any, operation: any) {
|
function endpoint(table: any, operation: any) {
|
||||||
return {
|
return {
|
||||||
|
@ -25,6 +38,10 @@ function generateReadJson({
|
||||||
sort,
|
sort,
|
||||||
paginate,
|
paginate,
|
||||||
}: any = {}): QueryJson {
|
}: any = {}): QueryJson {
|
||||||
|
const tableObj = { ...TABLE }
|
||||||
|
if (table) {
|
||||||
|
tableObj.name = table
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint(table || TABLE_NAME, "READ"),
|
endpoint: endpoint(table || TABLE_NAME, "READ"),
|
||||||
resource: {
|
resource: {
|
||||||
|
@ -34,14 +51,7 @@ function generateReadJson({
|
||||||
sort: sort || {},
|
sort: sort || {},
|
||||||
paginate: paginate || {},
|
paginate: paginate || {},
|
||||||
meta: {
|
meta: {
|
||||||
table: {
|
table: tableObj,
|
||||||
type: "table",
|
|
||||||
sourceType: TableSourceType.EXTERNAL,
|
|
||||||
sourceId: "SOURCE_ID",
|
|
||||||
schema: {},
|
|
||||||
name: table || TABLE_NAME,
|
|
||||||
primary: ["id"],
|
|
||||||
} as any,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +59,9 @@ function generateReadJson({
|
||||||
function generateCreateJson(table = TABLE_NAME, body = {}): QueryJson {
|
function generateCreateJson(table = TABLE_NAME, body = {}): QueryJson {
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint(table, "CREATE"),
|
endpoint: endpoint(table, "CREATE"),
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
body,
|
body,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +71,15 @@ function generateUpdateJson({
|
||||||
body = {},
|
body = {},
|
||||||
filters = {},
|
filters = {},
|
||||||
meta = {},
|
meta = {},
|
||||||
|
}: {
|
||||||
|
table: string
|
||||||
|
body?: any
|
||||||
|
filters?: any
|
||||||
|
meta?: any
|
||||||
}): QueryJson {
|
}): QueryJson {
|
||||||
|
if (!meta.table) {
|
||||||
|
meta.table = TABLE
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint(table, "UPDATE"),
|
endpoint: endpoint(table, "UPDATE"),
|
||||||
filters,
|
filters,
|
||||||
|
@ -70,6 +91,9 @@ function generateUpdateJson({
|
||||||
function generateDeleteJson(table = TABLE_NAME, filters = {}): QueryJson {
|
function generateDeleteJson(table = TABLE_NAME, filters = {}): QueryJson {
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint(table, "DELETE"),
|
endpoint: endpoint(table, "DELETE"),
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
filters,
|
filters,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +126,9 @@ function generateRelationshipJson(config: { schema?: string } = {}): QueryJson {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
extra: { idFilter: {} },
|
extra: { idFilter: {} },
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +163,9 @@ function generateManyRelationshipJson(config: { schema?: string } = {}) {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
extra: { idFilter: {} },
|
extra: { idFilter: {} },
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,7 +349,7 @@ describe("SQL query builder", () => {
|
||||||
)
|
)
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [date, limit],
|
bindings: [date, limit],
|
||||||
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"`,
|
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" >= $1 limit $2) as "${TABLE_NAME}"`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -338,7 +368,7 @@ describe("SQL query builder", () => {
|
||||||
)
|
)
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [date, limit],
|
bindings: [date, limit],
|
||||||
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" < $1 limit $2) as "${TABLE_NAME}"`,
|
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" <= $1 limit $2) as "${TABLE_NAME}"`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -572,7 +602,7 @@ describe("SQL query builder", () => {
|
||||||
)
|
)
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: ["2000-01-01 00:00:00", 500],
|
bindings: ["2000-01-01 00:00:00", 500],
|
||||||
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" > $1 limit $2) as "${TABLE_NAME}"`,
|
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" >= $1 limit $2) as "${TABLE_NAME}"`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -591,7 +621,7 @@ describe("SQL query builder", () => {
|
||||||
)
|
)
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: ["2010-01-01 00:00:00", 500],
|
bindings: ["2010-01-01 00:00:00", 500],
|
||||||
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" < $1 limit $2) as "${TABLE_NAME}"`,
|
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."dob" <= $1 limit $2) as "${TABLE_NAME}"`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,26 @@ import {
|
||||||
QueryJson,
|
QueryJson,
|
||||||
SourceName,
|
SourceName,
|
||||||
SqlQuery,
|
SqlQuery,
|
||||||
|
Table,
|
||||||
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import Sql from "../base/sql"
|
import Sql from "../base/sql"
|
||||||
import { SqlClient } from "../utils"
|
import { SqlClient } from "../utils"
|
||||||
import AliasTables from "../../api/controllers/row/alias"
|
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
|
// this doesn't exist strictly
|
||||||
|
const TABLE: Table = {
|
||||||
|
type: "table",
|
||||||
|
sourceType: TableSourceType.EXTERNAL,
|
||||||
|
sourceId: "SOURCE_ID",
|
||||||
|
schema: {},
|
||||||
|
name: "tableName",
|
||||||
|
primary: ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const AliasTables = sdk.rows.AliasTables
|
||||||
|
|
||||||
function multiline(sql: string) {
|
function multiline(sql: string) {
|
||||||
return sql.replace(/\n/g, "").replace(/ +/g, " ")
|
return sql.replace(/\n/g, "").replace(/ +/g, " ")
|
||||||
|
@ -103,7 +117,8 @@ describe("Captures of real examples", () => {
|
||||||
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
|
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
|
||||||
const filters = queryJson.filters
|
const filters = queryJson.filters
|
||||||
const notEqualsValue = Object.values(filters?.notEqual!)[0]
|
const notEqualsValue = Object.values(filters?.notEqual!)[0]
|
||||||
const rangeValue = Object.values(filters?.range!)[0]
|
const rangeValue: { high?: string | number; low?: string | number } =
|
||||||
|
Object.values(filters?.range!)[0]
|
||||||
const equalValue = Object.values(filters?.equal!)[0]
|
const equalValue = Object.values(filters?.equal!)[0]
|
||||||
|
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
|
@ -220,6 +235,9 @@ describe("Captures of real examples", () => {
|
||||||
resource: {
|
resource: {
|
||||||
fields,
|
fields,
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
table: TABLE,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./utils"
|
||||||
|
export { SqlStatements } from "./sqlStatements"
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { FieldType, Table, FieldSchema } from "@budibase/types"
|
||||||
|
import { SqlClient } from "./utils"
|
||||||
|
import { Knex } from "knex"
|
||||||
|
|
||||||
|
export class SqlStatements {
|
||||||
|
client: string
|
||||||
|
table: Table
|
||||||
|
allOr: boolean | undefined
|
||||||
|
constructor(
|
||||||
|
client: string,
|
||||||
|
table: Table,
|
||||||
|
{ allOr }: { allOr?: boolean } = {}
|
||||||
|
) {
|
||||||
|
this.client = client
|
||||||
|
this.table = table
|
||||||
|
this.allOr = allOr
|
||||||
|
}
|
||||||
|
|
||||||
|
getField(key: string): FieldSchema | undefined {
|
||||||
|
const fieldName = key.split(".")[1]
|
||||||
|
return this.table.schema[fieldName]
|
||||||
|
}
|
||||||
|
|
||||||
|
between(
|
||||||
|
query: Knex.QueryBuilder,
|
||||||
|
key: string,
|
||||||
|
low: number | string,
|
||||||
|
high: number | string
|
||||||
|
) {
|
||||||
|
// Use a between operator if we have 2 valid range values
|
||||||
|
const field = this.getField(key)
|
||||||
|
if (
|
||||||
|
field?.type === FieldType.BIGINT &&
|
||||||
|
this.client === SqlClient.SQL_LITE
|
||||||
|
) {
|
||||||
|
query = query.whereRaw(
|
||||||
|
`CAST(${key} AS INTEGER) BETWEEN CAST(? AS INTEGER) AND CAST(? AS INTEGER)`,
|
||||||
|
[low, high]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const fnc = this.allOr ? "orWhereBetween" : "whereBetween"
|
||||||
|
query = query[fnc](key, [low, high])
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
lte(query: Knex.QueryBuilder, key: string, low: number | string) {
|
||||||
|
// Use just a single greater than operator if we only have a low
|
||||||
|
const field = this.getField(key)
|
||||||
|
if (
|
||||||
|
field?.type === FieldType.BIGINT &&
|
||||||
|
this.client === SqlClient.SQL_LITE
|
||||||
|
) {
|
||||||
|
query = query.whereRaw(`CAST(${key} AS INTEGER) >= CAST(? AS INTEGER)`, [
|
||||||
|
low,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
const fnc = this.allOr ? "orWhere" : "where"
|
||||||
|
query = query[fnc](key, ">=", low)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
gte(query: Knex.QueryBuilder, key: string, high: number | string) {
|
||||||
|
const field = this.getField(key)
|
||||||
|
// Use just a single less than operator if we only have a high
|
||||||
|
if (
|
||||||
|
field?.type === FieldType.BIGINT &&
|
||||||
|
this.client === SqlClient.SQL_LITE
|
||||||
|
) {
|
||||||
|
query = query.whereRaw(`CAST(${key} AS INTEGER) <= CAST(? AS INTEGER)`, [
|
||||||
|
high,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
const fnc = this.allOr ? "orWhere" : "where"
|
||||||
|
query = query[fnc](key, "<=", high)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,18 +4,45 @@ import {
|
||||||
Datasource,
|
Datasource,
|
||||||
FieldType,
|
FieldType,
|
||||||
TableSourceType,
|
TableSourceType,
|
||||||
|
FieldSchema,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { DocumentType, SEPARATOR } from "../db/utils"
|
import { context, objectStore } from "@budibase/backend-core"
|
||||||
import { InvalidColumns, DEFAULT_BB_DATASOURCE_ID } from "../constants"
|
import { v4 } from "uuid"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { parseStringPromise as xmlParser } from "xml2js"
|
||||||
import env from "../environment"
|
import { formatBytes } from "../../utilities"
|
||||||
|
import bl from "bl"
|
||||||
|
import env from "../../environment"
|
||||||
|
import { DocumentType, SEPARATOR } from "../../db/utils"
|
||||||
|
import { InvalidColumns, DEFAULT_BB_DATASOURCE_ID } from "../../constants"
|
||||||
|
import { helpers, utils } from "@budibase/shared-core"
|
||||||
import { Knex } from "knex"
|
import { Knex } from "knex"
|
||||||
|
|
||||||
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
||||||
const ROW_ID_REGEX = /^\[.*]$/g
|
const ROW_ID_REGEX = /^\[.*]$/g
|
||||||
const ENCODED_SPACE = encodeURIComponent(" ")
|
const ENCODED_SPACE = encodeURIComponent(" ")
|
||||||
|
|
||||||
const SQL_NUMBER_TYPE_MAP = {
|
type PrimitiveTypes =
|
||||||
|
| FieldType.STRING
|
||||||
|
| FieldType.NUMBER
|
||||||
|
| FieldType.BOOLEAN
|
||||||
|
| FieldType.DATETIME
|
||||||
|
| FieldType.JSON
|
||||||
|
| FieldType.BIGINT
|
||||||
|
| FieldType.OPTIONS
|
||||||
|
|
||||||
|
function isPrimitiveType(type: FieldType): type is PrimitiveTypes {
|
||||||
|
return [
|
||||||
|
FieldType.STRING,
|
||||||
|
FieldType.NUMBER,
|
||||||
|
FieldType.BOOLEAN,
|
||||||
|
FieldType.DATETIME,
|
||||||
|
FieldType.JSON,
|
||||||
|
FieldType.BIGINT,
|
||||||
|
FieldType.OPTIONS,
|
||||||
|
].includes(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SQL_NUMBER_TYPE_MAP: Record<string, PrimitiveTypes> = {
|
||||||
integer: FieldType.NUMBER,
|
integer: FieldType.NUMBER,
|
||||||
int: FieldType.NUMBER,
|
int: FieldType.NUMBER,
|
||||||
decimal: FieldType.NUMBER,
|
decimal: FieldType.NUMBER,
|
||||||
|
@ -35,7 +62,7 @@ const SQL_NUMBER_TYPE_MAP = {
|
||||||
smallmoney: FieldType.NUMBER,
|
smallmoney: FieldType.NUMBER,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SQL_DATE_TYPE_MAP = {
|
const SQL_DATE_TYPE_MAP: Record<string, PrimitiveTypes> = {
|
||||||
timestamp: FieldType.DATETIME,
|
timestamp: FieldType.DATETIME,
|
||||||
time: FieldType.DATETIME,
|
time: FieldType.DATETIME,
|
||||||
datetime: FieldType.DATETIME,
|
datetime: FieldType.DATETIME,
|
||||||
|
@ -46,7 +73,7 @@ const SQL_DATE_TYPE_MAP = {
|
||||||
const SQL_DATE_ONLY_TYPES = ["date"]
|
const SQL_DATE_ONLY_TYPES = ["date"]
|
||||||
const SQL_TIME_ONLY_TYPES = ["time"]
|
const SQL_TIME_ONLY_TYPES = ["time"]
|
||||||
|
|
||||||
const SQL_STRING_TYPE_MAP = {
|
const SQL_STRING_TYPE_MAP: Record<string, PrimitiveTypes> = {
|
||||||
varchar: FieldType.STRING,
|
varchar: FieldType.STRING,
|
||||||
char: FieldType.STRING,
|
char: FieldType.STRING,
|
||||||
nchar: FieldType.STRING,
|
nchar: FieldType.STRING,
|
||||||
|
@ -58,22 +85,22 @@ const SQL_STRING_TYPE_MAP = {
|
||||||
text: FieldType.STRING,
|
text: FieldType.STRING,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SQL_BOOLEAN_TYPE_MAP = {
|
const SQL_BOOLEAN_TYPE_MAP: Record<string, PrimitiveTypes> = {
|
||||||
boolean: FieldType.BOOLEAN,
|
boolean: FieldType.BOOLEAN,
|
||||||
bit: FieldType.BOOLEAN,
|
bit: FieldType.BOOLEAN,
|
||||||
tinyint: FieldType.BOOLEAN,
|
tinyint: FieldType.BOOLEAN,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SQL_OPTIONS_TYPE_MAP = {
|
const SQL_OPTIONS_TYPE_MAP: Record<string, PrimitiveTypes> = {
|
||||||
"user-defined": FieldType.OPTIONS,
|
"user-defined": FieldType.OPTIONS,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SQL_MISC_TYPE_MAP = {
|
const SQL_MISC_TYPE_MAP: Record<string, PrimitiveTypes> = {
|
||||||
json: FieldType.JSON,
|
json: FieldType.JSON,
|
||||||
bigint: FieldType.BIGINT,
|
bigint: FieldType.BIGINT,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SQL_TYPE_MAP = {
|
const SQL_TYPE_MAP: Record<string, PrimitiveTypes> = {
|
||||||
...SQL_NUMBER_TYPE_MAP,
|
...SQL_NUMBER_TYPE_MAP,
|
||||||
...SQL_DATE_TYPE_MAP,
|
...SQL_DATE_TYPE_MAP,
|
||||||
...SQL_STRING_TYPE_MAP,
|
...SQL_STRING_TYPE_MAP,
|
||||||
|
@ -239,14 +266,14 @@ export function generateColumnDefinition(config: {
|
||||||
constraints.inclusion = options
|
constraints.inclusion = options
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema: any = {
|
const schema: FieldSchema = {
|
||||||
type: foundType,
|
type: foundType,
|
||||||
externalType,
|
externalType,
|
||||||
autocolumn,
|
autocolumn,
|
||||||
name,
|
name,
|
||||||
constraints,
|
constraints,
|
||||||
}
|
}
|
||||||
if (foundType === FieldType.DATETIME) {
|
if (schema.type === FieldType.DATETIME) {
|
||||||
schema.dateOnly = SQL_DATE_ONLY_TYPES.includes(lowerCaseType)
|
schema.dateOnly = SQL_DATE_ONLY_TYPES.includes(lowerCaseType)
|
||||||
schema.timeOnly = SQL_TIME_ONLY_TYPES.includes(lowerCaseType)
|
schema.timeOnly = SQL_TIME_ONLY_TYPES.includes(lowerCaseType)
|
||||||
}
|
}
|
||||||
|
@ -274,59 +301,6 @@ export function isIsoDateString(str: string) {
|
||||||
return d.toISOString() === trimmedValue
|
return d.toISOString() === trimmedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function will determine whether a column is a relationship and whether it
|
|
||||||
* is currently valid. The reason for the validity check is that tables can be deleted
|
|
||||||
* outside of Budibase control and if this is the case it will break Budibase relationships.
|
|
||||||
* The tableIds is a list passed down from the main finalise tables function, which is
|
|
||||||
* based on the tables that have just been fetched. This will only really be used on subsequent
|
|
||||||
* fetches to the first one - if the user is periodically refreshing Budibase knowledge of tables.
|
|
||||||
* @param column The column to check, to see if it is a valid relationship.
|
|
||||||
* @param tableIds The IDs of the tables which currently exist.
|
|
||||||
*/
|
|
||||||
export function shouldCopyRelationship(
|
|
||||||
column: { type: string; tableId?: string },
|
|
||||||
tableIds: string[]
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
column.type === FieldType.LINK &&
|
|
||||||
column.tableId &&
|
|
||||||
tableIds.includes(column.tableId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Similar function to the shouldCopyRelationship function, but instead this looks for options and boolean
|
|
||||||
* types. It is possible to switch a string -> options and a number -> boolean (and vice versus) need to make
|
|
||||||
* sure that these get copied over when tables are fetched. Also checks whether they are still valid, if a
|
|
||||||
* column has changed type in the external database then copying it over may not be possible.
|
|
||||||
* @param column The column to check for options or boolean type.
|
|
||||||
* @param fetchedColumn The fetched column to check for the type in the external database.
|
|
||||||
*/
|
|
||||||
export function shouldCopySpecialColumn(
|
|
||||||
column: { type: string },
|
|
||||||
fetchedColumn: { type: string } | undefined
|
|
||||||
) {
|
|
||||||
const isFormula = column.type === FieldType.FORMULA
|
|
||||||
const specialTypes = [
|
|
||||||
FieldType.OPTIONS,
|
|
||||||
FieldType.LONGFORM,
|
|
||||||
FieldType.ARRAY,
|
|
||||||
FieldType.FORMULA,
|
|
||||||
FieldType.BB_REFERENCE,
|
|
||||||
]
|
|
||||||
// column has been deleted, remove - formulas will never exist, always copy
|
|
||||||
if (!isFormula && column && !fetchedColumn) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const fetchedIsNumber =
|
|
||||||
!fetchedColumn || fetchedColumn.type === FieldType.NUMBER
|
|
||||||
return (
|
|
||||||
specialTypes.indexOf(column.type as FieldType) !== -1 ||
|
|
||||||
(fetchedIsNumber && column.type === FieldType.BOOLEAN)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Looks for columns which need to be copied over into the new table definitions, like relationships,
|
* Looks for columns which need to be copied over into the new table definitions, like relationships,
|
||||||
* options types and views.
|
* options types and views.
|
||||||
|
@ -348,6 +322,9 @@ function copyExistingPropsOver(
|
||||||
if (entities[tableName]?.created) {
|
if (entities[tableName]?.created) {
|
||||||
table.created = entities[tableName]?.created
|
table.created = entities[tableName]?.created
|
||||||
}
|
}
|
||||||
|
if (entities[tableName]?.constrained) {
|
||||||
|
table.constrained = entities[tableName]?.constrained
|
||||||
|
}
|
||||||
|
|
||||||
table.views = entities[tableName].views
|
table.views = entities[tableName].views
|
||||||
|
|
||||||
|
@ -356,12 +333,73 @@ function copyExistingPropsOver(
|
||||||
if (!Object.prototype.hasOwnProperty.call(existingTableSchema, key)) {
|
if (!Object.prototype.hasOwnProperty.call(existingTableSchema, key)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const column = existingTableSchema[key]
|
const column = existingTableSchema[key]
|
||||||
if (
|
|
||||||
shouldCopyRelationship(column, tableIds) ||
|
const existingColumnType = column?.type
|
||||||
shouldCopySpecialColumn(column, table.schema[key])
|
const updatedColumnType = table.schema[key]?.type
|
||||||
) {
|
|
||||||
table.schema[key] = existingTableSchema[key]
|
const keepIfType = (...validTypes: PrimitiveTypes[]) => {
|
||||||
|
return (
|
||||||
|
isPrimitiveType(updatedColumnType) &&
|
||||||
|
table.schema[key] &&
|
||||||
|
validTypes.includes(updatedColumnType)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldKeepSchema = false
|
||||||
|
switch (existingColumnType) {
|
||||||
|
case FieldType.FORMULA:
|
||||||
|
case FieldType.AUTO:
|
||||||
|
case FieldType.INTERNAL:
|
||||||
|
shouldKeepSchema = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case FieldType.LINK:
|
||||||
|
shouldKeepSchema =
|
||||||
|
existingColumnType === FieldType.LINK &&
|
||||||
|
tableIds.includes(column.tableId)
|
||||||
|
break
|
||||||
|
|
||||||
|
case FieldType.STRING:
|
||||||
|
case FieldType.OPTIONS:
|
||||||
|
case FieldType.LONGFORM:
|
||||||
|
case FieldType.BARCODEQR:
|
||||||
|
shouldKeepSchema = keepIfType(FieldType.STRING)
|
||||||
|
break
|
||||||
|
|
||||||
|
case FieldType.NUMBER:
|
||||||
|
case FieldType.BOOLEAN:
|
||||||
|
shouldKeepSchema = keepIfType(FieldType.BOOLEAN, FieldType.NUMBER)
|
||||||
|
break
|
||||||
|
|
||||||
|
case FieldType.ARRAY:
|
||||||
|
case FieldType.ATTACHMENTS:
|
||||||
|
case FieldType.ATTACHMENT_SINGLE:
|
||||||
|
case FieldType.JSON:
|
||||||
|
case FieldType.BB_REFERENCE:
|
||||||
|
shouldKeepSchema = keepIfType(FieldType.JSON, FieldType.STRING)
|
||||||
|
break
|
||||||
|
|
||||||
|
case FieldType.DATETIME:
|
||||||
|
shouldKeepSchema = keepIfType(FieldType.DATETIME, FieldType.STRING)
|
||||||
|
break
|
||||||
|
|
||||||
|
case FieldType.BIGINT:
|
||||||
|
shouldKeepSchema = keepIfType(FieldType.BIGINT, FieldType.NUMBER)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
utils.unreachable(existingColumnType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldKeepSchema) {
|
||||||
|
table.schema[key] = {
|
||||||
|
...existingTableSchema[key],
|
||||||
|
externalType:
|
||||||
|
existingTableSchema[key].externalType ||
|
||||||
|
table.schema[key]?.externalType,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,3 +472,74 @@ export function getPrimaryDisplay(testValue: unknown): string | undefined {
|
||||||
export function isValidFilter(value: any) {
|
export function isValidFilter(value: any) {
|
||||||
return value != null && value !== ""
|
return value != null && value !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function handleXml(response: any) {
|
||||||
|
let data,
|
||||||
|
rawXml = await response.text()
|
||||||
|
data =
|
||||||
|
(await xmlParser(rawXml, {
|
||||||
|
explicitArray: false,
|
||||||
|
trim: true,
|
||||||
|
explicitRoot: false,
|
||||||
|
})) || {}
|
||||||
|
// there is only one structure, its an array, return the array so it appears as rows
|
||||||
|
const keys = Object.keys(data)
|
||||||
|
if (keys.length === 1 && Array.isArray(data[keys[0]])) {
|
||||||
|
data = data[keys[0]]
|
||||||
|
}
|
||||||
|
return { data, rawXml }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleFileResponse(
|
||||||
|
response: any,
|
||||||
|
filename: string,
|
||||||
|
startTime: number
|
||||||
|
) {
|
||||||
|
let presignedUrl,
|
||||||
|
size = 0
|
||||||
|
const fileExtension = filename.includes(".")
|
||||||
|
? filename.split(".").slice(1).join(".")
|
||||||
|
: ""
|
||||||
|
|
||||||
|
const processedFileName = `${v4()}.${fileExtension}`
|
||||||
|
const key = `${context.getProdAppId()}/${processedFileName}`
|
||||||
|
const bucket = objectStore.ObjectStoreBuckets.TEMP
|
||||||
|
|
||||||
|
const stream = response.body.pipe(bl((error, data) => data))
|
||||||
|
|
||||||
|
if (response.body) {
|
||||||
|
const contentLength = response.headers.get("content-length")
|
||||||
|
if (contentLength) {
|
||||||
|
size = parseInt(contentLength, 10)
|
||||||
|
} else {
|
||||||
|
const chunks: Buffer[] = []
|
||||||
|
for await (const chunk of response.body) {
|
||||||
|
chunks.push(chunk)
|
||||||
|
size += chunk.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await objectStore.streamUpload({
|
||||||
|
bucket,
|
||||||
|
filename: key,
|
||||||
|
stream,
|
||||||
|
ttl: 1,
|
||||||
|
type: response.headers["content-type"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
presignedUrl = await objectStore.getPresignedUrl(bucket, key)
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
size,
|
||||||
|
name: processedFileName,
|
||||||
|
url: presignedUrl,
|
||||||
|
extension: fileExtension,
|
||||||
|
key: key,
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
code: response.status,
|
||||||
|
size: formatBytes(size.toString()),
|
||||||
|
time: `${Math.round(performance.now() - startTime)}ms`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,6 @@ export function init() {
|
||||||
// Because we can't pass functions into an Isolate, we remove them from
|
// Because we can't pass functions into an Isolate, we remove them from
|
||||||
// the passed context and rely on the withHelpers() method to add them
|
// the passed context and rely on the withHelpers() method to add them
|
||||||
// back in.
|
// back in.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { helpers, snippets, ...rest } = ctx
|
const { helpers, snippets, ...rest } = ctx
|
||||||
return vm.withContext(rest, () => vm.execute(js))
|
return vm.withContext(rest, () => vm.execute(js))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
@ -348,8 +348,7 @@ const preSaveAction: Partial<Record<SourceName, any>> = {
|
||||||
* Make sure all datasource entities have a display name selected
|
* Make sure all datasource entities have a display name selected
|
||||||
*/
|
*/
|
||||||
export function setDefaultDisplayColumns(datasource: Datasource) {
|
export function setDefaultDisplayColumns(datasource: Datasource) {
|
||||||
//
|
for (const entity of Object.values(datasource.entities || {})) {
|
||||||
for (let entity of Object.values(datasource.entities || {})) {
|
|
||||||
if (entity.primaryDisplay) {
|
if (entity.primaryDisplay) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as rows from "./rows"
|
||||||
import * as search from "./search"
|
import * as search from "./search"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import * as external from "./external"
|
import * as external from "./external"
|
||||||
|
import AliasTables from "./sqlAlias"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...attachments,
|
...attachments,
|
||||||
|
@ -10,4 +11,5 @@ export default {
|
||||||
...search,
|
...search,
|
||||||
utils,
|
utils,
|
||||||
external,
|
external,
|
||||||
|
AliasTables,
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import * as sqs from "./search/sqs"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
import sdk from "../../index"
|
||||||
|
import { searchInputMapping } from "./search/utils"
|
||||||
|
|
||||||
export { isValidFilter } from "../../../integrations/utils"
|
export { isValidFilter } from "../../../integrations/utils"
|
||||||
|
|
||||||
|
@ -76,12 +78,15 @@ export async function search(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const table = await sdk.tables.getTable(options.tableId)
|
||||||
|
options = searchInputMapping(table, options)
|
||||||
|
|
||||||
if (isExternalTable) {
|
if (isExternalTable) {
|
||||||
return external.search(options)
|
return external.search(options, table)
|
||||||
} else if (env.SQS_SEARCH_ENABLE) {
|
} else if (env.SQS_SEARCH_ENABLE) {
|
||||||
return sqs.search(options)
|
return sqs.search(options, table)
|
||||||
} else {
|
} else {
|
||||||
return internal.search(options)
|
return internal.search(options, table)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
RowSearchParams,
|
RowSearchParams,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as exporters from "../../../../api/controllers/view/exporters"
|
import * as exporters from "../../../../api/controllers/view/exporters"
|
||||||
import { handleRequest } from "../../../../api/controllers/row/external"
|
import { handleRequest } from "../../../../api/controllers/row/external"
|
||||||
|
@ -18,13 +19,13 @@ import {
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "./types"
|
import { ExportRowsParams, ExportRowsResult } from "./types"
|
||||||
import { HTTPError, db } from "@budibase/backend-core"
|
import { HTTPError, db } from "@budibase/backend-core"
|
||||||
import { searchInputMapping } from "./utils"
|
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
||||||
import sdk from "../../../"
|
import sdk from "../../../"
|
||||||
|
|
||||||
export async function search(
|
export async function search(
|
||||||
options: RowSearchParams
|
options: RowSearchParams,
|
||||||
|
table: Table
|
||||||
): Promise<SearchResponse<Row>> {
|
): Promise<SearchResponse<Row>> {
|
||||||
const { tableId } = options
|
const { tableId } = options
|
||||||
const { paginate, query, ...params } = options
|
const { paginate, query, ...params } = options
|
||||||
|
@ -68,8 +69,6 @@ export async function search(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const table = await sdk.tables.getTable(tableId)
|
|
||||||
options = searchInputMapping(table, options)
|
|
||||||
let rows = await handleRequest(Operation.READ, tableId, {
|
let rows = await handleRequest(Operation.READ, tableId, {
|
||||||
filters: query,
|
filters: query,
|
||||||
sort,
|
sort,
|
||||||
|
@ -150,11 +149,15 @@ export async function exportRows(
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasource = await sdk.datasources.get(datasourceId!)
|
const datasource = await sdk.datasources.get(datasourceId!)
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
if (!datasource || !datasource.entities) {
|
if (!datasource || !datasource.entities) {
|
||||||
throw new HTTPError("Datasource has not been configured for plus API.", 400)
|
throw new HTTPError("Datasource has not been configured for plus API.", 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = await search({ tableId, query: requestQuery, sort, sortOrder })
|
let result = await search(
|
||||||
|
{ tableId, query: requestQuery, sort, sortOrder },
|
||||||
|
table
|
||||||
|
)
|
||||||
let rows: Row[] = []
|
let rows: Row[] = []
|
||||||
let headers
|
let headers
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue