Merge branch 'develop' of github.com:Budibase/budibase into chore/esbuild

This commit is contained in:
mike12345567 2023-04-13 17:23:58 +01:00
commit 361005b521
342 changed files with 10333 additions and 8014 deletions

View File

@ -56,7 +56,6 @@ jobs:
run: yarn install:pro $BRANCH $BASE_BRANCH
- run: yarn
- run: yarn bootstrap
- run: yarn build:client
- run: yarn test
- uses: codecov/codecov-action@v3
with:
@ -80,14 +79,6 @@ jobs:
integration-test:
runs-on: ubuntu-latest
services:
couchdb:
image: ibmcom/couchdb3
env:
COUCHDB_PASSWORD: budibase
COUCHDB_USER: budibase
ports:
- 4567:5984
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
@ -96,10 +87,11 @@ jobs:
node-version: 14.x
- name: Install Pro
run: yarn install:pro $BRANCH $BASE_BRANCH
- run: yarn
- run: yarn bootstrap
- run: yarn build
- run: yarn && yarn bootstrap && yarn build
- run: |
cd qa-core
yarn
yarn api:test:ci
yarn setup
yarn test:ci
env:
BB_ADMIN_USER_EMAIL: admin
BB_ADMIN_USER_PASSWORD: admin

View File

@ -107,7 +107,7 @@ jobs:
git pull
mkdir sync
echo "Packaging chart to sync dir"
helm package charts/budibase --version 0.0.0-master --app-version "$RELEASE_VERSION" --destination sync
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
echo "Packaging successful"
git checkout gh-pages
echo "Indexing helm repo"

14
.nxignore Normal file
View File

