Merge branch 'develop' of github.com:Budibase/budibase into spreadsheet-integration

This commit is contained in:
Andrew Kingston 2023-04-11 17:36:28 +01:00
commit 0ee63417c1
293 changed files with 9058 additions and 7786 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

2
.nvmrc
View File

@ -1 +1 @@
v14.19.3
v14.20.1

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

@ -1 +1 @@
3.11.1
3.10.0

View File

@ -1,2 +1,2 @@
nodejs 14.19.3
nodejs 14.20.1
python 3.10.0

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

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

View File

@ -80,7 +80,6 @@ globals:
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,9 +1,7 @@
{
"version": "2.4.42-alpha.5",
"version": "2.4.44-alpha.12",
"npmClient": "yarn",
"packages": [
"packages/*"
],
"packages": ["packages/*"],
"command": {
"publish": {
"ignoreChanges": [
@ -13,6 +11,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

@ -12,7 +12,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",
@ -23,12 +23,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",
@ -37,15 +38,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}\"",
@ -53,12 +55,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 .",
@ -67,16 +69,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.5",
"version": "2.4.44-alpha.12",
"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.5",
"@budibase/types": "2.4.44-alpha.12",
"@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",
@ -64,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",
@ -80,6 +79,7 @@
"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",

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

@ -2,6 +2,7 @@ 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

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

@ -214,6 +214,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 +277,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 * 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

@ -8,3 +8,4 @@ export { default as Replication } from "./Replication"
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

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

@ -434,8 +434,8 @@ 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 +444,7 @@ export function pagination(
}: {
paginate: boolean
property: string
getKey?: (doc: any) => string | undefined
getKey?: (doc: T) => string | undefined
} = {
paginate: true,
property: "_id",

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

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

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

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

@ -3,5 +3,4 @@ process.env.MULTI_TENANCY = "1"
process.env.NODE_ENV = "jest"
process.env.MOCK_REDIS = "1"
process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error"
process.env.ENABLE_4XX_HTTP_LOGGING = "0"
process.env.REDIS_PASSWORD = "budibase"

View File

@ -1,4 +1,3 @@
import "./logging"
import env from "../src/environment"
import { cleanup } from "../src/timers"
import { mocks, testContainerUtils } from "./utilities"

View File

@ -1,34 +0,0 @@
export enum LogLevel {
TRACE = "trace",
DEBUG = "debug",
INFO = "info",
WARN = "warn",
ERROR = "error",
}
const LOG_INDEX: { [key in LogLevel]: number } = {
[LogLevel.TRACE]: 1,
[LogLevel.DEBUG]: 2,
[LogLevel.INFO]: 3,
[LogLevel.WARN]: 4,
[LogLevel.ERROR]: 5,
}
const setIndex = LOG_INDEX[process.env.LOG_LEVEL as LogLevel]
if (setIndex > LOG_INDEX.trace) {
global.console.trace = jest.fn()
}
if (setIndex > LOG_INDEX.debug) {
global.console.debug = jest.fn()
}
if (setIndex > LOG_INDEX.info) {
global.console.info = jest.fn()
global.console.log = jest.fn()
}
if (setIndex > LOG_INDEX.warn) {
global.console.warn = jest.fn()
}

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

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

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

@ -8,4 +8,6 @@ export * as plugins from "./plugins"
export * as sso from "./sso"
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,69 @@
import { ScimCreateGroupRequest, ScimCreateUserRequest } from "@budibase/types"
import { uuid } from "./common"
import { generator } from "./generator"
export function createUserRequest(userData?: {
externalId?: string
email?: string
firstName?: string
lastName?: string
username?: string
}) {
const {
externalId = uuid(),
email = generator.email(),
firstName = generator.first(),
lastName = generator.last(),
username = generator.name(),
} = userData || {}
const 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",
},
name: {
formatted: generator.name(),
familyName: lastName,
givenName: firstName,
},
roles: [],
}
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

@ -0,0 +1,10 @@
import { UserGroup } from "@budibase/types"
import { generator } from "./generator"
export function userGroup(): UserGroup {
return {
name: generator.word(),
icon: generator.word(),
color: generator.word(),
}
}

View File

@ -1217,15 +1217,6 @@
dependencies:
"@types/koa" "*"
"@types/koa-pino-logger@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/koa-pino-logger/-/koa-pino-logger-3.0.0.tgz#275d4b000abc14b1928dc2e9ab476c8296a64b6a"
integrity sha512-sP+12JNX01q+nHpCRqkVIuLjaRemQEfDoFg0evpTnjUEI3jUI2ZrOkhQ5coxn3yVm2tedui/2YhlaPn/XrYNWA==
dependencies:
"@types/koa" "*"
"@types/pino" "*"
"@types/pino-http" "*"
"@types/koa@*", "@types/koa@2.13.4":
version "2.13.4"
resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b"
@ -1273,44 +1264,6 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650"
integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA==
"@types/pino-http@*", "@types/pino-http@5.8.1":
version "5.8.1"
resolved "https://registry.yarnpkg.com/@types/pino-http/-/pino-http-5.8.1.tgz#ebb194750ad2f9245c3028b5d2c4e6d64f685ba9"
integrity sha512-A9MW6VCnx5ii7s+Fs5aFIw+aSZcBCpsZ/atpxamu8tTsvWFacxSf2Hrn1Ohn1jkVRB/LiPGOapRXcFawDBnDnA==
dependencies:
"@types/pino" "6.3"
"@types/pino-pretty@*":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/pino-pretty/-/pino-pretty-5.0.0.tgz#aa7a61cfd553b051764acfa0a49872f7a09a1722"
integrity sha512-N1uzqSzioqz8R3AkDbSJwcfDWeI3YMPNapSQQhnB2ISU4NYgUIcAh+hYT5ygqBM+klX4htpEhXMmoJv3J7GrdA==
dependencies:
pino-pretty "*"
"@types/pino-std-serializers@*":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1e28b80b554c8222858e99a4e0fc77fd070e10e8"
integrity sha512-gXfUZx2xIBbFYozGms53fT0nvkacx/+62c8iTxrEqH5PkIGAQvDbXg2774VWOycMPbqn5YJBQ3BMsg4Li3dWbg==
dependencies:
pino-std-serializers "*"
"@types/pino@*":
version "7.0.5"
resolved "https://registry.yarnpkg.com/@types/pino/-/pino-7.0.5.tgz#1c84a81b924a6a9e263dbb581dffdbad7a3c60c4"
integrity sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg==
dependencies:
pino "*"
"@types/pino@6.3":
version "6.3.12"
resolved "https://registry.yarnpkg.com/@types/pino/-/pino-6.3.12.tgz#4425db6ced806109c3df957100cba9dfcd73c228"
integrity sha512-dsLRTq8/4UtVSpJgl9aeqHvbh6pzdmjYD3C092SYgLD2TyoCqHpTJk6vp8DvCTGGc7iowZ2MoiYiVUUCcu7muw==
dependencies:
"@types/node" "*"
"@types/pino-pretty" "*"
"@types/pino-std-serializers" "*"
sonic-boom "^2.1.0"
"@types/pouchdb-adapter-cordova-sqlite@*":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-cordova-sqlite/-/pouchdb-adapter-cordova-sqlite-1.0.1.tgz#49e5ee6df7cc0c23196fcb340f43a560e74eb1d6"
@ -2539,6 +2492,16 @@ duplexer3@^0.1.4:
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==
duplexify@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0"
integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==
dependencies:
end-of-stream "^1.4.1"
inherits "^2.0.3"
readable-stream "^3.1.1"
stream-shift "^1.0.0"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@ -2728,9 +2691,9 @@ extsprintf@^1.2.0:
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
fast-copy@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.0.tgz#875ebf33b13948ae012b6e51d33da5e6e7571ab8"
integrity sha512-4HzS+9pQ5Yxtv13Lhs1Z1unMXamBdn5nA4bEi1abYpDNSpSp7ODYQ1KPMF6nTatfEzgH6/zPvXKU1zvHiUjWlA==
version "3.0.1"
resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa"
integrity sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==
fast-deep-equal@^3.1.1:
version "3.1.3"
@ -2742,7 +2705,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fast-redact@^3.1.1:
fast-redact@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
@ -2752,6 +2715,13 @@ fast-safe-stringify@^2.1.1:
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
fast-url-parser@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d"
integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==
dependencies:
punycode "^1.3.2"
fb-watchman@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
@ -4027,6 +3997,13 @@ koa-passport@4.1.4:
dependencies:
passport "^0.4.0"
koa-pino-logger@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/koa-pino-logger/-/koa-pino-logger-4.0.0.tgz#a45c9fd0a39e9cd84ac23da502055054ee51b80d"
integrity sha512-YI/LB9ajyLPpjvf6e+7Ewmn+OQkCJpu/Y9eI1n7fnipu5Y1NchuNlke0mqh3/2z+5oDYr7pijjOWruEDIfua2A==
dependencies:
pino-http "^6.5.0"
koa@2.13.4, koa@^2.13.4:
version "2.13.4"
resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e"
@ -4623,6 +4600,11 @@ object-inspect@^1.9.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
on-exit-leak-free@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz#b39c9e3bf7690d890f4861558b0d7b90a442d209"
integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==
on-exit-leak-free@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4"
@ -4857,7 +4839,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
pino-abstract-transport@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
@ -4865,10 +4847,28 @@ pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
readable-stream "^4.0.0"
split2 "^4.0.0"
pino-pretty@*:
version "9.1.1"
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-9.1.1.tgz#e7d64c1db98266ca428ab56567b844ba780cd0e1"
integrity sha512-iJrnjgR4FWQIXZkUF48oNgoRI9BpyMhaEmihonHeCnZ6F50ZHAS4YGfGBT/ZVNsPmd+hzkIPGzjKdY08+/yAXw==
pino-abstract-transport@v0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz#4b54348d8f73713bfd14e3dc44228739aa13d9c0"
integrity sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==
dependencies:
duplexify "^4.1.2"
split2 "^4.0.0"
pino-http@^6.5.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-6.6.0.tgz#d0a1deacada8c93327fdaa48f5bdc94bc43d3407"
integrity sha512-PlItaK2MLpoIMLEcClhfb1VQk/o6fKppINl5s6sPE/4rvufkdO3kCSs/92EwrBsB1yssRCQqDV+w1xpYuPVnjg==
dependencies:
fast-url-parser "^1.1.3"
get-caller-file "^2.0.5"
pino "^7.5.0"
pino-std-serializers "^5.0.0"
pino-pretty@10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-10.0.0.tgz#fd2f307ee897289f63d09b0b804ac2ecc9a18516"
integrity sha512-zKFjYXBzLaLTEAN1ayKpHXtL5UeRQC7R3lvhKe7fWs7hIVEjKGG/qIXwQt9HmeUp71ogUd/YcW+LmMwRp4KT6Q==
dependencies:
colorette "^2.0.7"
dateformat "^4.6.3"
@ -4885,27 +4885,32 @@ pino-pretty@*:
sonic-boom "^3.0.0"
strip-json-comments "^3.1.1"
pino-std-serializers@*, pino-std-serializers@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz#307490fd426eefc95e06067e85d8558603e8e844"
integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==
pino-std-serializers@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz#1791ccd2539c091ae49ce9993205e2cd5dbba1e2"
integrity sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==
pino@*:
version "8.8.0"
resolved "https://registry.yarnpkg.com/pino/-/pino-8.8.0.tgz#1f0d6695a224aa06afc7ad60f2ccc4772d3b9233"
integrity sha512-cF8iGYeu2ODg2gIwgAHcPrtR63ILJz3f7gkogaHC/TXVVXxZgInmNYiIpDYEwgEkxZti2Se6P2W2DxlBIZe6eQ==
pino-std-serializers@^5.0.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-5.6.0.tgz#31b141155d6520967c5ec72944d08fb45c490fd3"
integrity sha512-VdUXCw8gO+xhir7sFuoYSjTnzB+TMDGxhAC/ph3YS3sdHnXNdsK0wMtADNUltfeGkn2KDxEM21fnjF3RwXyC8A==
pino@^7.5.0:
version "7.11.0"
resolved "https://registry.yarnpkg.com/pino/-/pino-7.11.0.tgz#0f0ea5c4683dc91388081d44bff10c83125066f6"
integrity sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==
dependencies:
atomic-sleep "^1.0.0"
fast-redact "^3.1.1"
on-exit-leak-free "^2.1.0"
pino-abstract-transport v1.0.0
pino-std-serializers "^6.0.0"
process-warning "^2.0.0"
fast-redact "^3.0.0"
on-exit-leak-free "^0.2.0"
pino-abstract-transport v0.5.0
pino-std-serializers "^4.0.0"
process-warning "^1.0.0"
quick-format-unescaped "^4.0.3"
real-require "^0.2.0"
safe-stable-stringify "^2.3.1"
sonic-boom "^3.1.0"
thread-stream "^2.0.0"
real-require "^0.1.0"
safe-stable-stringify "^2.1.0"
sonic-boom "^2.2.1"
thread-stream "^0.15.1"
pirates@^4.0.4:
version "4.0.5"
@ -5156,10 +5161,10 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process-warning@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.1.0.tgz#1e60e3bfe8183033bbc1e702c2da74f099422d1a"
integrity sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg==
process-warning@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616"
integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==
process@^0.11.10:
version "0.11.10"
@ -5207,6 +5212,11 @@ punycode@1.3.2:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==
punycode@^1.3.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@ -5330,10 +5340,10 @@ readline-sync@^1.4.9:
resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b"
integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
real-require@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
real-require@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381"
integrity sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==
redis-commands@1.7.0:
version "1.7.0"
@ -5477,7 +5487,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-stable-stringify@^2.3.1:
safe-stable-stringify@^2.1.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz#ec7b037768098bf65310d1d64370de0dc02353aa"
integrity sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==
@ -5589,14 +5599,14 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
sonic-boom@^2.1.0:
sonic-boom@^2.2.1:
version "2.8.0"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.8.0.tgz#c1def62a77425090e6ad7516aad8eb402e047611"
integrity sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==
dependencies:
atomic-sleep "^1.0.0"
sonic-boom@^3.0.0, sonic-boom@^3.1.0:
sonic-boom@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.1.tgz#972ceab831b5840a08a002fa95a672008bda1c38"
integrity sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A==
@ -5701,6 +5711,11 @@ step@0.0.x:
resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2"
integrity sha512-qSSeQinUJk2w38vUFobjFoE307GqsozMC8VisOCkJLpklvKPT0ptPHwWOrENoag8rgLudvTkfP3bancwP93/Jw==
stream-shift@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
stream-to-array@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353"
@ -5897,12 +5912,12 @@ testcontainers@4.7.0:
stream-to-array "^2.3.0"
tar-fs "^2.1.0"
thread-stream@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.3.0.tgz#4fc07fb39eff32ae7bad803cb7dd9598349fed33"
integrity sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==
thread-stream@^0.15.1:
version "0.15.2"
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-0.15.2.tgz#fb95ad87d2f1e28f07116eb23d85aba3bc0425f4"
integrity sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==
dependencies:
real-require "^0.2.0"
real-require "^0.1.0"
through2@3.0.2:
version "3.0.2"

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "2.4.42-alpha.5",
"version": "2.4.44-alpha.12",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,8 +38,8 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "1.2.1",
"@budibase/shared-core": "2.4.42-alpha.5",
"@budibase/string-templates": "2.4.42-alpha.5",
"@budibase/shared-core": "2.4.44-alpha.12",
"@budibase/string-templates": "2.4.44-alpha.12",
"@spectrum-css/accordion": "3.0.24",
"@spectrum-css/actionbutton": "1.0.1",
"@spectrum-css/actiongroup": "1.0.1",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "2.4.42-alpha.5",
"version": "2.4.44-alpha.12",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -58,11 +58,11 @@
}
},
"dependencies": {
"@budibase/bbui": "2.4.42-alpha.5",
"@budibase/client": "2.4.42-alpha.5",
"@budibase/frontend-core": "2.4.42-alpha.5",
"@budibase/shared-core": "2.4.42-alpha.5",
"@budibase/string-templates": "2.4.42-alpha.5",
"@budibase/bbui": "2.4.44-alpha.12",
"@budibase/client": "2.4.44-alpha.12",
"@budibase/frontend-core": "2.4.44-alpha.12",
"@budibase/shared-core": "2.4.44-alpha.12",
"@budibase/string-templates": "2.4.44-alpha.12",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",

View File

@ -1,10 +0,0 @@
{
"reporterEnabled": "mochawesome",
"mochawesomeReporterOptions": {
"reportDir": "cypress/reports",
"quiet": true,
"overwrite": false,
"html": false,
"json": true
}
}

View File

@ -1,43 +0,0 @@
const testConfig = require("./testConfig.json")
// normal development system
const SERVER_PORT = testConfig.env.PORT
const WORKER_PORT = testConfig.env.WORKER_PORT
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "cypress"
}
process.env.ENABLE_ANALYTICS = "0"
process.env.JWT_SECRET = testConfig.env.JWT_SECRET
process.env.SELF_HOSTED = 1
process.env.WORKER_URL = `http://localhost:${WORKER_PORT}/`
process.env.APPS_URL = `http://localhost:${SERVER_PORT}/`
process.env.MINIO_URL = `http://localhost:4004`
process.env.MINIO_ACCESS_KEY = "budibase"
process.env.MINIO_SECRET_KEY = "budibase"
process.env.COUCH_DB_USER = "budibase"
process.env.COUCH_DB_PASSWORD = "budibase"
process.env.INTERNAL_API_KEY = "budibase"
process.env.ALLOW_DEV_AUTOMATIONS = 1
process.env.MOCK_REDIS = 1
// Stop info logs polluting test outputs
process.env.LOG_LEVEL = "error"
exports.run = (serverLoc = "../server/dist", workerLoc = "../worker/dist") => {
// require("dotenv").config({ path: resolve(dir, ".env") })
// don't make this a variable or top level require
// it will cause environment module to be loaded prematurely
// override the port with the worker port temporarily
process.env.PORT = WORKER_PORT
require(workerLoc)
// override the port with the server port
process.env.PORT = SERVER_PORT
require(serverLoc)
}
if (require.main === module) {
exports.run()
}

View File

@ -58,7 +58,7 @@
/>
{#if openBlocks[block.id]}
<Divider noMargin />
{#if filteredResults?.[idx]?.outputs.iterations}
{#if filteredResults?.[idx]?.outputs?.iterations}
<div style="display: flex; padding: 10px 10px 0px 12px;">
<Icon name="Reuse" />
<div style="margin-left: 10px;">

View File

@ -5,20 +5,33 @@
export let preAuthStep
export let datasource
export let disabled
export let samePage
$: tenantId = $auth.tenantId
</script>
<button
class:disabled
{disabled}
on:click={async () => {
let ds = datasource
let appId = $store.appId
if (!ds) {
ds = await preAuthStep()
const resp = await preAuthStep()
if (resp.datasource && resp.appId) {
ds = resp.datasource
appId = resp.appId
} else {
ds = resp
}
}
const url = `/api/global/auth/${tenantId}/datasource/google?datasourceId=${ds._id}&appId=${appId}`
if (samePage) {
window.location = url
} else {
window.open(url, "_blank")
}
window.open(
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${ds._id}&appId=${$store.appId}`,
"_blank"
)
}}
>
<img src={GoogleLogo} alt="google icon" />
@ -26,6 +39,10 @@
</button>
<style>
.disabled {
opacity: 0.5;
}
button {
width: 195px;
height: 40px;

View File

@ -3,6 +3,7 @@
import GoogleLogo from "assets/google-logo.png"
import { auth, organisation } from "stores/portal"
export let samePage
let show
$: tenantId = $auth.tenantId
@ -12,8 +13,14 @@
{#if show}
<FancyButton
icon={GoogleLogo}
on:click={() =>
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
on:click={() => {
const url = `/api/global/auth/${tenantId}/google`
if (samePage) {
window.location = url
} else {
window.open(url, "_blank")
}
}}
>
Log in with Google
</FancyButton>

View File

@ -9,6 +9,8 @@
import { oidc, organisation, auth } from "stores/portal"
import { onMount } from "svelte"
export let samePage
$: show = $organisation.oidc
let preDefinedIcons = {
@ -35,11 +37,14 @@
{#if show}
<FancyButton
icon={src}
on:click={() =>
window.open(
`/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`,
"_blank"
)}
on:click={() => {
const url = `/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`
if (samePage) {
window.location = url
} else {
window.open(url, "_blank")
}
}}
>
{`Log in with ${$oidc.name || "OIDC"}`}
</FancyButton>

View File

@ -78,8 +78,8 @@
<Layout gap="S" noPadding>
{#if loaded && ($organisation.google || $organisation.oidc)}
<FancyForm>
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
<GoogleButton />
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} samePage />
<GoogleButton samePage />
</FancyForm>
{/if}
{#if !$organisation.isSSOEnforced}

View File

@ -1,12 +1,15 @@
<script>
import { Button, FancyForm, FancyInput, FancyCheckbox } from "@budibase/bbui"
import GoogleButton from "components/backend/DatasourceNavigator/_components/GoogleButton.svelte"
import { capitalise } from "helpers/helpers"
import PanelHeader from "./PanelHeader.svelte"
import { helpers } from "@budibase/shared-core"
export let title = ""
export let onBack = null
export let onNext = () => {}
export let fields = {}
export let type = ""
let errors = {}
@ -57,8 +60,9 @@
}
$: isValid = getIsValid(fields, errors, values)
$: isGoogle = helpers.isGoogleSheets(type)
const handleNext = () => {
const handleNext = async () => {
const parsedValues = {}
Object.entries(values).forEach(([name, value]) => {
@ -69,7 +73,10 @@
}
})
return onNext(parsedValues)
if (isGoogle) {
parsedValues.isGoogle = isGoogle
}
return await onNext(parsedValues)
}
</script>
@ -99,7 +106,11 @@
{/each}
</FancyForm>
</div>
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
{#if isGoogle}
<GoogleButton disabled={!isValid} preAuthStep={handleNext} samePage />
{:else}
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
{/if}
</div>
<style>

View File

@ -4,19 +4,20 @@
import DataPanel from "./_components/DataPanel.svelte"
import DatasourceConfigPanel from "./_components/DatasourceConfigPanel.svelte"
import ExampleApp from "./_components/ExampleApp.svelte"
import { FancyButton, notifications, Modal } from "@budibase/bbui"
import { FancyButton, notifications, Modal, Body } from "@budibase/bbui"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import { SplitPage } from "@budibase/frontend-core"
import { API } from "api"
import { store, automationStore } from "builderStore"
import { saveDatasource } from "builderStore/datasource"
import { integrations } from "stores/backend"
import { auth, admin } from "stores/portal"
import { auth, admin, organisation } from "stores/portal"
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
import Spinner from "components/common/Spinner.svelte"
import { helpers } from "@budibase/shared-core"
let name = "My first app"
let url = "my-first-app"
@ -25,10 +26,11 @@
let plusIntegrations = {}
let integrationsLoading = true
$: getIntegrations()
let creationLoading = false
let uploadModal
let googleComplete = false
$: getIntegrations()
const createApp = async useSampleData => {
creationLoading = true
@ -62,6 +64,7 @@
await store.actions.screens.save(defaultScreenTemplate)
appId = createdApp.instance._id
return createdApp
} catch (e) {
creationLoading = false
throw e
@ -74,6 +77,13 @@
const newPlusIntegrations = {}
Object.entries($integrations).forEach(([integrationType, schema]) => {
// google sheets not available in self-host
if (
helpers.isGoogleSheets(integrationType) &&
!$organisation.googleDatasourceConfigured
) {
return
}
if (schema?.plus) {
newPlusIntegrations[integrationType] = schema
}
@ -92,12 +102,17 @@
notifications.success(`App created successfully`)
}
const handleCreateApp = async ({ datasourceConfig, useSampleData }) => {
const handleCreateApp = async ({
datasourceConfig,
useSampleData,
isGoogle,
}) => {
try {
await createApp(useSampleData)
const app = await createApp(useSampleData)
let datasource
if (datasourceConfig) {
await saveDatasource({
datasource = await saveDatasource({
plus: true,
auth: undefined,
name: plusIntegrations[stage].friendlyName,
@ -107,7 +122,14 @@
})
}
goToApp()
store.set()
if (isGoogle) {
googleComplete = true
return { datasource, appId: app.appId }
} else {
goToApp()
}
} catch (e) {
console.log(e)
creationLoading = false
@ -127,8 +149,15 @@
<SplitPage>
{#if stage === "name"}
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
{:else if googleComplete}
<div class="centered">
<Body
>Please login to your Google account in the new tab which as opened to
continue.</Body
>
</div>
{:else if integrationsLoading || creationLoading}
<div class="spinner">
<div class="centered">
<Spinner />
</div>
{:else if stage === "data"}
@ -174,8 +203,13 @@
<DatasourceConfigPanel
title={plusIntegrations[stage].friendlyName}
fields={plusIntegrations[stage].datasource}
type={stage}
onBack={() => (stage = "data")}
onNext={data => handleCreateApp({ datasourceConfig: data })}
onNext={data => {
const isGoogle = data.isGoogle
delete data.isGoogle
return handleCreateApp({ datasourceConfig: data, isGoogle })
}}
/>
{:else}
<p>There was an problem. Please refresh the page and try again.</p>
@ -186,7 +220,7 @@
</SplitPage>
<style>
.spinner {
.centered {
display: flex;
justify-content: center;
align-items: center;

View File

@ -27,6 +27,7 @@
import { onMount } from "svelte"
import { API } from "api"
import { organisation, admin, licensing } from "stores/portal"
import Scim from "./scim.svelte"
const ConfigTypes = {
Google: "google",
@ -606,12 +607,17 @@
</Tags>
</div>
</Layout>
<div>
<Button disabled={oidcSaveButtonDisabled} cta on:click={() => saveOIDC()}>
Save
</Button>
</div>
{/if}
{#if $licensing.scimEnabled}
<Divider />
<Scim />
{/if}
</Layout>
<style>

View File

@ -0,0 +1,161 @@
<script>
import {
Button,
Heading,
Label,
notifications,
Layout,
Body,
Toggle,
Input,
Icon,
Helpers,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { API } from "api"
import { organisation, auth } from "stores/portal"
const configType = "scim"
$: scimEnabled = false
$: apiKey = null
async function saveSCIM() {
try {
await API.saveConfig({
type: configType,
config: {
enabled: scimEnabled,
},
})
notifications.success(`Settings saved`)
} catch (e) {
notifications.error(e.message)
return
}
}
async function fetchConfig() {
try {
const scimConfig = await API.getConfig(configType)
scimEnabled = scimConfig?.config?.enabled
} catch (error) {
console.error(error)
notifications.error(
`Error fetching SCIM config - ${error?.message || "unknown error"}`
)
}
}
async function fetchAPIKey() {
try {
apiKey = await auth.fetchAPIKey()
} catch (err) {
notifications.error(
`Unable to fetch API key - ${err?.message || "unknown error"}`
)
}
}
onMount(async () => {
await Promise.all(fetchConfig(), fetchAPIKey())
})
const copyToClipboard = async value => {
await Helpers.copyToClipboard(value)
notifications.success("Copied")
}
$: settings = [
{
title: "Provisioning URL",
value: `${$organisation.platformUrl}/api/global/scim/v2`,
},
{ title: "Provisioning Token", value: apiKey },
]
</script>
<Layout gap="XS" noPadding>
<div class="provider-title">
<Heading size="S">SCIM</Heading>
</div>
<Body size="S">Sync users with your identity provider.</Body>
<div class="form-row">
<Label size="L">Activated</Label>
<Toggle text="" bind:value={scimEnabled} />
</div>
{#if scimEnabled}
{#each settings as setting}
<div class="form-row">
<Label size="L" tooltip={""}>{setting.title}</Label>
<div class="inputContainer">
<div class="input">
<Input value={setting.value} readonly={true} />
</div>
<div class="copy" on:click={() => copyToClipboard(setting.value)}>
<Icon size="S" name="Copy" />
</div>
</div>
</div>
{/each}
{/if}
</Layout>
<div>
<Button cta on:click={saveSCIM}>Save</Button>
</div>
<!-- TODO: DRY -->
<style>
.form-row {
display: grid;
grid-template-columns: 120px 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
input[type="file"] {
display: none;
}
.sso-link-icon {
padding-top: 4px;
margin-left: 3px;
}
.sso-link {
margin-top: 12px;
display: flex;
flex-direction: row;
align-items: center;
}
.enforce-sso-title {
margin-right: 10px;
}
.enforce-sso-heading-container {
display: flex;
flex-direction: row;
align-items: start;
}
.provider-title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-m);
}
.provider-title span {
flex: 1 1 auto;
}
.inputContainer {
display: flex;
flex-direction: row;
}
.input {
flex: 1;
}
.copy {
display: flex;
align-items: center;
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,15 @@
<script>
import { Icon } from "@budibase/bbui"
</script>
<div class="scim-banner">
<Icon name="Info" size="S" />
Users are synced from your AD
</div>
<style>
.scim-banner {
display: flex;
gap: var(--spacing-s);
}
</style>

View File

@ -1,12 +1,23 @@
<script>
import { Page } from "@budibase/bbui"
import { Page, notifications } from "@budibase/bbui"
import { onMount } from "svelte"
import { SideNav, SideNavItem, Content } from "components/portal/page"
import { isActive, goto } from "@roxi/routify"
import { menu } from "stores/portal"
import { menu, features } from "stores/portal"
$: wide = $isActive("./users/index") || $isActive("./groups/index")
$: pages = $menu.find(x => x.title === "Users")?.subPages || []
$: !pages.length && $goto("../")
onMount(async () => {
try {
await features.init()
} catch (error) {
notifications.error(
`Error fetching feature configs - ${error?.message || "unknown error"}`
)
}
})
</script>
<Page>

View File

@ -14,7 +14,7 @@
} from "@budibase/bbui"
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
import { createPaginationStore } from "helpers/pagination"
import { users, apps, groups, auth } from "stores/portal"
import { users, apps, groups, auth, features } from "stores/portal"
import { onMount, setContext } from "svelte"
import { roles } from "stores/backend"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -24,18 +24,23 @@
import AppNameTableRenderer from "../users/_components/AppNameTableRenderer.svelte"
import RemoveUserTableRenderer from "./_components/RemoveUserTableRenderer.svelte"
import AppRoleTableRenderer from "../users/_components/AppRoleTableRenderer.svelte"
import ScimBanner from "../_components/SCIMBanner.svelte"
export let groupId
const userSchema = {
$: userSchema = {
email: {
width: "1fr",
},
_id: {
displayName: "",
width: "auto",
borderLeft: true,
},
...(readonly
? {}
: {
_id: {
displayName: "",
width: "auto",
borderLeft: true,
},
}),
}
const appSchema = {
name: {
@ -70,7 +75,9 @@
let loaded = false
let editModal, deleteModal
$: readonly = !$auth.isAdmin
const scimEnabled = $features.isScimEnabled
$: readonly = !$auth.isAdmin || scimEnabled
$: page = $pageInfo.page
$: fetchUsers(page, searchTerm)
$: group = $groups.find(x => x._id === groupId)
@ -182,11 +189,15 @@
<Layout noPadding gap="S">
<div class="header">
<Heading size="S">Users</Heading>
<div bind:this={popoverAnchor}>
<Button disabled={readonly} on:click={popover.show()} cta
>Add user</Button
>
</div>
{#if !scimEnabled}
<div bind:this={popoverAnchor}>
<Button disabled={readonly} on:click={popover.show()} cta
>Add user</Button
>
</div>
{:else}
<ScimBanner />
{/if}
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
bind:searchTerm

View File

@ -13,7 +13,7 @@
Search,
notifications,
} from "@budibase/bbui"
import { groups, auth, licensing, admin } from "stores/portal"
import { groups, auth, licensing, admin, features } from "stores/portal"
import { onMount } from "svelte"
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
import { cloneDeep } from "lodash/fp"
@ -21,6 +21,7 @@
import UsersTableRenderer from "./_components/UsersTableRenderer.svelte"
import GroupNameTableRenderer from "./_components/GroupNameTableRenderer.svelte"
import { goto } from "@roxi/routify"
import ScimBanner from "../_components/SCIMBanner.svelte"
const DefaultGroup = {
name: "",
@ -106,10 +107,14 @@
<div class="controls">
<ButtonGroup>
{#if $licensing.groupsEnabled}
<!--Show the group create button-->
<Button disabled={readonly} cta on:click={showCreateGroupModal}>
Add group
</Button>
{#if !$features.isScimEnabled}
<!--Show the group create button-->
<Button disabled={readonly} cta on:click={showCreateGroupModal}>
Add group
</Button>
{:else}
<ScimBanner />
{/if}
{:else}
<Button
primary

View File

@ -19,7 +19,7 @@
Table,
} from "@budibase/bbui"
import { onMount, setContext } from "svelte"
import { users, auth, groups, apps, licensing } from "stores/portal"
import { users, auth, groups, apps, licensing, features } from "stores/portal"
import { roles } from "stores/backend"
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
@ -31,18 +31,23 @@
import GroupNameTableRenderer from "../groups/_components/GroupNameTableRenderer.svelte"
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
import ScimBanner from "../_components/SCIMBanner.svelte"
export let userId
const groupSchema = {
$: groupSchema = {
name: {
width: "1fr",
},
_id: {
displayName: "",
width: "auto",
borderLeft: true,
},
...(readonly
? {}
: {
_id: {
displayName: "",
width: "auto",
borderLeft: true,
},
}),
}
const appSchema = {
name: {
@ -81,9 +86,10 @@
let user
let loaded = false
const scimEnabled = $features.isScimEnabled
$: isSSO = !!user?.provider
$: readonly = !$auth.isAdmin
$: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : ""
$: readonly = !$auth.isAdmin || scimEnabled
$: privileged = user?.admin?.global || user?.builder?.global
$: nameLabel = getNameLabel(user)
$: initials = getInitials(nameLabel)
@ -260,7 +266,12 @@
{/if}
</div>
<Layout noPadding gap="S">
<Heading size="S">Details</Heading>
<div class="details-title">
<Heading size="S">Details</Heading>
{#if scimEnabled}
<ScimBanner />
{/if}
</div>
<div class="fields">
<div class="field">
<Label size="L">Email</Label>
@ -284,10 +295,11 @@
</div>
<!-- don't let a user remove the privileges that let them be here -->
{#if userId !== $auth.user._id}
<!-- Disabled if it's not admin, enabled for SCIM integration -->
<div class="field">
<Label size="L">Role</Label>
<Select
disabled={readonly}
disabled={!$auth.isAdmin}
value={globalRole}
options={Constants.BudibaseRoleOptions}
on:change={updateUserRole}
@ -404,4 +416,9 @@
width: 100%;
text-align: center;
}
.details-title {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -13,7 +13,14 @@
Divider,
} from "@budibase/bbui"
import AddUserModal from "./_components/AddUserModal.svelte"
import { users, groups, auth, licensing, organisation } from "stores/portal"
import {
users,
groups,
auth,
licensing,
organisation,
features,
} from "stores/portal"
import { onMount } from "svelte"
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
@ -28,6 +35,7 @@
import { Constants, Utils, fetchData } from "@budibase/frontend-core"
import { API } from "api"
import { OnboardingType } from "../../../../../constants"
import ScimBanner from "../_components/SCIMBanner.svelte"
const fetch = fetchData({
API,
@ -53,7 +61,7 @@
]
let userData = []
$: readonly = !$auth.isAdmin
$: readonly = !$auth.isAdmin || $features.isScimEnabled
$: debouncedUpdateFetch(searchEmail)
$: schema = {
email: {
@ -230,14 +238,18 @@
</Layout>
<Divider />
<div class="controls">
<ButtonGroup>
<Button disabled={readonly} on:click={createUserModal.show} cta>
Add users
</Button>
<Button disabled={readonly} on:click={importUsersModal.show} secondary>
Import
</Button>
</ButtonGroup>
{#if !readonly}
<ButtonGroup>
<Button disabled={readonly} on:click={createUserModal.show} cta>
Add users
</Button>
<Button disabled={readonly} on:click={importUsersModal.show} secondary>
Import
</Button>
</ButtonGroup>
{:else}
<ScimBanner />
{/if}
<div class="controls-right">
<Search bind:value={searchEmail} placeholder="Search" />
{#if selectedRows.length > 0}

View File

@ -0,0 +1,54 @@
import { writable } from "svelte/store"
import { API } from "api"
import { licensing } from "./licensing"
import { ConfigType } from "../../../../types/src/documents"
export const createFeatureStore = () => {
const internalStore = writable({
scim: {
isFeatureFlagEnabled: false,
isConfigFlagEnabled: false,
},
})
const store = writable({
isScimEnabled: false,
})
internalStore.subscribe(s => {
store.update(state => ({
...state,
isScimEnabled: s.scim.isFeatureFlagEnabled && s.scim.isConfigFlagEnabled,
}))
})
licensing.subscribe(v => {
internalStore.update(state => ({
...state,
scim: {
...state.scim,
isFeatureFlagEnabled: v.scimEnabled,
},
}))
})
const actions = {
init: async () => {
const scimConfig = await API.getConfig(ConfigType.SCIM)
internalStore.update(state => ({
...state,
scim: {
...state.scim,
isConfigFlagEnabled: scimConfig?.config?.enabled,
},
}))
},
}
return {
subscribe: store.subscribe,
...actions,
}
}
export const features = createFeatureStore()

View File

@ -14,3 +14,4 @@ export { overview } from "./overview"
export { environment } from "./environment"
export { menu } from "./menu"
export { auditLogs } from "./auditLogs"
export { features } from "./features"

View File

@ -18,6 +18,7 @@ export const createLicensingStore = () => {
groupsEnabled: false,
backupsEnabled: false,
brandingEnabled: false,
scimEnabled: false,
// the currently used quotas from the db
quotaUsage: undefined,
// derived quota metrics for percentages used
@ -66,6 +67,7 @@ export const createLicensingStore = () => {
const backupsEnabled = license.features.includes(
Constants.Features.BACKUPS
)
const scimEnabled = license.features.includes(Constants.Features.SCIM)
const environmentVariablesEnabled = license.features.includes(
Constants.Features.ENVIRONMENT_VARIABLES
)
@ -88,6 +90,7 @@ export const createLicensingStore = () => {
groupsEnabled,
backupsEnabled,
brandingEnabled,
scimEnabled,
environmentVariablesEnabled,
auditLogsEnabled,
enforceableSSO,

View File

@ -1,13 +0,0 @@
{
"baseUrl": "http://localhost:4100",
"projectId": "bmbemn",
"reporterOptions": {
"configFile": "reporterConfig.json"
},
"env": {
"PORT": "4100",
"WORKER_PORT": "4200",
"JWT_SECRET": "test",
"HOST_IP": ""
}
}

View File

@ -1,4 +0,0 @@
// @ts-ignore
import { run } from "../setup"
run("../server/src/index", "../worker/src/index")

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "2.4.42-alpha.5",
"version": "2.4.44-alpha.12",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "dist/index.js",
"bin": {
@ -29,9 +29,9 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "2.4.42-alpha.5",
"@budibase/string-templates": "2.4.42-alpha.5",
"@budibase/types": "2.4.42-alpha.5",
"@budibase/backend-core": "2.4.44-alpha.12",
"@budibase/string-templates": "2.4.44-alpha.12",
"@budibase/types": "2.4.44-alpha.12",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",

View File

@ -785,7 +785,7 @@
"barStyle": "picker",
"options": [
{
"label": "Extra amall",
"label": "Extra small",
"value": "XS"
},
{

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "2.4.42-alpha.5",
"version": "2.4.44-alpha.12",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,11 +19,11 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "2.4.42-alpha.5",
"@budibase/frontend-core": "2.4.42-alpha.5",
"@budibase/shared-core": "2.4.42-alpha.5",
"@budibase/string-templates": "2.4.42-alpha.5",
"@budibase/types": "2.4.42-alpha.5",
"@budibase/bbui": "2.4.44-alpha.12",
"@budibase/frontend-core": "2.4.44-alpha.12",
"@budibase/shared-core": "2.4.44-alpha.12",
"@budibase/string-templates": "2.4.44-alpha.12",
"@budibase/types": "2.4.44-alpha.12",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View File

@ -1,13 +1,13 @@
{
"name": "@budibase/frontend-core",
"version": "2.4.42-alpha.5",
"version": "2.4.44-alpha.12",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "2.4.42-alpha.5",
"@budibase/shared-core": "2.4.42-alpha.5",
"@budibase/bbui": "2.4.44-alpha.12",
"@budibase/shared-core": "2.4.44-alpha.12",
"dayjs": "^1.11.7",
"lodash": "^4.17.21",
"socket.io-client": "^4.6.1",

View File

@ -69,6 +69,7 @@ export const Features = {
AUDIT_LOGS: "auditLogs",
ENFORCEABLE_SSO: "enforceableSSO",
BRANDING: "branding",
SCIM: "scim",
}
// Role IDs

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/sdk",
"version": "2.4.42-alpha.5",
"version": "2.4.44-alpha.12",
"description": "Budibase Public API SDK",
"author": "Budibase",
"license": "MPL-2.0",

View File

@ -44,6 +44,7 @@ const config: Config.InitialOptions = {
// The use of coverage with couchdb view functions breaks tests
"!src/db/views/staticViews.*",
"!src/**/*.spec.{js,ts}",
"!src/tests/**/*.{js,ts}",
],
coverageReporters: ["lcov", "json", "clover"],
}

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "2.4.42-alpha.5",
"version": "2.4.44-alpha.12",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -14,7 +14,7 @@
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
"test": "NODE_OPTIONS=\"--max-old-space-size=4096\" bash scripts/test.sh",
"test": "bash scripts/test.sh",
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
"test:watch": "jest --watch",
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
@ -26,6 +26,7 @@
"dev:stack:down": "node scripts/dev/manage.js down",
"dev:stack:nuke": "node scripts/dev/manage.js nuke",
"dev:builder": "yarn run dev:stack:up && nodemon",
"dev:built": "yarn run dev:stack:up && yarn run run:docker",
"specs": "ts-node specs/generate.ts && openapi-typescript specs/openapi.yaml --output src/definitions/openapi.ts",
"initialise": "node scripts/initialise.js",
"env:multi:enable": "node scripts/multiTenancy.js enable",
@ -44,12 +45,12 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "2.4.42-alpha.5",
"@budibase/client": "2.4.42-alpha.5",
"@budibase/pro": "2.4.42-alpha.5",
"@budibase/shared-core": "2.4.42-alpha.5",
"@budibase/string-templates": "2.4.42-alpha.5",
"@budibase/types": "2.4.42-alpha.5",
"@budibase/backend-core": "2.4.44-alpha.12",
"@budibase/client": "2.4.44-alpha.12",
"@budibase/pro": "2.4.44-alpha.12",
"@budibase/shared-core": "2.4.44-alpha.12",
"@budibase/string-templates": "2.4.44-alpha.12",
"@budibase/types": "2.4.44-alpha.12",
"@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0",
@ -86,7 +87,6 @@
"koa-body": "4.2.0",
"koa-compress": "4.0.1",
"koa-connect": "2.1.0",
"koa-pino-logger": "3.0.0",
"koa-send": "5.0.0",
"koa-session": "5.12.0",
"koa-static": "5.0.0",
@ -100,7 +100,6 @@
"node-fetch": "2.6.7",
"open": "8.4.0",
"pg": "8.5.1",
"pino-pretty": "4.0.0",
"posthog-node": "1.3.0",
"pouchdb": "7.3.0",
"pouchdb-adapter-memory": "7.2.2",

View File

@ -45,6 +45,8 @@ async function init() {
BB_ADMIN_USER_PASSWORD: "",
PLUGINS_DIR: "",
TENANT_FEATURE_FLAGS: "*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR",
HTTP_MIGRATIONS: "0",
HTTP_LOGGING: "0",
}
let envFile = ""
Object.keys(envFileJson).forEach(key => {

View File

@ -1,35 +0,0 @@
/******************************************************
* This script just makes it easy to re-create *
* a cypress like environment for testing the backend *
******************************************************/
import path from "path"
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
const SERVER_PORT = "4100"
const WORKER_PORT = "4200"
// @ts-ignore
process.env.NODE_ENV = "cypress"
process.env.ENABLE_ANALYTICS = "0"
process.env.JWT_SECRET = "budibase"
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
process.env.SELF_HOSTED = "1"
process.env.WORKER_URL = `http://localhost:${WORKER_PORT}/`
process.env.MINIO_URL = `http://localhost:4004`
process.env.MINIO_ACCESS_KEY = "budibase"
process.env.MINIO_SECRET_KEY = "budibase"
process.env.COUCH_DB_USER = "budibase"
process.env.COUCH_DB_PASSWORD = "budibase"
process.env.INTERNAL_API_KEY = "budibase"
process.env.ALLOW_DEV_AUTOMATIONS = "1"
// don't make this a variable or top level require
// it will cause environment module to be loaded prematurely
// override the port with the worker port temporarily
process.env.PORT = WORKER_PORT
const worker = require("../../worker/src/index")
// override the port with the server port
process.env.PORT = SERVER_PORT
const server = require("../src/app")

View File

@ -1,12 +1,14 @@
#!/bin/bash
set -e
if [[ -n $CI ]]
then
# --runInBand performs better in ci where resources are limited
export NODE_OPTIONS="--max-old-space-size=4096"
echo "jest --coverage --runInBand --forceExit"
jest --coverage --runInBand --forceExit
else
# --maxWorkers performs better in development
echo "jest --coverage --maxWorkers=2"
jest --coverage --maxWorkers=2
fi
echo "jest --coverage --maxWorkers=2 --forceExit"
jest --coverage --maxWorkers=2 --forceExit
fi

View File

@ -2,9 +2,9 @@ import { DocumentType } from "../../db/utils"
import { Plugin } from "@budibase/types"
import { db as dbCore, context, tenancy } from "@budibase/backend-core"
import { getComponentLibraryManifest } from "../../utilities/fileSystem"
import { BBContext } from "@budibase/types"
import { UserCtx } from "@budibase/types"
export async function fetchAppComponentDefinitions(ctx: BBContext) {
export async function fetchAppComponentDefinitions(ctx: UserCtx) {
try {
const db = context.getAppDB()
const app = await db.get(DocumentType.APP_METADATA)

View File

@ -0,0 +1,32 @@
import { Ctx } from "@budibase/types"
import { logging } from "@budibase/backend-core"
interface LogRequest {
message: string
data?: any
}
interface ErrorRequest {
message: string
}
export async function log(ctx: Ctx<LogRequest>) {
const body = ctx.request.body
console.trace(body.message, body.data)
console.debug(body.message, body.data)
console.info(body.message, body.data)
console.warn(body.message, body.data)
console.error(body.message, body.data)
ctx.status = 204
}
export async function alert(ctx: Ctx<ErrorRequest>) {
const body = ctx.request.body
logging.logAlert(body.message, new Error(body.message))
ctx.status = 204
}
export async function error(ctx: Ctx<ErrorRequest>) {
const body = ctx.request.body
throw new Error(body.message)
}

View File

@ -0,0 +1,97 @@
import { FieldType } from "@budibase/types"
import { AutoFieldSubTypes } from "../../../../constants"
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
import { importToRows } from "../utils"
describe("utils", () => {
const config = new TestConfiguration()
beforeEach(async () => {
await config.init()
})
afterAll(config.end)
describe("importToRows", () => {
it("consecutive row have consecutive auto ids", async () => {
await config.doInContext(config.appId, async () => {
const table = await config.createTable({
name: "table",
type: "table",
schema: {
autoId: {
name: "autoId",
type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID,
autocolumn: true,
constraints: {
type: FieldType.NUMBER,
presence: true,
},
},
name: {
name: "name",
type: FieldType.STRING,
constraints: {
type: FieldType.STRING,
presence: true,
},
},
},
})
const data = [{ name: "Alice" }, { name: "Bob" }, { name: "Claire" }]
const result = importToRows(data, table, config.user)
expect(result).toEqual([
expect.objectContaining({
autoId: 1,
name: "Alice",
}),
expect.objectContaining({
autoId: 2,
name: "Bob",
}),
expect.objectContaining({
autoId: 3,
name: "Claire",
}),
])
})
})
it("can import data without a specific user performing the action", async () => {
await config.doInContext(config.appId, async () => {
const table = await config.createTable({
name: "table",
type: "table",
schema: {
autoId: {
name: "autoId",
type: FieldType.NUMBER,
subtype: AutoFieldSubTypes.AUTO_ID,
autocolumn: true,
constraints: {
type: FieldType.NUMBER,
presence: true,
},
},
name: {
name: "name",
type: FieldType.STRING,
constraints: {
type: FieldType.STRING,
presence: true,
},
},
},
})
const data = [{ name: "Alice" }, { name: "Bob" }, { name: "Claire" }]
const result = importToRows(data, table)
expect(result).toHaveLength(3)
})
})
})
})

View File

@ -20,7 +20,13 @@ import viewTemplate from "../view/viewBuilder"
import { cloneDeep } from "lodash/fp"
import { quotas } from "@budibase/pro"
import { events, context } from "@budibase/backend-core"
import { Database, Datasource, SourceName, Table } from "@budibase/types"
import {
ContextUser,
Database,
Datasource,
SourceName,
Table,
} from "@budibase/types"
export async function clearColumns(table: any, columnNames: any) {
const db: Database = context.getAppDB()
@ -99,33 +105,36 @@ export function makeSureTableUpToDate(table: any, tableToSave: any) {
return tableToSave
}
export function importToRows(data: any, table: any, user: any = {}) {
export function importToRows(
data: any[],
table: Table,
user: ContextUser | null = null
) {
let finalData: any = []
for (let i = 0; i < data.length; i++) {
let row = data[i]
row._id = generateRowID(table._id)
row._id = generateRowID(table._id!)
row.tableId = table._id
const processed: any = inputProcessing(user, table, row, {
const processed = inputProcessing(user, table, row, {
noAutoRelationships: true,
})
row = processed.row
table = processed.table
let fieldName: any
let schema: any
for ([fieldName, schema] of Object.entries(table.schema)) {
for (const [fieldName, schema] of Object.entries(table.schema)) {
// check whether the options need to be updated for inclusion as part of the data import
if (
(schema.type === FieldTypes.OPTIONS ||
schema.type === FieldTypes.ARRAY) &&
row[fieldName] &&
(!schema.constraints.inclusion ||
schema.constraints.inclusion.indexOf(row[fieldName]) === -1)
(!schema.constraints!.inclusion ||
schema.constraints!.inclusion.indexOf(row[fieldName]) === -1)
) {
schema.constraints.inclusion = [
...schema.constraints.inclusion,
schema.constraints!.inclusion = [
...schema.constraints!.inclusion!,
row[fieldName],
]
schema.constraints.inclusion.sort()
schema.constraints!.inclusion.sort()
}
}

View File

@ -25,6 +25,7 @@ import devRoutes from "./dev"
import cloudRoutes from "./cloud"
import migrationRoutes from "./migrations"
import pluginRoutes from "./plugin"
import opsRoutes from "./ops"
import Router from "@koa/router"
import { api as pro } from "@budibase/pro"
@ -63,6 +64,7 @@ export const mainRoutes: Router[] = [
rowRoutes,
migrationRoutes,
pluginRoutes,
opsRoutes,
scheduleRoutes,
environmentVariableRoutes,
// these need to be handled last as they still use /api/:tableId

View File

@ -0,0 +1,30 @@
import Router from "@koa/router"
import * as controller from "../controllers/ops"
import { middleware } from "@budibase/backend-core"
import Joi from "joi"
export function logsValidator() {
return middleware.joiValidator.body(
Joi.object({
message: Joi.string().required(),
data: Joi.object(),
})
)
}
export function errorValidator() {
return middleware.joiValidator.body(
Joi.object({
message: Joi.string().required(),
})
)
}
const router: Router = new Router()
router
.post("/api/ops/log", logsValidator(), controller.log)
.post("/api/ops/error", errorValidator(), controller.error)
.post("/api/ops/alert", errorValidator(), controller.alert)
export default router

View File

@ -19,11 +19,14 @@ describe("/automations", () => {
afterAll(setup.afterAll)
// For some reason this cannot be a beforeAll or the test "tests the automation successfully" fail
beforeEach(async () => {
beforeAll(async () => {
await config.init()
})
beforeEach(() => {
events.automation.deleted.mockClear()
})
describe("get definitions", () => {
it("returns a list of definitions for actions", async () => {
const res = await request

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