@ -0,0 +1,14 @@
node_modules
**/node_modules
**/dist/
**/.routify/
**/coverage/
**/yarn-error.log
**/prebuilds/
**/build/
packages/server/builder/*
packages/server/client/*

View File

@ -64,6 +64,8 @@ spec:
value: {{ .Values.globals.enableAnalytics | quote }}
- name: API_ENCRYPTION_KEY
value: {{ .Values.globals.apiEncryptionKey | quote }}
- name: HTTP_LOGGING
value: {{ .Values.services.apps.httpLogging | quote }}
- name: INTERNAL_API_KEY
valueFrom:
secretKeyRef:
@ -119,7 +121,7 @@ spec:
- name: MULTI_TENANCY
value: {{ .Values.globals.multiTenancy | quote }}
- name: LOG_LEVEL
value: {{ default "info" .Values.services.apps.logLevel | quote }}
value: {{ .Values.services.apps.logLevel | quote }}
- name: REDIS_PASSWORD
value: {{ .Values.services.redis.password }}
- name: REDIS_URL
@ -180,18 +182,6 @@ spec:
- name: DD_APM_DD_URL
value: https://trace.agent.datadoghq.eu
{{ end }}
{{ if .Values.globals.elasticApmEnabled }}
- name: ELASTIC_APM_ENABLED
value: {{ .Values.globals.elasticApmEnabled | quote }}
{{ end }}
{{ if .Values.globals.elasticApmSecretToken }}
- name: ELASTIC_APM_SECRET_TOKEN
value: {{ .Values.globals.elasticApmSecretToken | quote }}
{{ end }}
{{ if .Values.globals.elasticApmServerUrl }}
- name: ELASTIC_APM_SERVER_URL
value: {{ .Values.globals.elasticApmServerUrl | quote }}
{{ end }}
{{ if .Values.globals.globalAgentHttpProxy }}
- name: GLOBAL_AGENT_HTTP_PROXY
value: {{ .Values.globals.globalAgentHttpProxy | quote }}
@ -209,7 +199,7 @@ spec:
value: {{ .Values.services.tlsRejectUnauthorized }}
{{ end }}
image: budibase/apps:{{ .Values.globals.appVersion }}
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@ -37,7 +37,7 @@ spec:
{{ end }}
spec:
containers:
- image: budibase/proxy:{{ .Values.globals.appVersion }}
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always
name: proxy-service
ports:

View File

@ -64,6 +64,8 @@ spec:
{{ end }}
- name: API_ENCRYPTION_KEY
value: {{ .Values.globals.apiEncryptionKey | quote }}
- name: HTTP_LOGGING
value: {{ .Values.services.worker.httpLogging | quote }}
- name: INTERNAL_API_KEY
valueFrom:
secretKeyRef:
@ -115,7 +117,7 @@ spec:
- name: MULTI_TENANCY
value: {{ .Values.globals.multiTenancy | quote }}
- name: LOG_LEVEL
value: {{ default "info" .Values.services.worker.logLevel | quote }}
value: {{ .Values.services.worker.logLevel | quote }}
- name: REDIS_PASSWORD
value: {{ .Values.services.redis.password | quote }}
- name: REDIS_URL
@ -170,18 +172,6 @@ spec:
- name: DD_APM_DD_URL
value: https://trace.agent.datadoghq.eu
{{ end }}
{{ if .Values.globals.elasticApmEnabled }}
- name: ELASTIC_APM_ENABLED
value: {{ .Values.globals.elasticApmEnabled | quote }}
{{ end }}
{{ if .Values.globals.elasticApmSecretToken }}
- name: ELASTIC_APM_SECRET_TOKEN
value: {{ .Values.globals.elasticApmSecretToken | quote }}
{{ end }}
{{ if .Values.globals.elasticApmServerUrl }}
- name: ELASTIC_APM_SERVER_URL
value: {{ .Values.globals.elasticApmServerUrl | quote }}
{{ end }}
{{ if .Values.globals.globalAgentHttpProxy }}
- name: GLOBAL_AGENT_HTTP_PROXY
value: {{ .Values.globals.globalAgentHttpProxy | quote }}
@ -198,8 +188,7 @@ spec:
- name: NODE_TLS_REJECT_UNAUTHORIZED
value: {{ .Values.services.tlsRejectUnauthorized }}
{{ end }}
image: budibase/worker:{{ .Values.globals.appVersion }}
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@ -74,13 +74,12 @@ tolerations: []
affinity: {}
globals:
appVersion: "latest"
appVersion: "" # Use as an override to .Chart.AppVersion
budibaseEnv: PRODUCTION
tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
enableAnalytics: "1"
sentryDSN: ""
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
logLevel: info
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
accountPortalUrl: ""
@ -107,9 +106,6 @@ globals:
smtp:
enabled: false
# elasticApmEnabled:
# elasticApmSecretToken:
# elasticApmServerUrl:
# globalAgentHttpProxy:
# globalAgentHttpsProxy:
# globalAgentNoProxy:
@ -137,6 +133,7 @@ services:
port: 4002
replicaCount: 1
logLevel: info
httpLogging: 1
resources: {}
# nodeDebug: "" # set the value of NODE_DEBUG
# annotations:
@ -147,6 +144,8 @@ services:
worker:
port: 4003
replicaCount: 1
logLevel: info
httpLogging: 1
resources: {}
# annotations:
# co.elastic.logs/multiline.type: pattern

View File

@ -1,5 +1,5 @@
{
"version": "2.4.42-alpha.7",
"version": "2.4.44-alpha.20",
"npmClient": "yarn",
"useWorkspaces": true,
"packages": [
@ -14,6 +14,9 @@
"# We ignore every JSON file, except for built-in-modules, built-ins and plugins defined in babel-preset-env/data.",
"@(!(built-in-modules|built-ins|plugins|package)).json"
]
},
"run": {
"loadEnvFiles": false
}
}
}

10
nx.json Normal file
View File

@ -0,0 +1,10 @@
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test"]
}
}
}
}

View File

@ -15,7 +15,7 @@
"husky": "^7.0.1",
"js-yaml": "^4.1.0",
"kill-port": "^1.6.1",
"lerna": "^3.14.1",
"lerna": "^6.6.1",
"madge": "^6.0.0",
"prettier": "^2.3.1",
"prettier-plugin-svelte": "^2.3.0",
@ -26,12 +26,13 @@
},
"scripts": {
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
"build": "lerna run build",
"build:client": "lerna run build --ignore @budibase/backend-core --ignore @budibase/worker --ignore @budibase/server --ignore @budibase/builder --ignore @budibase/cli --ignore @budibase/sdk",
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
"build:backend": "lerna run build --ignore @budibase/client --ignore @budibase/bbui --ignore @budibase/builder --ignore @budibase/cli",
"build:sdk": "lerna run build:sdk",
"bootstrap": "lerna bootstrap",
"postbootstrap": "lerna link && ./scripts/link-dependencies.sh",
"build": "lerna run --stream build",
"build:dev": "lerna run --stream prebuild && tsc --build --watch --preserveWatchOutput",
"backend:bootstrap": "./scripts/scopeBackend.sh 'lerna bootstrap' && yarn run postbootstrap",
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
"build:sdk": "lerna run --stream build:sdk",
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
@ -40,15 +41,16 @@
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
"nuke:packages": "yarn run restore",
"nuke:docker": "lerna run --parallel dev:stack:nuke",
"nuke:docker": "lerna run --stream --parallel dev:stack:nuke",
"clean": "lerna clean",
"kill-builder": "kill-port 3000",
"kill-server": "kill-port 4001 4002",
"kill-all": "yarn run kill-builder && yarn run kill-server",
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
"test": "lerna run test",
"dev": "yarn run kill-all && lerna link && lerna run --stream --parallel dev:builder --concurrency 1 --stream",
"dev:noserver": "yarn run kill-builder && lerna link && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
"dev:built": "cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
"test": "lerna run --stream test --stream",
"test:pro": "bash scripts/pro/test.sh",
"lint:eslint": "eslint packages && eslint qa-core",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
@ -56,12 +58,12 @@
"lint:fix:eslint": "eslint --fix packages qa-core",
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"build:specs": "lerna run specs",
"build:docker": "lerna run build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:pre": "lerna run build && lerna run predocker",
"build:specs": "lerna run --stream specs",
"build:docker": "lerna run --stream build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:pre": "lerna run --stream build && lerna run --stream predocker",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
@ -70,16 +72,16 @@
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb",
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
"build:docs": "lerna run build:docs",
"build:docs": "lerna run --stream build:docs",
"release:helm": "node scripts/releaseHelmChart",
"env:multi:enable": "lerna run env:multi:enable",
"env:multi:disable": "lerna run env:multi:disable",
"env:selfhost:enable": "lerna run env:selfhost:enable",
"env:selfhost:disable": "lerna run env:selfhost:disable",
"env:multi:enable": "lerna run --stream env:multi:enable",
"env:multi:disable": "lerna run --stream env:multi:disable",
"env:selfhost:enable": "lerna run --stream env:selfhost:enable",
"env:selfhost:disable": "lerna run --stream env:selfhost:disable",
"env:localdomain:enable": "./scripts/localdomain.sh enable",
"env:localdomain:disable": "./scripts/localdomain.sh disable",
"env:account:enable": "lerna run env:account:enable",
"env:account:disable": "lerna run env:account:disable",
"env:account:enable": "lerna run --stream env:account:enable",
"env:account:disable": "lerna run --stream env:account:disable",
"mode:self": "yarn env:selfhost:enable && yarn env:multi:disable && yarn env:account:disable",
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
"mode:account": "yarn mode:cloud && yarn env:account:enable",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "2.4.42-alpha.7",
"version": "2.4.44-alpha.20",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -24,7 +24,7 @@
"dependencies": {
"@budibase/nano": "10.1.2",
"@budibase/pouchdb-replication-stream": "1.2.10",
"@budibase/types": "2.4.42-alpha.7",
"@budibase/types": "2.4.44-alpha.20",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0",
@ -39,6 +39,7 @@
"joi": "17.6.0",
"jsonwebtoken": "9.0.0",
"koa-passport": "4.1.4",
"koa-pino-logger": "4.0.0",
"lodash": "4.17.21",
"lodash.isarguments": "3.1.0",
"node-fetch": "2.6.7",
@ -56,6 +57,7 @@
"uuid": "8.3.2"
},
"devDependencies": {
"@jest/test-sequencer": "29.5.0",
"@swc/core": "^1.3.25",
"@swc/jest": "^0.2.24",
"@trendyol/jest-testcontainers": "^2.1.1",
@ -63,11 +65,9 @@
"@types/ioredis": "4.28.0",
"@types/jest": "28.1.1",
"@types/koa": "2.13.4",
"@types/koa-pino-logger": "3.0.0",
"@types/lodash": "4.14.180",
"@types/node": "14.18.20",
"@types/node-fetch": "2.6.1",
"@types/pino-http": "5.8.1",
"@types/pouchdb": "6.4.0",
"@types/redlock": "4.0.3",
"@types/semver": "7.3.7",
@ -75,13 +75,14 @@
"@types/uuid": "8.3.4",
"chance": "1.1.8",
"ioredis-mock": "5.8.0",
"jest": "28.1.1",
"jest": "29.5.0",
"jest-serial-runner": "^1.2.1",
"koa": "2.13.4",
"nodemon": "2.0.16",
"pino-pretty": "10.0.0",
"pouchdb-adapter-memory": "7.2.2",
"timekeeper": "2.2.0",
"ts-jest": "28.0.4",
"ts-jest": "29.0.5",
"ts-node": "10.8.1",
"tsconfig-paths": "4.0.0",
"typescript": "4.7.3"

View File

@ -1,4 +1,5 @@
#!/bin/bash
set -e
if [[ -n $CI ]]
then
@ -7,6 +8,6 @@ then
jest --coverage --runInBand --forceExit
else
# --maxWorkers performs better in development
echo "jest --coverage"
jest --coverage
fi
echo "jest --coverage --forceExit"
jest --coverage --forceExit
fi

View File

@ -1,4 +1,5 @@
import { structures, testEnv } from "../../../tests"
import { structures } from "../../../tests"
import { testEnv } from "../../../tests/extra"
import * as auth from "../auth"
import * as events from "../../events"

View File

@ -1,7 +1,8 @@
import { DBTestConfiguration } from "../../../tests/extra"
import {
structures,
DBTestConfiguration,
expectFunctionWasCalledTimesWith,
mocks,
} from "../../../tests"
import { Writethrough } from "../writethrough"
import { getDB } from "../../db"
@ -77,9 +78,9 @@ describe("writethrough", () => {
expect.arrayContaining([current._rev, current._rev, newRev])
)
expectFunctionWasCalledTimesWith(
console.warn,
mocks.alerts.logWarn,
2,
"bb-warn: Ignoring redlock conflict in write-through cache"
"Ignoring redlock conflict in write-through cache"
)
const output = await db.get(current._id)

View File

@ -5,6 +5,8 @@ import {
GoogleInnerConfig,
OIDCConfig,
OIDCInnerConfig,
SCIMConfig,
SCIMInnerConfig,
SettingsConfig,
SettingsInnerConfig,
SMTPConfig,
@ -32,8 +34,7 @@ export async function getConfig<T extends Config>(
const db = context.getGlobalDB()
try {
// await to catch error
const config = (await db.get(generateConfigID(type))) as T
return config
return (await db.get(generateConfigID(type))) as T
} catch (e: any) {
if (e.status === 404) {
return
@ -242,3 +243,10 @@ export async function getSMTPConfig(
}
}
}
// SCIM
export async function getSCIMConfig(): Promise<SCIMInnerConfig | undefined> {
const config = await getConfig<SCIMConfig>(ConfigType.SCIM)
return config?.config
}

View File

@ -1,9 +1,5 @@
import {
DBTestConfiguration,
generator,
testEnv,
structures,
} from "../../../tests"
import { generator, structures } from "../../../tests"
import { DBTestConfiguration, testEnv } from "../../../tests/extra"
import { ConfigType } from "@budibase/types"
import env from "../../environment"
import * as configs from "../configs"

View File

@ -22,6 +22,7 @@ export enum Header {
TOKEN = "x-budibase-token",
CSRF_TOKEN = "x-csrf-token",
CORRELATION_ID = "x-budibase-correlation-id",
AUTHORIZATION = "authorization",
}
export enum GlobalRole {
@ -38,6 +39,7 @@ export enum Config {
GOOGLE = "google",
OIDC = "oidc",
OIDC_LOGOS = "logos_oidc",
SCIM = "scim",
}
export const MIN_VALID_DATE = new Date(-2147483647000)

View File

@ -2,7 +2,7 @@
// store an app ID to pretend there is a context
import env from "../environment"
import Context from "./Context"
import * as conversions from "../db/conversions"
import * as conversions from "../docIds/conversions"
import { getDB } from "../db/db"
import {
DocumentType,
@ -43,8 +43,12 @@ export function baseGlobalDBName(tenantId: string | undefined | null) {
}
}
export function getPlatformURL() {
return env.PLATFORM_URL
}
export function isMultiTenant() {
return env.MULTI_TENANCY
return !!env.MULTI_TENANCY
}
export function isTenantIdSet() {
@ -214,6 +218,13 @@ export function doInEnvironmentContext(
return newContext(updates, task)
}
export function doInScimContext(task: any) {
const updates: ContextMap = {
isScim: true,
}
return newContext(updates, task)
}
export function getEnvironmentVariables() {
const context = Context.get()
if (!context.environmentVariables) {
@ -270,3 +281,9 @@ export function getDevAppDB(opts?: any): Database {
}
return getDB(conversions.getDevelopmentAppID(appId), opts)
}
export function isScim(): boolean {
const context = Context.get()
const scimCall = context?.isScim
return !!scimCall
}

View File

@ -1,6 +1,6 @@
import { testEnv } from "../../../tests"
const context = require("../")
const { DEFAULT_TENANT_ID } = require("../../constants")
import { testEnv } from "../../../tests/extra"
import * as context from "../"
import { DEFAULT_TENANT_ID } from "../../constants"
describe("context", () => {
describe("doInTenant", () => {
@ -131,4 +131,17 @@ describe("context", () => {
})
})
})
describe("doInScimContext", () => {
it("returns true when set", () => {
context.doInScimContext(() => {
const isScim = context.isScim()
expect(isScim).toBe(true)
})
})
it("returns false when not set", () => {
const isScim = context.isScim()
expect(isScim).toBe(false)
})
})
})

View File

@ -6,4 +6,5 @@ export type ContextMap = {
appId?: string
identity?: IdentityContext
environmentVariables?: Record<string, string>
isScim?: boolean
}

View File

@ -15,7 +15,7 @@ import { getCouchInfo } from "./connections"
import { directCouchCall } from "./utils"
import { getPouchDB } from "./pouchDB"
import { WriteStream, ReadStream } from "fs"
import { newid } from "../../newid"
import { newid } from "../../docIds/newid"
function buildNano(couchInfo: { url: string; cookie: string }) {
return Nano({

View File

@ -2,9 +2,10 @@ export * from "./couch"
export * from "./db"
export * from "./utils"
export * from "./views"
export * from "./conversions"
export * from "../docIds/conversions"
export { default as Replication } from "./Replication"
// exports to support old export structure
export * from "../constants/db"
export { getGlobalDBName, baseGlobalDBName } from "../context"
export * from "./lucene"
export * as searchIndexes from "./searchIndexes"

View File

@ -1,12 +1,14 @@
import fetch from "node-fetch"
import { getCouchInfo } from "./couch"
import { SearchFilters, Row } from "@budibase/types"
import { createUserIndex } from "./searchIndexes/searchIndexes"
const QUERY_START_REGEX = /\d[0-9]*:/g
interface SearchResponse<T> {
rows: T[] | any[]
bookmark: string
bookmark?: string
totalRows: number
}
interface PaginatedSearchResponse<T> extends SearchResponse<T> {
@ -42,23 +44,26 @@ export function removeKeyNumbering(key: any): string {
* Optionally takes a base lucene query object.
*/
export class QueryBuilder<T> {
dbName: string
index: string
query: SearchFilters
limit: number
sort?: string
bookmark?: string
sortOrder: string
sortType: string
includeDocs: boolean
version?: string
indexBuilder?: () => Promise<any>
noEscaping = false
#dbName: string
#index: string
#query: SearchFilters
#limit: number
#sort?: string
#bookmark?: string
#sortOrder: string
#sortType: string
#includeDocs: boolean
#version?: string
#indexBuilder?: () => Promise<any>
#noEscaping = false
#skip?: number
static readonly maxLimit = 200
constructor(dbName: string, index: string, base?: SearchFilters) {
this.dbName = dbName
this.index = index
this.query = {
this.#dbName = dbName
this.#index = index
this.#query = {
allOr: false,
string: {},
fuzzy: {},
@ -73,86 +78,96 @@ export class QueryBuilder<T> {
containsAny: {},
...base,
}
this.limit = 50
this.sortOrder = "ascending"
this.sortType = "string"
this.includeDocs = true
this.#limit = 50
this.#sortOrder = "ascending"
this.#sortType = "string"
this.#includeDocs = true
}
disableEscaping() {
this.noEscaping = true
this.#noEscaping = true
return this
}
setIndexBuilder(builderFn: () => Promise<any>) {
this.indexBuilder = builderFn
this.#indexBuilder = builderFn
return this
}
setVersion(version?: string) {
if (version != null) {
this.version = version
this.#version = version
}
return this
}
setTable(tableId: string) {
this.query.equal!.tableId = tableId
this.#query.equal!.tableId = tableId
return this
}
setLimit(limit?: number) {
if (limit != null) {
this.limit = limit
this.#limit = limit
}
return this
}
setSort(sort?: string) {
if (sort != null) {
this.sort = sort
this.#sort = sort
}
return this
}
setSortOrder(sortOrder?: string) {
if (sortOrder != null) {
this.sortOrder = sortOrder
this.#sortOrder = sortOrder
}
return this
}
setSortType(sortType?: string) {
if (sortType != null) {
this.sortType = sortType
this.#sortType = sortType
}
return this
}
setBookmark(bookmark?: string) {
if (bookmark != null) {
this.bookmark = bookmark
this.#bookmark = bookmark
}
return this
}
setSkip(skip: number | undefined) {
this.#skip = skip
return this
}
excludeDocs() {
this.includeDocs = false
this.#includeDocs = false
return this
}
includeDocs() {
this.#includeDocs = true
return this
}
addString(key: string, partial: string) {
this.query.string![key] = partial
this.#query.string![key] = partial
return this
}
addFuzzy(key: string, fuzzy: string) {
this.query.fuzzy![key] = fuzzy
this.#query.fuzzy![key] = fuzzy
return this
}
addRange(key: string, low: string | number, high: string | number) {
this.query.range![key] = {
this.#query.range![key] = {
low,
high,
}
@ -160,51 +175,51 @@ export class QueryBuilder<T> {
}
addEqual(key: string, value: any) {
this.query.equal![key] = value
this.#query.equal![key] = value
return this
}
addNotEqual(key: string, value: any) {
this.query.notEqual![key] = value
this.#query.notEqual![key] = value
return this
}
addEmpty(key: string, value: any) {
this.query.empty![key] = value
this.#query.empty![key] = value
return this
}
addNotEmpty(key: string, value: any) {
this.query.notEmpty![key] = value
this.#query.notEmpty![key] = value
return this
}
addOneOf(key: string, value: any) {
this.query.oneOf![key] = value
this.#query.oneOf![key] = value
return this
}
addContains(key: string, value: any) {
this.query.contains![key] = value
this.#query.contains![key] = value
return this
}
addNotContains(key: string, value: any) {
this.query.notContains![key] = value
this.#query.notContains![key] = value
return this
}
addContainsAny(key: string, value: any) {
this.query.containsAny![key] = value
this.#query.containsAny![key] = value
return this
}
setAllOr() {
this.query.allOr = true
this.#query.allOr = true
}
handleSpaces(input: string) {
if (this.noEscaping) {
if (this.#noEscaping) {
return input
} else {
return input.replace(/ /g, "_")
@ -219,7 +234,7 @@ export class QueryBuilder<T> {
* @returns {string|*}
*/
preprocess(value: any, { escape, lowercase, wrap, type }: any = {}) {
const hasVersion = !!this.version
const hasVersion = !!this.#version
// Determine if type needs wrapped
const originalType = typeof value
// Convert to lowercase
@ -227,7 +242,7 @@ export class QueryBuilder<T> {
value = value.toLowerCase ? value.toLowerCase() : value
}
// Escape characters
if (!this.noEscaping && escape && originalType === "string") {
if (!this.#noEscaping && escape && originalType === "string") {
value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
}
@ -242,7 +257,7 @@ export class QueryBuilder<T> {
isMultiCondition() {
let count = 0
for (let filters of Object.values(this.query)) {
for (let filters of Object.values(this.#query)) {
// not contains is one massive filter in allOr mode
if (typeof filters === "object") {
count += Object.keys(filters).length
@ -272,13 +287,13 @@ export class QueryBuilder<T> {
buildSearchQuery() {
const builder = this
let allOr = this.query && this.query.allOr
let allOr = this.#query && this.#query.allOr
let query = allOr ? "" : "*:*"
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
let tableId
if (this.query.equal!.tableId) {
tableId = this.query.equal!.tableId
delete this.query.equal!.tableId
if (this.#query.equal!.tableId) {
tableId = this.#query.equal!.tableId
delete this.#query.equal!.tableId
}
const equal = (key: string, value: any) => {
@ -363,8 +378,8 @@ export class QueryBuilder<T> {
}
// Construct the actual lucene search query string from JSON structure
if (this.query.string) {
build(this.query.string, (key: string, value: any) => {
if (this.#query.string) {
build(this.#query.string, (key: string, value: any) => {
if (!value) {
return null
}
@ -376,8 +391,8 @@ export class QueryBuilder<T> {
return `${key}:${value}*`
})
}
if (this.query.range) {
build(this.query.range, (key: string, value: any) => {
if (this.#query.range) {
build(this.#query.range, (key: string, value: any) => {
if (!value) {
return null
}
@ -392,8 +407,8 @@ export class QueryBuilder<T> {
return `${key}:[${low} TO ${high}]`
})
}
if (this.query.fuzzy) {
build(this.query.fuzzy, (key: string, value: any) => {
if (this.#query.fuzzy) {
build(this.#query.fuzzy, (key: string, value: any) => {
if (!value) {
return null
}
@ -405,34 +420,34 @@ export class QueryBuilder<T> {
return `${key}:${value}~`
})
}
if (this.query.equal) {
build(this.query.equal, equal)
if (this.#query.equal) {
build(this.#query.equal, equal)
}
if (this.query.notEqual) {
build(this.query.notEqual, (key: string, value: any) => {
if (this.#query.notEqual) {
build(this.#query.notEqual, (key: string, value: any) => {
if (!value) {
return null
}
return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
})
}
if (this.query.empty) {
build(this.query.empty, (key: string) => `!${key}:["" TO *]`)
if (this.#query.empty) {
build(this.#query.empty, (key: string) => `!${key}:["" TO *]`)
}
if (this.query.notEmpty) {
build(this.query.notEmpty, (key: string) => `${key}:["" TO *]`)
if (this.#query.notEmpty) {
build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`)
}
if (this.query.oneOf) {
build(this.query.oneOf, oneOf)
if (this.#query.oneOf) {
build(this.#query.oneOf, oneOf)
}
if (this.query.contains) {
build(this.query.contains, contains)
if (this.#query.contains) {
build(this.#query.contains, contains)
}
if (this.query.notContains) {
build(this.compressFilters(this.query.notContains), notContains)
if (this.#query.notContains) {
build(this.compressFilters(this.#query.notContains), notContains)
}
if (this.query.containsAny) {
build(this.query.containsAny, containsAny)
if (this.#query.containsAny) {
build(this.#query.containsAny, containsAny)
}
// make sure table ID is always added as an AND
if (tableId) {
@ -446,29 +461,65 @@ export class QueryBuilder<T> {
buildSearchBody() {
let body: any = {
q: this.buildSearchQuery(),
limit: Math.min(this.limit, 200),
include_docs: this.includeDocs,
limit: Math.min(this.#limit, QueryBuilder.maxLimit),
include_docs: this.#includeDocs,
}
if (this.bookmark) {
body.bookmark = this.bookmark
if (this.#bookmark) {
body.bookmark = this.#bookmark
}
if (this.sort) {
const order = this.sortOrder === "descending" ? "-" : ""
const type = `<${this.sortType}>`
body.sort = `${order}${this.handleSpaces(this.sort)}${type}`
if (this.#sort) {
const order = this.#sortOrder === "descending" ? "-" : ""
const type = `<${this.#sortType}>`
body.sort = `${order}${this.handleSpaces(this.#sort)}${type}`
}
return body
}
async run() {
if (this.#skip) {
await this.#skipItems(this.#skip)
}
return await this.#execute()
}
/**
* Lucene queries do not support pagination and use bookmarks instead.
* For the given builder, walk through pages using bookmarks until the desired
* page has been met.
*/
async #skipItems(skip: number) {
// Lucene does not support pagination.
// Handle pagination by finding the right bookmark
const prevIncludeDocs = this.#includeDocs
const prevLimit = this.#limit
this.excludeDocs()
let skipRemaining = skip
let iterationFetched = 0
do {
const toSkip = Math.min(QueryBuilder.maxLimit, skipRemaining)
this.setLimit(toSkip)
const { bookmark, rows } = await this.#execute()
this.setBookmark(bookmark)
iterationFetched = rows.length
skipRemaining -= rows.length
} while (skipRemaining > 0 && iterationFetched > 0)
this.#includeDocs = prevIncludeDocs
this.#limit = prevLimit
}
async #execute() {
const { url, cookie } = getCouchInfo()
const fullPath = `${url}/${this.dbName}/_design/database/_search/${this.index}`
const fullPath = `${url}/${this.#dbName}/_design/database/_search/${
this.#index
}`
const body = this.buildSearchBody()
try {
return await runQuery<T>(fullPath, body, cookie)
} catch (err: any) {
if (err.status === 404 && this.indexBuilder) {
await this.indexBuilder()
if (err.status === 404 && this.#indexBuilder) {
await this.#indexBuilder()
return await runQuery<T>(fullPath, body, cookie)
} else {
throw err
@ -502,8 +553,9 @@ async function runQuery<T>(
}
const json = await response.json()
let output: any = {
let output: SearchResponse<T> = {
rows: [],
totalRows: 0,
}
if (json.rows != null && json.rows.length > 0) {
output.rows = json.rows.map((row: any) => row.doc)
@ -511,6 +563,9 @@ async function runQuery<T>(
if (json.bookmark) {
output.bookmark = json.bookmark
}
if (json.total_rows) {
output.totalRows = json.total_rows
}
return output
}
@ -543,8 +598,8 @@ async function recursiveSearch<T>(
if (rows.length >= params.limit) {
return rows
}
let pageSize = 200
if (rows.length > params.limit - 200) {
let pageSize = QueryBuilder.maxLimit
if (rows.length > params.limit - QueryBuilder.maxLimit) {
pageSize = params.limit - rows.length
}
const page = await new QueryBuilder<T>(dbName, index, query)
@ -559,7 +614,7 @@ async function recursiveSearch<T>(
if (!page.rows.length) {
return rows
}
if (page.rows.length < 200) {
if (page.rows.length < QueryBuilder.maxLimit) {
return [...rows, ...page.rows]
}
const newParams = {
@ -597,7 +652,7 @@ export async function paginatedSearch<T>(
if (limit == null || isNaN(limit) || limit < 0) {
limit = 50
}
limit = Math.min(limit, 200)
limit = Math.min(limit, QueryBuilder.maxLimit)
const search = new QueryBuilder<T>(dbName, index, query)
if (params.version) {
search.setVersion(params.version)

View File

@ -0,0 +1 @@
export * from "./searchIndexes"

View File

@ -0,0 +1,62 @@
import { User, SearchIndex } from "@budibase/types"
import { getGlobalDB } from "../../context"
export async function createUserIndex() {
const db = getGlobalDB()
let designDoc
try {
designDoc = await db.get("_design/database")
} catch (err: any) {
if (err.status === 404) {
designDoc = { _id: "_design/database" }
}
}
const fn = function (user: User) {
if (user._id && !user._id.startsWith("us_")) {
return
}
const ignoredFields = [
"_id",
"_rev",
"password",
"account",
"license",
"budibaseAccess",
"accountPortalAccess",
"csrfToken",
]
function idx(input: Record<string, any>, prev?: string) {
for (let key of Object.keys(input)) {
if (ignoredFields.includes(key)) {
continue
}
let idxKey = prev != null ? `${prev}.${key}` : key
if (typeof input[key] === "string") {
// eslint-disable-next-line no-undef
// @ts-ignore
index(idxKey, input[key].toLowerCase(), { facet: true })
} else if (typeof input[key] !== "object") {
// eslint-disable-next-line no-undef
// @ts-ignore
index(idxKey, input[key], { facet: true })
} else {
idx(input[key], idxKey)
}
}
}
idx(user)
}
designDoc.indexes = {
[SearchIndex.USER]: {
index: fn.toString(),
analyzer: {
default: "keyword",
name: "perfield",
},
},
}
await db.put(designDoc)
}

View File

@ -1,4 +1,4 @@
import { newid } from "../../newid"
import { newid } from "../../docIds/newid"
import { getDB } from "../db"
import { Database } from "@budibase/types"
import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
@ -136,6 +136,106 @@ describe("lucene", () => {
const resp = await builder.run()
expect(resp.rows.length).toBe(2)
})
describe("skip", () => {
const skipDbName = `db-${newid()}`
let docs: {
_id: string
property: string
array: string[]
}[]
beforeAll(async () => {
const db = getDB(skipDbName)
docs = Array(QueryBuilder.maxLimit * 2.5)
.fill(0)
.map((_, i) => ({
_id: i.toString().padStart(3, "0"),
property: `value_${i.toString().padStart(3, "0")}`,
array: [],
}))
await db.bulkDocs(docs)
await db.put({
_id: "_design/database",
indexes: {
[INDEX_NAME]: {
index: index,
analyzer: "standard",
},
},
})
})
it("should be able to apply skip", async () => {
const builder = new QueryBuilder(skipDbName, INDEX_NAME)
const firstResponse = await builder.run()
builder.setSkip(40)
const secondResponse = await builder.run()
// Return the default limit
expect(firstResponse.rows.length).toBe(50)
expect(secondResponse.rows.length).toBe(50)
// Should have the expected overlap
expect(firstResponse.rows.slice(40)).toEqual(
secondResponse.rows.slice(0, 10)
)
})
it("should handle limits", async () => {
const builder = new QueryBuilder(skipDbName, INDEX_NAME)
builder.setLimit(10)
builder.setSkip(50)
builder.setSort("_id")
const resp = await builder.run()
expect(resp.rows.length).toBe(10)
expect(resp.rows).toEqual(
docs.slice(50, 60).map(expect.objectContaining)
)
})
it("should be able to skip searching through multiple responses", async () => {
const builder = new QueryBuilder(skipDbName, INDEX_NAME)
// Skipping 2 max limits plus a little bit more
const skip = QueryBuilder.maxLimit * 2 + 37
builder.setSkip(skip)
builder.setSort("_id")
const resp = await builder.run()
expect(resp.rows.length).toBe(50)
expect(resp.rows).toEqual(
docs.slice(skip, skip + resp.rows.length).map(expect.objectContaining)
)
})
it("should not return results if skipping all docs", async () => {
const builder = new QueryBuilder(skipDbName, INDEX_NAME)
// Skipping 2 max limits plus a little bit more
const skip = docs.length + 1
builder.setSkip(skip)
const resp = await builder.run()
expect(resp.rows.length).toBe(0)
})
it("skip should respect with filters", async () => {
const builder = new QueryBuilder(skipDbName, INDEX_NAME)
builder.setLimit(10)
builder.setSkip(50)
builder.addString("property", "value_1")
builder.setSort("property")
const resp = await builder.run()
expect(resp.rows.length).toBe(10)
expect(resp.rows).toEqual(
docs.slice(150, 160).map(expect.objectContaining)
)
})
})
})
describe("paginated search", () => {

View File

@ -3,7 +3,7 @@ import {
getProdAppID,
isDevAppID,
isProdAppID,
} from "../conversions"
} from "../../docIds/conversions"
import { generateAppID } from "../utils"
describe("utils", () => {

View File

@ -1,257 +1,12 @@
import { newid } from "../newid"
import env from "../environment"
import {
DEFAULT_TENANT_ID,
SEPARATOR,
DocumentType,
UNICODE_MAX,
ViewName,
InternalTable,
APP_PREFIX,
} from "../constants"
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
import { getTenantId, getGlobalDBName } from "../context"
import { doWithDB, directCouchAllDbs } from "./db"
import { getAppMetadata } from "../cache/appMetadata"
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
import { App, Database } from "@budibase/types"
/**
* Generates a new app ID.
* @returns {string} The new app ID which the app doc can be stored under.
*/
export const generateAppID = (tenantId?: string | null) => {
let id = APP_PREFIX
if (tenantId) {
id += `${tenantId}${SEPARATOR}`
}
return `${id}${newid()}`
}
/**
* If creating DB allDocs/query params with only a single top level ID this can be used, this
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
* More complex cases such as link docs and rows which have multiple levels of IDs that their
* ID consists of need their own functions to build the allDocs parameters.
* @param {string} docType The type of document which input params are being built for, e.g. user,
* link, app, table and so on.
* @param {string|null} docId The ID of the document minus its type - this is only needed if looking
* for a singular document.
* @param {object} otherProps Add any other properties onto the request, e.g. include_docs.
* @returns {object} Parameters which can then be used with an allDocs request.
*/
export function getDocParams(
docType: string,
docId?: string | null,
otherProps: any = {}
) {
if (docId == null) {
docId = ""
}
return {
...otherProps,
startkey: `${docType}${SEPARATOR}${docId}`,
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
}
}
/**
* Gets the DB allDocs/query params for retrieving a row.
* @param {string|null} tableId The table in which the rows have been stored.
* @param {string|null} rowId The ID of the row which is being specifically queried for. This can be
* left null to get all the rows in the table.
* @param {object} otherProps Any other properties to add to the request.
* @returns {object} Parameters which can then be used with an allDocs request.
*/
export function getRowParams(
tableId?: string | null,
rowId?: string | null,
otherProps = {}
) {
if (tableId == null) {
return getDocParams(DocumentType.ROW, null, otherProps)
}
const endOfKey = rowId == null ? `${tableId}${SEPARATOR}` : rowId
return getDocParams(DocumentType.ROW, endOfKey, otherProps)
}
/**
* Retrieve the correct index for a view based on default design DB.
*/
export function getQueryIndex(viewName: ViewName) {
return `database/${viewName}`
}
/**
* Gets a new row ID for the specified table.
* @param {string} tableId The table which the row is being created for.
* @param {string|null} id If an ID is to be used then the UUID can be substituted for this.
* @returns {string} The new ID which a row doc can be stored under.
*/
export function generateRowID(tableId: string, id?: string) {
id = id || newid()
return `${DocumentType.ROW}${SEPARATOR}${tableId}${SEPARATOR}${id}`
}
/**
* Check if a given ID is that of a table.
* @returns {boolean}
*/
export const isTableId = (id: string) => {
// this includes datasource plus tables
return (
id &&
(id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) ||
id.startsWith(`${DocumentType.DATASOURCE_PLUS}${SEPARATOR}`))
)
}
/**
* Check if a given ID is that of a datasource or datasource plus.
* @returns {boolean}
*/
export const isDatasourceId = (id: string) => {
// this covers both datasources and datasource plus
return id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
}
/**
* Generates a new workspace ID.
* @returns {string} The new workspace ID which the workspace doc can be stored under.
*/
export function generateWorkspaceID() {
return `${DocumentType.WORKSPACE}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving workspaces.
*/
export function getWorkspaceParams(id = "", otherProps = {}) {
return {
...otherProps,
startkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}`,
endkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`,
}
}
/**
* Generates a new global user ID.
* @returns {string} The new user ID which the user doc can be stored under.
*/
export function generateGlobalUserID(id?: any) {
return `${DocumentType.USER}${SEPARATOR}${id || newid()}`
}
/**
* Gets parameters for retrieving users.
*/
export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
if (!globalId) {
globalId = ""
}
const startkey = otherProps?.startkey
return {
...otherProps,
// need to include this incase pagination
startkey: startkey
? startkey
: `${DocumentType.USER}${SEPARATOR}${globalId}`,
endkey: `${DocumentType.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
}
}
/**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/
export function getUserMetadataParams(userId?: string | null, otherProps = {}) {
return getRowParams(InternalTable.USER_METADATA, userId, otherProps)
}
/**
* Generates a new user ID based on the passed in global ID.
* @param {string} globalId The ID of the global user.
* @returns {string} The new user ID which the user doc can be stored under.
*/
export function generateUserMetadataID(globalId: string) {
return generateRowID(InternalTable.USER_METADATA, globalId)
}
/**
* Breaks up the ID to get the global ID.
*/
export function getGlobalIDFromUserMetadataID(id: string) {
const prefix = `${DocumentType.ROW}${SEPARATOR}${InternalTable.USER_METADATA}${SEPARATOR}`
if (!id || !id.includes(prefix)) {
return id
}
return id.split(prefix)[1]
}
export function getUsersByAppParams(appId: any, otherProps: any = {}) {
const prodAppId = getProdAppID(appId)
return {
...otherProps,
startkey: prodAppId,
endkey: `${prodAppId}${UNICODE_MAX}`,
}
}
/**
* Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level.
*/
export function generateTemplateID(ownerId: any) {
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
}
export function generateAppUserID(prodAppId: string, userId: string) {
return `${prodAppId}${SEPARATOR}${userId}`
}
/**
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
*/
export function getTemplateParams(
ownerId: any,
templateId: any,
otherProps = {}
) {
if (!templateId) {
templateId = ""
}
let final
if (templateId) {
final = templateId
} else {
final = `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}`
}
return {
...otherProps,
startkey: final,
endkey: `${final}${UNICODE_MAX}`,
}
}
/**
* Generates a new role ID.
* @returns {string} The new role ID which the role doc can be stored under.
*/
export function generateRoleID(id?: any) {
return `${DocumentType.ROLE}${SEPARATOR}${id || newid()}`
}
/**
* Gets parameters for retrieving a role, this is a utility function for the getDocParams function.
*/
export function getRoleParams(roleId?: string | null, otherProps = {}) {
return getDocParams(DocumentType.ROLE, roleId, otherProps)
}
export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : ""
return `startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"`
}
import { getStartEndKeyURL } from "../docIds"
export * from "../docIds"
/**
* if in production this will use the CouchDB _all_dbs call to retrieve a list of databases. If testing
@ -411,31 +166,8 @@ export async function dbExists(dbName: any) {
)
}
/**
* Generates a new dev info document ID - this is scoped to a user.
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
*/
export const generateDevInfoID = (userId: any) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
}
/**
* Generates a new plugin ID - to be used in the global DB.
* @returns {string} The new plugin ID which a plugin metadata document can be stored under.
*/
export const generatePluginID = (name: string) => {
return `${DocumentType.PLUGIN}${SEPARATOR}${name}`
}
/**
* Gets parameters for retrieving automations, this is a utility function for the getDocParams function.
*/
export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
}
export function pagination(
data: any[],
export function pagination<T>(
data: T[],
pageSize: number,
{
paginate,
@ -444,7 +176,7 @@ export function pagination(
}: {
paginate: boolean
property: string
getKey?: (doc: any) => string | undefined
getKey?: (doc: T) => string | undefined
} = {
paginate: true,
property: "_id",

View File

@ -0,0 +1,102 @@
import {
APP_PREFIX,
DocumentType,
InternalTable,
SEPARATOR,
} from "../constants"
import { newid } from "./newid"
/**
* Generates a new app ID.
* @returns {string} The new app ID which the app doc can be stored under.
*/
export const generateAppID = (tenantId?: string | null) => {
let id = APP_PREFIX
if (tenantId) {
id += `${tenantId}${SEPARATOR}`
}
return `${id}${newid()}`
}
/**
* Gets a new row ID for the specified table.
* @param {string} tableId The table which the row is being created for.
* @param {string|null} id If an ID is to be used then the UUID can be substituted for this.
* @returns {string} The new ID which a row doc can be stored under.
*/
export function generateRowID(tableId: string, id?: string) {
id = id || newid()
return `${DocumentType.ROW}${SEPARATOR}${tableId}${SEPARATOR}${id}`
}
/**
* Generates a new workspace ID.
* @returns {string} The new workspace ID which the workspace doc can be stored under.
*/
export function generateWorkspaceID() {
return `${DocumentType.WORKSPACE}${SEPARATOR}${newid()}`
}
/**
* Generates a new global user ID.
* @returns {string} The new user ID which the user doc can be stored under.
*/
export function generateGlobalUserID(id?: any) {
return `${DocumentType.USER}${SEPARATOR}${id || newid()}`
}
/**
* Generates a new user ID based on the passed in global ID.
* @param {string} globalId The ID of the global user.
* @returns {string} The new user ID which the user doc can be stored under.
*/
export function generateUserMetadataID(globalId: string) {
return generateRowID(InternalTable.USER_METADATA, globalId)
}
/**
* Breaks up the ID to get the global ID.
*/
export function getGlobalIDFromUserMetadataID(id: string) {
const prefix = `${DocumentType.ROW}${SEPARATOR}${InternalTable.USER_METADATA}${SEPARATOR}`
if (!id || !id.includes(prefix)) {
return id
}
return id.split(prefix)[1]
}
/**
* Generates a template ID.
* @param ownerId The owner/user of the template, this could be global or a workspace level.
*/
export function generateTemplateID(ownerId: any) {
return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
}
export function generateAppUserID(prodAppId: string, userId: string) {
return `${prodAppId}${SEPARATOR}${userId}`
}
/**
* Generates a new role ID.
* @returns {string} The new role ID which the role doc can be stored under.
*/
export function generateRoleID(id?: any) {
return `${DocumentType.ROLE}${SEPARATOR}${id || newid()}`
}
/**
* Generates a new dev info document ID - this is scoped to a user.
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
*/
export const generateDevInfoID = (userId: any) => {
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
}
/**
* Generates a new plugin ID - to be used in the global DB.
* @returns {string} The new plugin ID which a plugin metadata document can be stored under.
*/
export const generatePluginID = (name: string) => {
return `${DocumentType.PLUGIN}${SEPARATOR}${name}`
}

View File

@ -0,0 +1,2 @@
export * from "./ids"
export * from "./params"

View File

@ -0,0 +1,174 @@
import {
DocumentType,
InternalTable,
SEPARATOR,
UNICODE_MAX,
ViewName,
} from "../constants"
import { getProdAppID } from "./conversions"
/**
* If creating DB allDocs/query params with only a single top level ID this can be used, this
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
* More complex cases such as link docs and rows which have multiple levels of IDs that their
* ID consists of need their own functions to build the allDocs parameters.
* @param {string} docType The type of document which input params are being built for, e.g. user,
* link, app, table and so on.
* @param {string|null} docId The ID of the document minus its type - this is only needed if looking
* for a singular document.
* @param {object} otherProps Add any other properties onto the request, e.g. include_docs.
* @returns {object} Parameters which can then be used with an allDocs request.
*/
export function getDocParams(
docType: string,
docId?: string | null,
otherProps: any = {}
) {
if (docId == null) {
docId = ""
}
return {
...otherProps,
startkey: `${docType}${SEPARATOR}${docId}`,
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
}
}
/**
* Gets the DB allDocs/query params for retrieving a row.
* @param {string|null} tableId The table in which the rows have been stored.
* @param {string|null} rowId The ID of the row which is being specifically queried for. This can be
* left null to get all the rows in the table.
* @param {object} otherProps Any other properties to add to the request.
* @returns {object} Parameters which can then be used with an allDocs request.
*/
export function getRowParams(
tableId?: string | null,
rowId?: string | null,
otherProps = {}
) {
if (tableId == null) {
return getDocParams(DocumentType.ROW, null, otherProps)
}
const endOfKey = rowId == null ? `${tableId}${SEPARATOR}` : rowId
return getDocParams(DocumentType.ROW, endOfKey, otherProps)
}
/**
* Retrieve the correct index for a view based on default design DB.
*/
export function getQueryIndex(viewName: ViewName) {
return `database/${viewName}`
}
/**
* Check if a given ID is that of a table.
* @returns {boolean}
*/
export const isTableId = (id: string) => {
// this includes datasource plus tables
return (
id &&
(id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) ||
id.startsWith(`${DocumentType.DATASOURCE_PLUS}${SEPARATOR}`))
)
}
/**
* Check if a given ID is that of a datasource or datasource plus.
* @returns {boolean}
*/
export const isDatasourceId = (id: string) => {
// this covers both datasources and datasource plus
return id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`)
}
/**
* Gets parameters for retrieving workspaces.
*/
export function getWorkspaceParams(id = "", otherProps = {}) {
return {
...otherProps,
startkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}`,
endkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`,
}
}
/**
* Gets parameters for retrieving users.
*/
export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
if (!globalId) {
globalId = ""
}
const startkey = otherProps?.startkey
return {
...otherProps,
// need to include this incase pagination
startkey: startkey
? startkey
: `${DocumentType.USER}${SEPARATOR}${globalId}`,
endkey: `${DocumentType.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
}
}
/**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/
export function getUserMetadataParams(userId?: string | null, otherProps = {}) {
return getRowParams(InternalTable.USER_METADATA, userId, otherProps)
}
export function getUsersByAppParams(appId: any, otherProps: any = {}) {
const prodAppId = getProdAppID(appId)
return {
...otherProps,
startkey: prodAppId,
endkey: `${prodAppId}${UNICODE_MAX}`,
}
}
/**
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
*/
export function getTemplateParams(
ownerId: any,
templateId: any,
otherProps = {}
) {
if (!templateId) {
templateId = ""
}
let final
if (templateId) {
final = templateId
} else {
final = `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}`
}
return {
...otherProps,
startkey: final,
endkey: `${final}${UNICODE_MAX}`,
}
}
/**
* Gets parameters for retrieving a role, this is a utility function for the getDocParams function.
*/
export function getRoleParams(roleId?: string | null, otherProps = {}) {
return getDocParams(DocumentType.ROLE, roleId, otherProps)
}
export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : ""
return `startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"`
}
/**
* Gets parameters for retrieving automations, this is a utility function for the getDocParams function.
*/
export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
}

View File

@ -36,6 +36,15 @@ function getAPIEncryptionKey() {
: process.env.JWT_SECRET // fallback to the JWT_SECRET used historically
}
function httpLogging() {
if (process.env.HTTP_LOGGING === undefined) {
// on by default unless otherwise specified
return true
}
return process.env.HTTP_LOGGING
}
const environment = {
isTest,
isJest,
@ -90,11 +99,11 @@ const environment = {
USE_COUCH: process.env.USE_COUCH || true,
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
SERVICE: process.env.SERVICE || "budibase",
LOG_LEVEL: process.env.LOG_LEVEL,
LOG_LEVEL: process.env.LOG_LEVEL || "info",
SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
DEPLOYMENT_ENVIRONMENT:
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true,
HTTP_LOGGING: httpLogging(),
ENABLE_AUDIT_LOG_IP_ADDR: process.env.ENABLE_AUDIT_LOG_IP_ADDR,
// smtp
SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED,

View File

@ -97,3 +97,11 @@ export class InvalidAPIKeyError extends BudibaseError {
)
}
}
// USERS
export class EmailUnavailableError extends Error {
constructor(email: string) {
super(`Email already in use: '${email}'`)
}
}

View File

@ -2,14 +2,6 @@ import { Event, Identity, Group } from "@budibase/types"
import { EventProcessor } from "./types"
import env from "../../environment"
const getTimestampString = (timestamp?: string | number) => {
let timestampString = ""
if (timestamp) {
timestampString = `[timestamp=${new Date(timestamp).toISOString()}]`
}
return timestampString
}
const skipLogging = env.SELF_HOSTED && !env.isDev()
export default class LoggingProcessor implements EventProcessor {
@ -22,32 +14,21 @@ export default class LoggingProcessor implements EventProcessor {
if (skipLogging) {
return
}
let timestampString = getTimestampString(timestamp)
let message = `[audit] [identityType=${identity.type}] ${timestampString} ${event} `
if (env.isDev()) {
message = message + `[debug: [properties=${JSON.stringify(properties)}] ]`
}
console.log(message)
console.log(`[audit] [identityType=${identity.type}] ${event}`, properties)
}
async identify(identity: Identity, timestamp?: string | number) {
if (skipLogging) {
return
}
let timestampString = getTimestampString(timestamp)
console.log(
`[audit] [${JSON.stringify(identity)}] ${timestampString} identified`
)
console.log(`[audit] identified`, identity)
}
async identifyGroup(group: Group, timestamp?: string | number) {
if (skipLogging) {
return
}
let timestampString = getTimestampString(timestamp)
console.log(
`[audit] [${JSON.stringify(group)}] ${timestampString} group identified`
)
console.log(`[audit] group identified`, group)
}
shutdown(): void {

View File

@ -1,4 +1,4 @@
import { testEnv } from "../../../../../tests"
import { testEnv } from "../../../../../tests/extra"
import PosthogProcessor from "../PosthogProcessor"
import { Event, IdentityType, Hosting } from "@budibase/types"
const tk = require("timekeeper")

View File

@ -9,12 +9,13 @@ import {
GroupUsersDeletedEvent,
GroupAddedOnboardingEvent,
GroupPermissionsEditedEvent,
UserGroupRoles,
} from "@budibase/types"
import { isScim } from "../../context"
async function created(group: UserGroup, timestamp?: number) {
const properties: GroupCreatedEvent = {
groupId: group._id as string,
viaScim: isScim(),
audited: {
name: group.name,
},
@ -25,6 +26,7 @@ async function created(group: UserGroup, timestamp?: number) {
async function updated(group: UserGroup) {
const properties: GroupUpdatedEvent = {
groupId: group._id as string,
viaScim: isScim(),
audited: {
name: group.name,
},
@ -35,6 +37,7 @@ async function updated(group: UserGroup) {
async function deleted(group: UserGroup) {
const properties: GroupDeletedEvent = {
groupId: group._id as string,
viaScim: isScim(),
audited: {
name: group.name,
},
@ -46,6 +49,7 @@ async function usersAdded(count: number, group: UserGroup) {
const properties: GroupUsersAddedEvent = {
count,
groupId: group._id as string,
viaScim: isScim(),
audited: {
name: group.name,
},
@ -57,6 +61,7 @@ async function usersDeleted(count: number, group: UserGroup) {
const properties: GroupUsersDeletedEvent = {
count,
groupId: group._id as string,
viaScim: isScim(),
audited: {
name: group.name,
},

View File

@ -15,10 +15,12 @@ import {
UserUpdatedEvent,
UserOnboardingEvent,
} from "@budibase/types"
import { isScim } from "../../context"
async function created(user: User, timestamp?: number) {
const properties: UserCreatedEvent = {
userId: user._id as string,
viaScim: isScim(),
audited: {
email: user.email,
},
@ -29,6 +31,7 @@ async function created(user: User, timestamp?: number) {
async function updated(user: User) {
const properties: UserUpdatedEvent = {
userId: user._id as string,
viaScim: isScim(),
audited: {
email: user.email,
},
@ -39,6 +42,7 @@ async function updated(user: User) {
async function deleted(user: User) {
const properties: UserDeletedEvent = {
userId: user._id as string,
viaScim: isScim(),
audited: {
email: user.email,
},

View File

@ -1,60 +0,0 @@
import { Header } from "./constants"
import env from "./environment"
const correlator = require("correlation-id")
import { Options } from "pino-http"
import { IncomingMessage } from "http"
const NonErrors = ["AccountError"]
function isSuppressed(e?: any) {
return e && e["suppressAlert"]
}
export function logAlert(message: string, e?: any) {
if (e && NonErrors.includes(e.name) && isSuppressed(e)) {
return
}
let errorJson = ""
if (e) {
errorJson = ": " + JSON.stringify(e, Object.getOwnPropertyNames(e))
}
console.error(`bb-alert: ${message} ${errorJson}`)
}
export function logAlertWithInfo(
message: string,
db: string,
id: string,
error: any
) {
message = `${message} - db: ${db} - doc: ${id} - error: `
logAlert(message, error)
}
export function logWarn(message: string) {
console.warn(`bb-warn: ${message}`)
}
export function pinoSettings(): Options {
return {
prettyPrint: {
levelFirst: true,
},
genReqId: correlator.getId,
level: env.LOG_LEVEL || "error",
autoLogging: {
ignore: (req: IncomingMessage) => !!req.url?.includes("/health"),
},
}
}
const setCorrelationHeader = (headers: any) => {
const correlationId = correlator.getId()
if (correlationId) {
headers[Header.CORRELATION_ID] = correlationId
}
}
export const correlation = {
setHeader: setCorrelationHeader,
}

View File

@ -0,0 +1,26 @@
const NonErrors = ["AccountError"]
function isSuppressed(e?: any) {
return e && e["suppressAlert"]
}
export function logAlert(message: string, e?: any) {
if (e && NonErrors.includes(e.name) && isSuppressed(e)) {
return
}
console.error(`bb-alert: ${message}`, e)
}
export function logAlertWithInfo(
message: string,
db: string,
id: string,
error: any
) {
message = `${message} - db: ${db} - doc: ${id} - error: `
logAlert(message, error)
}
export function logWarn(message: string) {
console.warn(`bb-warn: ${message}`)
}

View File

@ -0,0 +1,13 @@
import { Header } from "../../constants"
const correlator = require("correlation-id")
export const setHeader = (headers: any) => {
const correlationId = correlator.getId()
if (correlationId) {
headers[Header.CORRELATION_ID] = correlationId
}
}
export function getId() {
return correlator.getId()
}

View File

@ -0,0 +1 @@
export * from "./correlation"

View File

@ -0,0 +1,17 @@
import { Header } from "../../constants"
import { v4 as uuid } from "uuid"
const correlator = require("correlation-id")
const correlation = (ctx: any, next: any) => {
// use the provided correlation id header if present
let correlationId = ctx.headers[Header.CORRELATION_ID]
if (!correlationId) {
correlationId = uuid()
}
return correlator.withId(correlationId, () => {
return next()
})
}
export default correlation

View File

@ -0,0 +1,6 @@
export * as correlation from "./correlation/correlation"
export { default as logger } from "./pino/logger"
export * from "./alerts"
// turn off or on context logging i.e. tenantId, appId etc
export let LOG_CONTEXT = true

View File

@ -0,0 +1,170 @@
import env from "../../environment"
import pino, { LoggerOptions } from "pino"
import * as context from "../../context"
import * as correlation from "../correlation"
import { IdentityType } from "@budibase/types"
import { LOG_CONTEXT } from "../index"
// LOGGER
const pinoOptions: LoggerOptions = {
level: env.LOG_LEVEL,
formatters: {
level: label => {
return { level: label.toUpperCase() }
},
bindings: () => {
return {}
},
},
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
}
if (env.isDev()) {
pinoOptions.transport = {
target: "pino-pretty",
options: {
singleLine: true,
},
}
}
export const logger = pino(pinoOptions)
// CONSOLE OVERRIDES
interface MergingObject {
objects?: any[]
tenantId?: string
appId?: string
identityId?: string
identityType?: IdentityType
correlationId?: string
err?: Error
}
function isPlainObject(obj: any) {
return typeof obj === "object" && obj !== null && !(obj instanceof Error)
}
function isError(obj: any) {
return obj instanceof Error
}
function isMessage(obj: any) {
return typeof obj === "string"
}
/**
* Backwards compatibility between console logging statements
* and pino logging requirements.
*/
function getLogParams(args: any[]): [MergingObject, string] {
let error = undefined
let objects: any[] = []
let message = ""
args.forEach(arg => {
if (isMessage(arg)) {
message = `${message} ${arg}`.trimStart()
}
if (isPlainObject(arg)) {
objects.push(arg)
}
if (isError(arg)) {
error = arg
}
})
const identity = getIdentity()
let contextObject = {}
if (LOG_CONTEXT) {
contextObject = {
tenantId: getTenantId(),
appId: getAppId(),
identityId: identity?._id,
identityType: identity?.type,
correlationId: correlation.getId(),
}
}
const mergingObject = {
objects: objects.length ? objects : undefined,
err: error,
...contextObject,
}
return [mergingObject, message]
}
console.log = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg)
logger.info(obj, msg)
}
console.info = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg)
logger.info(obj, msg)
}
console.warn = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg)
logger.warn(obj, msg)
}
console.error = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg)
logger.error(obj, msg)
}
/**
* custom trace impl - this resembles the node trace behaviour rather
* than traditional trace logging
* @param arg
*/
console.trace = (...arg: any[]) => {
const [obj, msg] = getLogParams(arg)
if (!obj.err) {
// to get stack trace
obj.err = new Error()
}
logger.trace(obj, msg)
}
console.debug = (...arg: any) => {
const [obj, msg] = getLogParams(arg)
logger.debug(obj, msg)
}
// CONTEXT
const getTenantId = () => {
let tenantId
try {
tenantId = context.getTenantId()
} catch (e: any) {
// do nothing
}
return tenantId
}
const getAppId = () => {
let appId
try {
appId = context.getAppId()
} catch (e) {
// do nothing
}
return appId
}
const getIdentity = () => {
let identity
try {
identity = context.getIdentity()
} catch (e) {
// do nothing
}
return identity
}
export default logger

View File

@ -0,0 +1,45 @@
import env from "../../environment"
import logger from "./logger"
import { IncomingMessage } from "http"
const pino = require("koa-pino-logger")
import { Options } from "pino-http"
import { Ctx } from "@budibase/types"
const correlator = require("correlation-id")
export function pinoSettings(): Options {
return {
logger,
genReqId: correlator.getId,
autoLogging: {
ignore: (req: IncomingMessage) => !!req.url?.includes("/health"),
},
serializers: {
req: req => {
return {
method: req.method,
url: req.url,
correlationId: req.id,
}
},
res: res => {
return {
status: res.statusCode,
}
},
},
}
}
function getMiddleware() {
if (env.HTTP_LOGGING) {
return pino(pinoSettings())
} else {
return (ctx: Ctx, next: any) => {
return next()
}
}
}
const pinoMiddleware = getMiddleware()
export default pinoMiddleware

View File

@ -96,9 +96,15 @@ export default function (
}
try {
// check the actual user is authenticated first, try header or cookie
const headerToken = ctx.request.headers[Header.TOKEN]
let headerToken = ctx.request.headers[Header.TOKEN]
const authCookie = getCookie(ctx, Cookie.Auth) || openJwt(headerToken)
const apiKey = ctx.request.headers[Header.API_KEY]
let apiKey = ctx.request.headers[Header.API_KEY]
if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) {
apiKey = ctx.request.headers[Header.AUTHORIZATION].split(" ")[1]
}
const tenantId = ctx.request.headers[Header.TENANT_ID]
let authenticated = false,
user = null,

View File

@ -1,6 +1,5 @@
import { APIError } from "@budibase/types"
import * as errors from "../errors"
import env from "../environment"
export async function errorHandling(ctx: any, next: any) {
try {
@ -9,9 +8,10 @@ export async function errorHandling(ctx: any, next: any) {
const status = err.status || err.statusCode || 500
ctx.status = status
if (status > 499 || env.ENABLE_4XX_HTTP_LOGGING) {
ctx.log.error(err)
console.trace(err)
if (status >= 400 && status < 500) {
console.warn(err)
} else {
console.error(err)
}
const error = errors.getPublicError(err)

View File

@ -14,7 +14,8 @@ export { default as csrf } from "./csrf"
export { default as adminOnly } from "./adminOnly"
export { default as builderOrAdmin } from "./builderOrAdmin"
export { default as builderOnly } from "./builderOnly"
export { default as logging } from "./logging"
export { default as pino } from "../logging/pino/middleware"
export { default as correlation } from "../logging/correlation/middleware"
export { default as errorHandling } from "./errorHandling"
export { default as querystringToBody } from "./querystringToBody"
export * as joiValidator from "./joi-validator"

View File

@ -1,90 +0,0 @@
const correlator = require("correlation-id")
import { Header } from "../constants"
import { v4 as uuid } from "uuid"
import * as context from "../context"
const debug = console.warn
const trace = console.trace
const log = console.log
const info = console.info
const warn = console.warn
const error = console.error
const getTenantId = () => {
let tenantId
try {
tenantId = context.getTenantId()
} catch (e: any) {
// do nothing
}
return tenantId
}
const getAppId = () => {
let appId
try {
appId = context.getAppId()
} catch (e) {
// do nothing
}
return appId
}
const getIdentityId = () => {
let identityId
try {
const identity = context.getIdentity()
identityId = identity?._id
} catch (e) {
// do nothing
}
return identityId
}
const print = (fn: any, data: any[]) => {
let message = ""
const correlationId = correlator.getId()
if (correlationId) {
message = message + `[correlationId=${correlator.getId()}]`
}
const tenantId = getTenantId()
if (tenantId) {
message = message + ` [tenantId=${tenantId}]`
}
const appId = getAppId()
if (appId) {
message = message + ` [appId=${appId}]`
}
const identityId = getIdentityId()
if (identityId) {
message = message + ` [identityId=${identityId}]`
}
if (!process.env.CI) {
fn(message, data)
}
}
const logging = (ctx: any, next: any) => {
// use the provided correlation id header if present
let correlationId = ctx.headers[Header.CORRELATION_ID]
if (!correlationId) {
correlationId = uuid()
}
return correlator.withId(correlationId, () => {
console.debug = data => print(debug, data)
console.trace = data => print(trace, data)
console.log = data => print(log, data)
console.info = data => print(info, data)
console.warn = data => print(warn, data)
console.error = data => print(error, data)
return next()
})
}
export default logging

View File

@ -78,17 +78,23 @@ export async function postAuth(
),
{ successRedirect: "/", failureRedirect: "/error" },
async (err: any, tokens: string[]) => {
const baseUrl = `/builder/app/${authStateCookie.appId}/data`
// update the DB for the datasource with all the user info
await doWithDB(authStateCookie.appId, async (db: Database) => {
const datasource = await db.get(authStateCookie.datasourceId)
let datasource
try {
datasource = await db.get(authStateCookie.datasourceId)
} catch (err: any) {
if (err.status === 404) {
ctx.redirect(baseUrl)
}
}
if (!datasource.config) {
datasource.config = {}
}
datasource.config.auth = { type: "google", ...tokens }
await db.put(datasource)
ctx.redirect(
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
)
ctx.redirect(`${baseUrl}/datasource/${authStateCookie.datasourceId}`)
})
}
)(ctx, next)

View File

@ -1,6 +1,7 @@
import fetch from "node-fetch"
import * as sso from "./sso"
import { ssoCallbackUrl } from "../utils"
import { validEmail } from "../../../utils"
import {
ConfigType,
OIDCInnerConfig,
@ -11,6 +12,7 @@ import {
JwtClaims,
SaveSSOUserFunction,
} from "@budibase/types"
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {
@ -86,15 +88,6 @@ function getEmail(profile: SSOProfile, jwtClaims: JwtClaims) {
)
}
function validEmail(value: string) {
return (
value &&
!!value.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
)
)
}
/**
* Create an instance of the oidc passport strategy. This wrapper fetches the configuration
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.

View File

@ -1,4 +1,5 @@
import { structures, testEnv, mocks } from "../../../../../tests"
import { structures, mocks } from "../../../../../tests"
import { testEnv } from "../../../../../tests/extra"
import { SSOAuthDetails, User } from "@budibase/types"
import { HTTPError } from "../../../../errors"

View File

@ -99,9 +99,7 @@ export const runMigration = async (
options.force[migrationType] &&
options.force[migrationType].includes(migrationName)
) {
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing`
)
log(`[Migration: ${migrationName}] [DB: ${dbName}] Forcing`)
} else {
// no force, exit
return
@ -111,7 +109,7 @@ export const runMigration = async (
// check if the migration is not a no-op
if (!options.noOp) {
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}`
`[Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}`
)
if (migration.preventRetry) {
@ -131,9 +129,7 @@ export const runMigration = async (
await migration.fn(db)
}
log(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete`
)
log(`[Migration: ${migrationName}] [DB: ${dbName}] Complete`)
}
// mark as complete
@ -141,7 +137,7 @@ export const runMigration = async (
await db.put(doc)
} catch (err) {
console.error(
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `,
`[Migration: ${migrationName}] [DB: ${dbName}] Error: `,
err
)
throw err

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`migrations should match snapshot 1`] = `
Object {
{
"_id": "migrations",
"_rev": "1-2f64479842a0513aa8b97f356b0b9127",
"createdAt": "2020-01-01T00:00:00.000Z",

View File

@ -1,4 +1,4 @@
import { testEnv, DBTestConfiguration } from "../../../tests"
import { testEnv, DBTestConfiguration } from "../../../tests/extra"
import * as migrations from "../index"
import * as context from "../../context"
import { MigrationType } from "@budibase/types"

View File

@ -1,6 +1,6 @@
import * as app from "../app"
import { getAppFileUrl } from "../app"
import { testEnv } from "../../../../tests"
import { testEnv } from "../../../../tests/extra"
describe("app", () => {
beforeEach(() => {

View File

@ -1,5 +1,5 @@
import * as global from "../global"
import { testEnv } from "../../../../tests"
import { testEnv } from "../../../../tests/extra"
describe("global", () => {
describe("getGlobalFileUrl", () => {

View File

@ -1,5 +1,6 @@
import * as plugins from "../plugins"
import { structures, testEnv } from "../../../../tests"
import { structures } from "../../../../tests"
import { testEnv } from "../../../../tests/extra"
describe("plugins", () => {
describe("enrichPluginURLs", () => {

View File

@ -1,4 +1,5 @@
import { DBTestConfiguration, structures } from "../../../tests"
import { structures } from "../../../tests"
import { DBTestConfiguration } from "../../../tests/extra"
import * as tenants from "../tenants"
describe("tenants", () => {

View File

@ -12,7 +12,7 @@ export enum SecretOption {
ENCRYPTION = "encryption",
}
function getSecret(secretOption: SecretOption): string {
export function getSecret(secretOption: SecretOption): string {
let secret, secretName
switch (secretOption) {
case SecretOption.ENCRYPTION:

View File

@ -0,0 +1,31 @@
import { encrypt, decrypt, SecretOption, getSecret } from "../encryption"
import env from "../../environment"
describe("encryption", () => {
it("should throw an error if API encryption key is not set", () => {
const jwt = getSecret(SecretOption.API)
expect(jwt).toBe(env.JWT_SECRET)
})
it("should throw an error if encryption key is not set", () => {
expect(() => getSecret(SecretOption.ENCRYPTION)).toThrow(
'Secret "ENCRYPTION_KEY" has not been set in environment.'
)
})
it("should encrypt and decrypt a string using API encryption key", () => {
env._set("API_ENCRYPTION_KEY", "api_secret")
const plaintext = "budibase"
const apiEncrypted = encrypt(plaintext, SecretOption.API)
const decrypted = decrypt(apiEncrypted, SecretOption.API)
expect(decrypted).toEqual(plaintext)
})
it("should encrypt and decrypt a string using encryption key", () => {
env._set("ENCRYPTION_KEY", "normal_secret")
const plaintext = "budibase"
const encryptionEncrypted = encrypt(plaintext, SecretOption.ENCRYPTION)
const decrypted = decrypt(encryptionEncrypted, SecretOption.ENCRYPTION)
expect(decrypted).toEqual(plaintext)
})
})

View File

@ -3,8 +3,8 @@ import {
getTenantId,
getTenantIDFromAppID,
isMultiTenant,
getPlatformURL,
} from "../context"
import env from "../environment"
import {
BBContext,
TenantResolutionStrategy,
@ -93,7 +93,7 @@ export const getTenantIDFromCtx = (
// subdomain
if (isAllowed(TenantResolutionStrategy.SUBDOMAIN)) {
// e.g. budibase.app or local.com:10000
const platformHost = new URL(env.PLATFORM_URL).host.split(":")[0]
const platformHost = new URL(getPlatformURL()).host.split(":")[0]
// e.g. tenant.budibase.app or tenant.local.com
const requestHost = ctx.host
// parse the tenant id from the difference

View File

@ -0,0 +1,184 @@
import { TenantResolutionStrategy } from "@budibase/types"
import { addTenantToUrl, isUserInAppTenant, getTenantIDFromCtx } from "../"
import { isMultiTenant, getTenantIDFromAppID } from "../../context"
jest.mock("../../context", () => ({
getTenantId: jest.fn(() => "budibase"),
isMultiTenant: jest.fn(() => true),
getTenantIDFromAppID: jest.fn(),
getPlatformURL: jest.fn(() => "https://app.com"),
DEFAULT_TENANT_ID: "default",
}))
const mockedIsMultiTenant = isMultiTenant as jest.MockedFunction<
typeof isMultiTenant
>
const mockedGetTenantIDFromAppID = getTenantIDFromAppID as jest.MockedFunction<
typeof getTenantIDFromAppID
>
describe("addTenantToUrl", () => {
it("should append tenantId parameter to the URL", () => {
const url = "https://budibase.com"
const expectedUrl = "https://budibase.com?tenantId=budibase"
expect(addTenantToUrl(url)).toEqual(expectedUrl)
})
it("should append tenantId parameter to the URL query string", () => {
const url = "https://budibase.com?var=test"
const expectedUrl = "https://budibase.com?var=test&tenantId=budibase"
expect(addTenantToUrl(url)).toEqual(expectedUrl)
})
it("should not append tenantId parameter to the URL if isMultiTenant is false", () => {
mockedIsMultiTenant.mockImplementation(() => false)
const url = "https://budibase.com"
const expectedUrl = "https://budibase.com"
expect(addTenantToUrl(url)).toEqual(expectedUrl)
})
})
describe("isUserInAppTenant", () => {
mockedGetTenantIDFromAppID.mockImplementation(() => "budibase")
const mockUser = { tenantId: "budibase" }
it("returns true if user tenant ID matches app tenant ID", () => {
const appId = "app-budibase"
const result = isUserInAppTenant(appId, mockUser)
expect(result).toBe(true)
})
it("uses default tenant ID if user is not provided", () => {
const appId = "app-budibase"
const result = isUserInAppTenant(appId)
expect(result).toBe(true)
})
it("uses default tenant ID if app tenant ID is not found", () => {
const appId = "not-budibase-app"
const result = isUserInAppTenant(appId, mockUser)
expect(result).toBe(true)
})
it("returns false if user tenant ID does not match app tenant ID", () => {
const appId = "app-budibase"
mockedGetTenantIDFromAppID.mockImplementation(() => "not-budibase")
const result = isUserInAppTenant(appId, mockUser)
expect(result).toBe(false)
})
})
let mockOpts: any = {}
function createCtx(opts: {
originalUrl?: string
headers?: Record<string, string>
qsTenantId?: string
userTenantId?: string
host?: string
path?: string
}) {
const createdCtx: any = {
originalUrl: opts.originalUrl || "budibase.com",
matched: [{ name: "name" }],
throw: jest.fn(),
request: { headers: {} },
}
if (opts.headers) {
createdCtx.request.headers = opts.headers
}
if (opts.qsTenantId) {
createdCtx.request.query = { tenantId: opts.qsTenantId }
}
if (opts.userTenantId) {
createdCtx.user = { tenantId: opts.userTenantId }
}
if (opts.host) {
createdCtx.host = opts.host
}
if (opts.path) {
createdCtx.matched = [
{
paramNames: [{ name: "tenantId" }],
params: () => ({ tenantId: opts.path }),
captures: jest.fn(),
},
]
}
return createdCtx as any
}
describe("getTenantIDFromCtx", () => {
describe("when tenant can be found", () => {
it("returns the tenant ID from the user object", () => {
mockedIsMultiTenant.mockImplementation(() => true)
const ctx = createCtx({ userTenantId: "budibase" })
expect(getTenantIDFromCtx(ctx, mockOpts)).toEqual("budibase")
})
it("returns the tenant ID from the header", () => {
mockedIsMultiTenant.mockImplementation(() => true)
const ctx = createCtx({ headers: { "x-budibase-tenant-id": "budibase" } })
mockOpts = { includeStrategies: [TenantResolutionStrategy.HEADER] }
expect(getTenantIDFromCtx(ctx, mockOpts)).toEqual("budibase")
})
it("returns the tenant ID from the query param", () => {
mockedIsMultiTenant.mockImplementation(() => true)
mockOpts = { includeStrategies: [TenantResolutionStrategy.QUERY] }
const ctx = createCtx({ qsTenantId: "budibase" })
expect(getTenantIDFromCtx(ctx, mockOpts)).toEqual("budibase")
})
it("returns the tenant ID from the subdomain", () => {
mockedIsMultiTenant.mockImplementation(() => true)
const ctx = createCtx({ host: "bb.app.com" })
mockOpts = { includeStrategies: [TenantResolutionStrategy.SUBDOMAIN] }
expect(getTenantIDFromCtx(ctx, mockOpts)).toEqual("bb")
})
it("returns the tenant ID from the path", () => {
mockedIsMultiTenant.mockImplementation(() => true)
const ctx = createCtx({ path: "bb" })
mockOpts = { includeStrategies: [TenantResolutionStrategy.PATH] }
expect(getTenantIDFromCtx(ctx, mockOpts)).toEqual("bb")
})
})
describe("when tenant cannot be found", () => {
it("throws a 403 error if allowNoTenant is false", () => {
const ctx = createCtx({})
mockOpts = {
allowNoTenant: false,
excludeStrategies: [
TenantResolutionStrategy.QUERY,
TenantResolutionStrategy.SUBDOMAIN,
TenantResolutionStrategy.PATH,
],
}
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeNull()
expect(ctx.throw).toBeCalledTimes(1)
expect(ctx.throw).toBeCalledWith(403, "Tenant id not set")
})
it("returns null if allowNoTenant is true", () => {
const ctx = createCtx({})
mockOpts = {
allowNoTenant: true,
excludeStrategies: [
TenantResolutionStrategy.QUERY,
TenantResolutionStrategy.SUBDOMAIN,
TenantResolutionStrategy.PATH,
],
}
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeNull()
})
})
it("returns the default tenant ID when isMultiTenant() returns false", () => {
mockedIsMultiTenant.mockImplementation(() => false)
const ctx = createCtx({})
expect(getTenantIDFromCtx(ctx, mockOpts)).toEqual("default")
})
})

View File

@ -8,8 +8,10 @@ import {
DocumentType,
SEPARATOR,
directCouchFind,
getGlobalUserParams,
pagination,
} from "./db"
import { BulkDocsResponse, User } from "@budibase/types"
import { BulkDocsResponse, SearchUsersRequest, User } from "@budibase/types"
import { getGlobalDB } from "./context"
import * as context from "./context"
@ -199,3 +201,41 @@ export const searchGlobalUsersByEmail = async (
}
return users
}
const PAGE_LIMIT = 8
export const paginatedUsers = async ({
page,
email,
appId,
}: SearchUsersRequest = {}) => {
const db = getGlobalDB()
// get one extra document, to have the next page
const opts: any = {
include_docs: true,
limit: PAGE_LIMIT + 1,
}
// add a startkey if the page was specified (anchor)
if (page) {
opts.startkey = page
}
// property specifies what to use for the page/anchor
let userList: User[],
property = "_id",
getKey
if (appId) {
userList = await searchGlobalUsersByApp(appId, opts)
getKey = (doc: any) => getGlobalUserByAppPage(appId, doc)
} else if (email) {
userList = await searchGlobalUsersByEmail(email, opts)
property = "email"
} else {
// no search, query allDocs
const response = await db.allDocs(getGlobalUserParams(null, opts))
userList = response.rows.map((row: any) => row.doc)
}
return pagination(userList, PAGE_LIMIT, {
paginate: true,
property,
getKey,
})
}

View File

@ -1,5 +1,5 @@
import env from "../environment"
export * from "../newid"
export * from "../docIds/newid"
const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt")
const SALT_ROUNDS = env.SALT_ROUNDS || 10

View File

@ -1,2 +1,3 @@
export * from "./hashing"
export * from "./utils"
export * from "./stringUtils"

View File

@ -0,0 +1,8 @@
export function validEmail(value: string) {
return (
value &&
!!value.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
)
)
}

View File

@ -1,4 +1,5 @@
import { structures, DBTestConfiguration } from "../../../tests"
import { structures } from "../../../tests"
import { DBTestConfiguration } from "../../../tests/extra"
import * as utils from "../../utils"
import * as db from "../../db"
import { Header } from "../../constants"

View File

@ -1,9 +1,6 @@
export * as mocks from "./mocks"
export * as structures from "./structures"
export { generator } from "./structures"
export * as testEnv from "./testEnv"
export * as testContainerUtils from "./testContainerUtils"
export * from "./jestUtils"
export { default as DBTestConfiguration } from "./DBTestConfiguration"

View File

@ -0,0 +1,3 @@
jest.mock("../../../../src/logging/alerts")
import * as _alerts from "../../../../src/logging/alerts"
export const alerts = jest.mocked(_alerts)

View File

@ -0,0 +1,123 @@
beforeAll(async () => {
const processors = await import("../../../../src/events/processors")
const events = await import("../../../../src/events")
jest.spyOn(processors.analyticsProcessor, "processEvent")
jest.spyOn(events.identification, "identifyTenantGroup")
jest.spyOn(events.identification, "identifyUser")
jest.spyOn(events.backfill, "appSucceeded")
jest.spyOn(events.backfill, "tenantSucceeded")
jest.spyOn(events.account, "created")
jest.spyOn(events.account, "deleted")
jest.spyOn(events.account, "verified")
jest.spyOn(events.app, "created")
jest.spyOn(events.app, "updated")
jest.spyOn(events.app, "deleted")
jest.spyOn(events.app, "published")
jest.spyOn(events.app, "unpublished")
jest.spyOn(events.app, "templateImported")
jest.spyOn(events.app, "fileImported")
jest.spyOn(events.app, "versionUpdated")
jest.spyOn(events.app, "versionReverted")
jest.spyOn(events.app, "reverted")
jest.spyOn(events.app, "exported")
jest.spyOn(events.auth, "login")
jest.spyOn(events.auth, "logout")
jest.spyOn(events.auth, "SSOCreated")
jest.spyOn(events.auth, "SSOUpdated")
jest.spyOn(events.auth, "SSOActivated")
jest.spyOn(events.auth, "SSODeactivated")
jest.spyOn(events.automation, "created")
jest.spyOn(events.automation, "deleted")
jest.spyOn(events.automation, "tested")
jest.spyOn(events.automation, "stepCreated")
jest.spyOn(events.automation, "stepDeleted")
jest.spyOn(events.automation, "triggerUpdated")
jest.spyOn(events.datasource, "created")
jest.spyOn(events.datasource, "updated")
jest.spyOn(events.datasource, "deleted")
jest.spyOn(events.email, "SMTPCreated")
jest.spyOn(events.email, "SMTPUpdated")
jest.spyOn(events.layout, "created")
jest.spyOn(events.layout, "deleted")
jest.spyOn(events.org, "nameUpdated")
jest.spyOn(events.org, "logoUpdated")
jest.spyOn(events.org, "platformURLUpdated")
jest.spyOn(events.org, "analyticsOptOut")
jest.spyOn(events.installation, "versionChecked")
jest.spyOn(events.query, "created")
jest.spyOn(events.query, "updated")
jest.spyOn(events.query, "deleted")
jest.spyOn(events.query, "imported")
jest.spyOn(events.query, "previewed")
jest.spyOn(events.role, "created")
jest.spyOn(events.role, "updated")
jest.spyOn(events.role, "deleted")
jest.spyOn(events.role, "assigned")
jest.spyOn(events.role, "unassigned")
jest.spyOn(events.rows, "imported")
jest.spyOn(events.rows, "created")
jest.spyOn(events.screen, "created")
jest.spyOn(events.screen, "deleted")
jest.spyOn(events.user, "created")
jest.spyOn(events.user, "updated")
jest.spyOn(events.user, "deleted")
jest.spyOn(events.user, "permissionAdminAssigned")
jest.spyOn(events.user, "permissionAdminRemoved")
jest.spyOn(events.user, "permissionBuilderAssigned")
jest.spyOn(events.user, "permissionBuilderRemoved")
jest.spyOn(events.user, "invited")
jest.spyOn(events.user, "inviteAccepted")
jest.spyOn(events.user, "passwordForceReset")
jest.spyOn(events.user, "passwordUpdated")
jest.spyOn(events.user, "passwordResetRequested")
jest.spyOn(events.user, "passwordReset")
jest.spyOn(events.group, "created")
jest.spyOn(events.group, "updated")
jest.spyOn(events.group, "deleted")
jest.spyOn(events.group, "usersAdded")
jest.spyOn(events.group, "usersDeleted")
jest.spyOn(events.group, "createdOnboarding")
jest.spyOn(events.group, "permissionsEdited")
jest.spyOn(events.serve, "servedBuilder")
jest.spyOn(events.serve, "servedApp")
jest.spyOn(events.serve, "servedAppPreview")
jest.spyOn(events.table, "created")
jest.spyOn(events.table, "updated")
jest.spyOn(events.table, "deleted")
jest.spyOn(events.table, "exported")
jest.spyOn(events.table, "imported")
jest.spyOn(events.view, "created")
jest.spyOn(events.view, "updated")
jest.spyOn(events.view, "deleted")
jest.spyOn(events.view, "exported")
jest.spyOn(events.view, "filterCreated")
jest.spyOn(events.view, "filterUpdated")
jest.spyOn(events.view, "filterDeleted")
jest.spyOn(events.view, "calculationCreated")
jest.spyOn(events.view, "calculationUpdated")
jest.spyOn(events.view, "calculationDeleted")
jest.spyOn(events.plugin, "init")
jest.spyOn(events.plugin, "imported")
jest.spyOn(events.plugin, "deleted")
})

View File

@ -0,0 +1,17 @@
const mockFetch = jest.fn((url: any, opts: any) => {
const fetch = jest.requireActual("node-fetch")
const env = jest.requireActual("../../../../src/environment").default
if (url.includes(env.COUCH_DB_URL)) {
return fetch(url, opts)
}
return undefined
})
const enable = () => {
jest.mock("node-fetch", () => mockFetch)
}
export default {
...mockFetch,
enable,
}

View File

@ -1,9 +1,10 @@
jest.mock("../../../src/accounts")
import * as _accounts from "../../../src/accounts"
jest.mock("../../../../src/accounts")
import * as _accounts from "../../../../src/accounts"
export const accounts = jest.mocked(_accounts)
export * as date from "./date"
export * as licenses from "./licenses"
export { default as fetch } from "./fetch"
import "./posthog"
export * from "./alerts"
import "./events"
import "./posthog"

View File

@ -86,6 +86,10 @@ export const useAuditLogs = () => {
return useFeature(Feature.AUDIT_LOGS)
}
export const useScimIntegration = () => {
return useFeature(Feature.SCIM)
}
// QUOTAS
export const setAutomationLogsQuota = (value: number) => {

View File

@ -1,5 +1,5 @@
import { generator, uuid } from "."
import * as db from "../../../src/db/utils"
import { generateGlobalUserID } from "../../../../src/docIds"
import {
Account,
AccountSSOProvider,
@ -39,7 +39,7 @@ export const cloudAccount = (): CloudAccount => {
return {
...account(),
hosting: Hosting.CLOUD,
budibaseUserId: db.generateGlobalUserID(),
budibaseUserId: generateGlobalUserID(),
}
}

View File

@ -1,6 +1,6 @@
import { generator } from "."
import { App } from "@budibase/types"
import { DEFAULT_TENANT_ID, DocumentType } from "../../../src/constants"
import { DEFAULT_TENANT_ID, DocumentType } from "../../../../src/constants"
export function app(id: string): App {
return {

View File

@ -1,5 +1,5 @@
import { structures } from ".."
import { newid } from "../../../src/newid"
import { newid } from "../../../../src/docIds/newid"
export function id() {
return `db_${newid()}`

View File

@ -10,3 +10,4 @@ export * as tenant from "./tenants"
export * as users from "./users"
export * as userGroups from "./userGroups"
export { generator } from "./generator"
export * as scim from "./scim"

View File

@ -0,0 +1,81 @@
import { ScimCreateGroupRequest, ScimCreateUserRequest } from "@budibase/types"
import { uuid } from "./common"
import { generator } from "./generator"
import _ from "lodash"
interface CreateUserRequestFields {
externalId: string
email: string
firstName: string
lastName: string
username: string
}
export function createUserRequest(userData?: Partial<CreateUserRequestFields>) {
const defaultValues = {
externalId: uuid(),
email: generator.email(),
firstName: generator.first(),
lastName: generator.last(),
username: generator.name(),
}
const { externalId, email, firstName, lastName, username } = _.assign(
defaultValues,
userData
)
let user: ScimCreateUserRequest = {
schemas: [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
],
externalId,
userName: username,
active: true,
emails: [
{
primary: true,
type: "work",
value: email,
},
],
meta: {
resourceType: "User",
},
roles: [],
}
if (firstName || lastName) {
user.name = {
formatted: [firstName, lastName].filter(s => s).join(" "),
familyName: lastName,
givenName: firstName,
}
}
return user
}
export function createGroupRequest(groupData?: {
externalId?: string
displayName?: string
}) {
const { externalId = uuid(), displayName = generator.word() } =
groupData || {}
const group: ScimCreateGroupRequest = {
schemas: [
"urn:ietf:params:scim:schemas:core:2.0:Group",
"http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group",
],
externalId: externalId,
displayName: displayName,
meta: {
resourceType: "Group",
created: new Date(),
lastModified: new Date(),
},
}
return group
}

View File

@ -1,4 +1,4 @@
import { newid } from "../../../src/newid"
import { newid } from "../../../../src/docIds/newid"
export function id() {
return `tenant-${newid()}`

View File

@ -1,5 +1,5 @@
import "./mocks"
import * as structures from "./structures"
import "../core/utilities/mocks"
import * as structures from "../core/utilities/structures"
import * as testEnv from "./testEnv"
import * as context from "../../src/context"

View File

@ -0,0 +1,2 @@
export * as testEnv from "./testEnv"
export { default as DBTestConfiguration } from "./DBTestConfiguration"

View File

@ -1,6 +1,6 @@
import env from "../../src/environment"
import * as context from "../../src/context"
import * as structures from "./structures"
import * as structures from "../core/utilities/structures"
// TENANCY

View File

@ -1 +1 @@
export * from "./utilities"
export * from "./core/utilities"

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