Merge branch 'develop' of github.com:Budibase/budibase into views-v2-frontend

This commit is contained in:
Andrew Kingston 2023-08-21 11:57:28 +01:00
commit 6d46fb2963
66 changed files with 1286 additions and 320 deletions

3
.vscode/launch.json vendored
View File

@ -1,3 +1,4 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
@ -8,7 +9,6 @@
"name": "Budibase Server", "name": "Budibase Server",
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"runtimeVersion": "14.20.1",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["${workspaceFolder}/packages/server/src/index.ts"], "args": ["${workspaceFolder}/packages/server/src/index.ts"],
"cwd": "${workspaceFolder}/packages/server" "cwd": "${workspaceFolder}/packages/server"
@ -17,7 +17,6 @@
"name": "Budibase Worker", "name": "Budibase Worker",
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"runtimeVersion": "14.20.1",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["${workspaceFolder}/packages/worker/src/index.ts"], "args": ["${workspaceFolder}/packages/worker/src/index.ts"],
"cwd": "${workspaceFolder}/packages/worker" "cwd": "${workspaceFolder}/packages/worker"

View File

@ -137,7 +137,6 @@ services:
path: /health path: /health
port: 10000 port: 10000
scheme: HTTP scheme: HTTP
enabled: true
periodSeconds: 3 periodSeconds: 3
failureThreshold: 1 failureThreshold: 1
livenessProbe: livenessProbe:
@ -170,7 +169,6 @@ services:
path: /health path: /health
port: 4002 port: 4002
scheme: HTTP scheme: HTTP
enabled: true
periodSeconds: 3 periodSeconds: 3
failureThreshold: 1 failureThreshold: 1
livenessProbe: livenessProbe:
@ -204,7 +202,6 @@ services:
path: /health path: /health
port: 4003 port: 4003
scheme: HTTP scheme: HTTP
enabled: true
periodSeconds: 3 periodSeconds: 3
failureThreshold: 1 failureThreshold: 1
livenessProbe: livenessProbe:
@ -411,14 +408,12 @@ couchdb:
## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes ## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
# FOR COUCHDB # FOR COUCHDB
livenessProbe: livenessProbe:
enabled: true
failureThreshold: 3 failureThreshold: 3
initialDelaySeconds: 0 initialDelaySeconds: 0
periodSeconds: 10 periodSeconds: 10
successThreshold: 1 successThreshold: 1
timeoutSeconds: 1 timeoutSeconds: 1
readinessProbe: readinessProbe:
enabled: true
failureThreshold: 3 failureThreshold: 3
initialDelaySeconds: 0 initialDelaySeconds: 0
periodSeconds: 10 periodSeconds: 10

View File

@ -5,11 +5,11 @@ ENV COUCHDB_PASSWORD admin
EXPOSE 5984 EXPOSE 5984
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \ RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - && \ wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add - && \
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \ apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \ apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \
apt-add-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ && \ apt-add-repository 'deb https://packages.adoptium.net/artifactory/deb bullseye main' && \
apt-get update && apt-get install -y --no-install-recommends adoptopenjdk-8-hotspot && \ apt-get update && apt-get install -y --no-install-recommends temurin-8-jdk && \
rm -rf /var/lib/apt/lists/ rm -rf /var/lib/apt/lists/
# setup clouseau # setup clouseau

View File

@ -27,6 +27,7 @@ services:
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL} BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD} BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
PLUGINS_DIR: ${PLUGINS_DIR} PLUGINS_DIR: ${PLUGINS_DIR}
OFFLINE_MODE: ${OFFLINE_MODE}
depends_on: depends_on:
- worker-service - worker-service
- redis-service - redis-service
@ -54,6 +55,7 @@ services:
INTERNAL_API_KEY: ${INTERNAL_API_KEY} INTERNAL_API_KEY: ${INTERNAL_API_KEY}
REDIS_URL: redis-service:6379 REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD} REDIS_PASSWORD: ${REDIS_PASSWORD}
OFFLINE_MODE: ${OFFLINE_MODE}
depends_on: depends_on:
- redis-service - redis-service
- minio-service - minio-service

View File

@ -1,5 +1,5 @@
{ {
"version": "2.9.24-alpha.4", "version": "2.9.30-alpha.6",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,7 +1,6 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { getCouchInfo } from "./couch" import { getCouchInfo } from "./couch"
import { SearchFilters, Row } from "@budibase/types" import { SearchFilters, Row, EmptyFilterOption } from "@budibase/types"
import { createUserIndex } from "./searchIndexes/searchIndexes"
const QUERY_START_REGEX = /\d[0-9]*:/g const QUERY_START_REGEX = /\d[0-9]*:/g
@ -65,6 +64,7 @@ export class QueryBuilder<T> {
this.#index = index this.#index = index
this.#query = { this.#query = {
allOr: false, allOr: false,
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
string: {}, string: {},
fuzzy: {}, fuzzy: {},
range: {}, range: {},
@ -218,6 +218,10 @@ export class QueryBuilder<T> {
this.#query.allOr = true this.#query.allOr = true
} }
setOnEmptyFilter(value: EmptyFilterOption) {
this.#query.onEmptyFilter = value
}
handleSpaces(input: string) { handleSpaces(input: string) {
if (this.#noEscaping) { if (this.#noEscaping) {
return input return input
@ -289,8 +293,9 @@ export class QueryBuilder<T> {
const builder = this const builder = this
let allOr = this.#query && this.#query.allOr let allOr = this.#query && this.#query.allOr
let query = allOr ? "" : "*:*" let query = allOr ? "" : "*:*"
let allFiltersEmpty = true
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true } const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
let tableId let tableId: string = ""
if (this.#query.equal!.tableId) { if (this.#query.equal!.tableId) {
tableId = this.#query.equal!.tableId tableId = this.#query.equal!.tableId
delete this.#query.equal!.tableId delete this.#query.equal!.tableId
@ -305,7 +310,7 @@ export class QueryBuilder<T> {
} }
const contains = (key: string, value: any, mode = "AND") => { const contains = (key: string, value: any, mode = "AND") => {
if (Array.isArray(value) && value.length === 0) { if (!value || (Array.isArray(value) && value.length === 0)) {
return null return null
} }
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
@ -384,6 +389,12 @@ export class QueryBuilder<T> {
built += ` ${mode} ` built += ` ${mode} `
} }
built += expression built += expression
if (
(typeof value !== "string" && value != null) ||
(typeof value === "string" && value !== tableId && value !== "")
) {
allFiltersEmpty = false
}
} }
if (opts?.returnBuilt) { if (opts?.returnBuilt) {
return built return built
@ -463,6 +474,13 @@ export class QueryBuilder<T> {
allOr = false allOr = false
build({ tableId }, equal) build({ tableId }, equal)
} }
if (allFiltersEmpty) {
if (this.#query.onEmptyFilter === EmptyFilterOption.RETURN_NONE) {
return ""
} else if (this.#query?.allOr) {
return query.replace("()", "(*:*)")
}
}
return query return query
} }

View File

@ -1,6 +1,6 @@
import { newid } from "../../docIds/newid" import { newid } from "../../docIds/newid"
import { getDB } from "../db" import { getDB } from "../db"
import { Database } from "@budibase/types" import { Database, EmptyFilterOption } from "@budibase/types"
import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene" import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
const INDEX_NAME = "main" const INDEX_NAME = "main"
@ -156,6 +156,76 @@ describe("lucene", () => {
expect(resp.rows.length).toBe(2) expect(resp.rows.length).toBe(2)
}) })
describe("empty filters behaviour", () => {
it("should return all rows by default", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.addEqual("property", "")
builder.addEqual("number", null)
builder.addString("property", "")
builder.addFuzzy("property", "")
builder.addNotEqual("number", undefined)
builder.addOneOf("number", null)
builder.addContains("array", undefined)
builder.addNotContains("array", null)
builder.addContainsAny("array", null)
const resp = await builder.run()
expect(resp.rows.length).toBe(3)
})
it("should return all rows when onEmptyFilter is ALL", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_ALL)
builder.setAllOr()
builder.addEqual("property", "")
builder.addEqual("number", null)
builder.addString("property", "")
builder.addFuzzy("property", "")
builder.addNotEqual("number", undefined)
builder.addOneOf("number", null)
builder.addContains("array", undefined)
builder.addNotContains("array", null)
builder.addContainsAny("array", null)
const resp = await builder.run()
expect(resp.rows.length).toBe(3)
})
it("should return no rows when onEmptyFilter is NONE", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE)
builder.addEqual("property", "")
builder.addEqual("number", null)
builder.addString("property", "")
builder.addFuzzy("property", "")
builder.addNotEqual("number", undefined)
builder.addOneOf("number", null)
builder.addContains("array", undefined)
builder.addNotContains("array", null)
builder.addContainsAny("array", null)
const resp = await builder.run()
expect(resp.rows.length).toBe(0)
})
it("should return all matching rows when onEmptyFilter is NONE, but a filter value is provided", async () => {
const builder = new QueryBuilder(dbName, INDEX_NAME)
builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE)
builder.addEqual("property", "")
builder.addEqual("number", 1)
builder.addString("property", "")
builder.addFuzzy("property", "")
builder.addNotEqual("number", undefined)
builder.addOneOf("number", null)
builder.addContains("array", undefined)
builder.addNotContains("array", null)
builder.addContainsAny("array", null)
const resp = await builder.run()
expect(resp.rows.length).toBe(1)
})
})
describe("skip", () => { describe("skip", () => {
const skipDbName = `db-${newid()}` const skipDbName = `db-${newid()}`
let docs: { let docs: {

View File

@ -1,5 +1,6 @@
import env from "../environment" import env from "../environment"
import * as context from "../context" import * as context from "../context"
export * from "./installation"
/** /**
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant. * Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.

View File

@ -0,0 +1,17 @@
export function processFeatureEnvVar<T>(
fullList: string[],
featureList?: string
) {
let list
if (!featureList) {
list = fullList
} else {
list = featureList.split(",")
}
for (let feature of list) {
if (!fullList.includes(feature)) {
throw new Error(`Feature: ${feature} is not an allowed option`)
}
}
return list as unknown as T[]
}

View File

@ -6,7 +6,8 @@ export * as roles from "./security/roles"
export * as permissions from "./security/permissions" export * as permissions from "./security/permissions"
export * as accounts from "./accounts" export * as accounts from "./accounts"
export * as installation from "./installation" export * as installation from "./installation"
export * as featureFlags from "./featureFlags" export * as featureFlags from "./features"
export * as features from "./features/installation"
export * as sessions from "./security/sessions" export * as sessions from "./security/sessions"
export * as platform from "./platform" export * as platform from "./platform"
export * as auth from "./auth" export * as auth from "./auth"

View File

@ -1,30 +1,30 @@
import env from "../environment" import env from "../environment"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import * as accounts from "../accounts" import * as accounts from "../accounts"
import * as accountSdk from "../accounts"
import * as cache from "../cache" import * as cache from "../cache"
import { getIdentity, getTenantId, getGlobalDB } from "../context" import { getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors" import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform" import * as platform from "../platform"
import * as sessions from "../security/sessions" import * as sessions from "../security/sessions"
import * as usersCore from "./users" import * as usersCore from "./users"
import { import {
Account,
AllDocsResponse, AllDocsResponse,
BulkUserCreated, BulkUserCreated,
BulkUserDeleted, BulkUserDeleted,
isSSOAccount,
isSSOUser,
RowResponse, RowResponse,
SaveUserOpts, SaveUserOpts,
User, User,
Account,
isSSOUser,
isSSOAccount,
UserStatus, UserStatus,
} from "@budibase/types" } from "@budibase/types"
import * as accountSdk from "../accounts"
import { import {
validateUniqueUser,
getAccountHolderFromUserIds, getAccountHolderFromUserIds,
isAdmin, isAdmin,
validateUniqueUser,
} from "./utils" } from "./utils"
import { searchExistingEmails } from "./lookup" import { searchExistingEmails } from "./lookup"
import { hash } from "../utils" import { hash } from "../utils"
@ -179,6 +179,14 @@ export class UserDB {
return user return user
} }
static async bulkGet(userIds: string[]) {
return await usersCore.bulkGetGlobalUsersById(userIds)
}
static async bulkUpdate(users: User[]) {
return await usersCore.bulkUpdateGlobalUsers(users)
}
static async save(user: User, opts: SaveUserOpts = {}): Promise<User> { static async save(user: User, opts: SaveUserOpts = {}): Promise<User> {
// default booleans to true // default booleans to true
if (opts.hashPassword == null) { if (opts.hashPassword == null) {

View File

@ -86,6 +86,10 @@ export const useAuditLogs = () => {
return useFeature(Feature.AUDIT_LOGS) return useFeature(Feature.AUDIT_LOGS)
} }
export const usePublicApiUserRoles = () => {
return useFeature(Feature.USER_ROLE_PUBLIC_API)
}
export const useScimIntegration = () => { export const useScimIntegration = () => {
return useFeature(Feature.SCIM) return useFeature(Feature.SCIM)
} }

View File

@ -35,22 +35,28 @@
{ value: "and", label: "Match all filters" }, { value: "and", label: "Match all filters" },
{ value: "or", label: "Match any filter" }, { value: "or", label: "Match any filter" },
] ]
const onEmptyOptions = [
{ value: "all", label: "Return all table rows" },
{ value: "none", label: "Return no rows" },
]
let rawFilters let rawFilters
let matchAny = false let matchAny = false
let onEmptyFilter = "all"
$: parseFilters(filters) $: parseFilters(filters)
$: dispatch("change", enrichFilters(rawFilters, matchAny)) $: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true }) $: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || [] $: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"] $: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
// Remove field key prefixes and determine whether to use the "match all" // Remove field key prefixes and determine which behaviours to use
// or "match any" behaviour
const parseFilters = filters => { const parseFilters = filters => {
matchAny = filters?.find(filter => filter.operator === "allOr") != null matchAny = filters?.find(filter => filter.operator === "allOr") != null
onEmptyFilter =
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
rawFilters = (filters || []) rawFilters = (filters || [])
.filter(filter => filter.operator !== "allOr") .filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
.map(filter => { .map(filter => {
const { field } = filter const { field } = filter
let newFilter = { ...filter } let newFilter = { ...filter }
@ -74,8 +80,8 @@
}) })
// Add field key prefixes and a special metadata filter object to indicate // Add field key prefixes and a special metadata filter object to indicate
// whether to use the "match all" or "match any" behaviour // how to handle filter behaviour
const enrichFilters = (rawFilters, matchAny) => { const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
let count = 1 let count = 1
return rawFilters return rawFilters
.filter(filter => filter.field) .filter(filter => filter.field)
@ -84,6 +90,7 @@
field: `${count++}:${filter.field}`, field: `${count++}:${filter.field}`,
})) }))
.concat(matchAny ? [{ operator: "allOr" }] : []) .concat(matchAny ? [{ operator: "allOr" }] : [])
.concat([{ onEmptyFilter }])
} }
const addFilter = () => { const addFilter = () => {
@ -195,6 +202,17 @@
on:change={e => (matchAny = e.detail === "or")} on:change={e => (matchAny = e.detail === "or")}
placeholder={null} placeholder={null}
/> />
{#if datasource?.type === "table"}
<Select
label="When filter empty"
value={onEmptyFilter}
options={onEmptyOptions}
getOptionLabel={opt => opt.label}
getOptionValue={opt => opt.value}
on:change={e => (onEmptyFilter = e.detail)}
placeholder={null}
/>
{/if}
</div> </div>
<div> <div>
<div class="filter-label"> <div class="filter-label">

View File

@ -4,11 +4,13 @@
$: isError = !value || value.toLowerCase() === "error" $: isError = !value || value.toLowerCase() === "error"
$: isStoppedError = value?.toLowerCase() === "stopped_error" $: isStoppedError = value?.toLowerCase() === "stopped_error"
$: isStopped = value?.toLowerCase() === "stopped" || isStoppedError $: isStopped = value?.toLowerCase() === "stopped"
$: info = getInfo(isError, isStopped) $: info = getInfo(isError, isStopped, isStoppedError)
const getInfo = (error, stopped) => { function getInfo(error, stopped, stoppedError) {
if (error) { if (stoppedError) {
return { color: "red", message: "Stopped - Error" }
} else if (error) {
return { color: "red", message: "Error" } return { color: "red", message: "Error" }
} else if (stopped) { } else if (stopped) {
return { color: "yellow", message: "Stopped" } return { color: "yellow", message: "Stopped" }

View File

@ -22,7 +22,8 @@
const ERROR = "error", const ERROR = "error",
SUCCESS = "success", SUCCESS = "success",
STOPPED = "stopped" STOPPED = "stopped",
STOPPED_ERROR = "stopped_error"
const sidePanel = getContext("side-panel") const sidePanel = getContext("side-panel")
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
@ -52,6 +53,7 @@
{ value: SUCCESS, label: "Success" }, { value: SUCCESS, label: "Success" },
{ value: ERROR, label: "Error" }, { value: ERROR, label: "Error" },
{ value: STOPPED, label: "Stopped" }, { value: STOPPED, label: "Stopped" },
{ value: STOPPED_ERROR, label: "Stopped - Error" },
] ]
const runHistorySchema = { const runHistorySchema = {

View File

@ -8,22 +8,16 @@
"esModuleInterop": true, "esModuleInterop": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"incremental": true, "incremental": true,
"types": [ "node", "jest" ], "types": ["node", "jest"],
"outDir": "dist", "outDir": "dist",
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "paths": {
"@budibase/types": ["../types/src"], "@budibase/types": ["../types/src"],
"@budibase/backend-core": ["../backend-core/src"], "@budibase/backend-core": ["../backend-core/src"],
"@budibase/backend-core/*": ["../backend-core/*"] "@budibase/backend-core/*": ["../backend-core/*"],
"@budibase/shared-core": ["../shared-core/src"]
} }
}, },
"include": [ "include": ["src/**/*"],
"src/**/*" "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.spec.js"]
],
"exclude": [
"node_modules",
"dist",
"**/*.spec.ts",
"**/*.spec.js"
]
} }

View File

@ -2408,6 +2408,13 @@
"label": "Disabled", "label": "Disabled",
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
},
{
"type": "text",
"label": "Initial form step",
"key": "initialFormStep",
"defaultValue": 1
} }
], ],
"context": [ "context": [

View File

@ -9,6 +9,7 @@
export let size export let size
export let disabled = false export let disabled = false
export let actionType = "Create" export let actionType = "Create"
export let initialFormStep = 1
// Not exposed as a builder setting. Used internally to disable validation // Not exposed as a builder setting. Used internally to disable validation
// for fields rendered in things like search blocks. // for fields rendered in things like search blocks.
@ -21,10 +22,18 @@
const context = getContext("context") const context = getContext("context")
const { API, fetchDatasourceSchema } = getContext("sdk") const { API, fetchDatasourceSchema } = getContext("sdk")
const getInitialFormStep = () => {
const parsedFormStep = parseInt(initialFormStep)
if (isNaN(parsedFormStep)) {
return 1
}
return parsedFormStep
}
let loaded = false let loaded = false
let schema let schema
let table let table
let currentStep = writable(1) let currentStep = writable(getInitialFormStep())
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: schemaKey = generateSchemaKey(schema) $: schemaKey = generateSchemaKey(schema)

@ -1 +1 @@
Subproject commit 9b9c8cc08f271bfc5dd401860f344f6eb336ab35 Subproject commit 06a28b18a409cc12e9e8a5b69a094adcc6babd5a

View File

@ -12,6 +12,7 @@ const baseConfig: Config.InitialProjectOptions = {
}, },
moduleNameMapper: { moduleNameMapper: {
"@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1", "@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1",
"@budibase/shared-core/(.*)": "<rootDir>/../shared-core/$1",
"@budibase/backend-core": "<rootDir>/../backend-core/src", "@budibase/backend-core": "<rootDir>/../backend-core/src",
"@budibase/shared-core": "<rootDir>/../shared-core/src", "@budibase/shared-core": "<rootDir>/../shared-core/src",
"@budibase/types": "<rootDir>/../types/src", "@budibase/types": "<rootDir>/../types/src",

View File

@ -179,5 +179,20 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"oracledb": "5.3.0" "oracledb": "5.3.0"
},
"nx": {
"targets": {
"dev:builder": {
"dependsOn": [
{
"comment": "Required for pro usage when submodule not loaded",
"projects": [
"@budibase/backend-core"
],
"target": "build"
}
]
}
}
} }
} }

View File

@ -1521,7 +1521,7 @@
"type": "boolean" "type": "boolean"
}, },
"builder": { "builder": {
"description": "Describes if the user is a builder user or not.", "description": "Describes if the user is a builder user or not. This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"properties": { "properties": {
"global": { "global": {
@ -1531,7 +1531,7 @@
} }
}, },
"admin": { "admin": {
"description": "Describes if the user is an admin user or not.", "description": "Describes if the user is an admin user or not. This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"properties": { "properties": {
"global": { "global": {
@ -1541,7 +1541,7 @@
} }
}, },
"roles": { "roles": {
"description": "Contains the roles of the user per app (assuming they are not a builder user).", "description": "Contains the roles of the user per app (assuming they are not a builder user). This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string", "type": "string",
@ -1588,7 +1588,7 @@
"type": "boolean" "type": "boolean"
}, },
"builder": { "builder": {
"description": "Describes if the user is a builder user or not.", "description": "Describes if the user is a builder user or not. This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"properties": { "properties": {
"global": { "global": {
@ -1598,7 +1598,7 @@
} }
}, },
"admin": { "admin": {
"description": "Describes if the user is an admin user or not.", "description": "Describes if the user is an admin user or not. This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"properties": { "properties": {
"global": { "global": {
@ -1608,7 +1608,7 @@
} }
}, },
"roles": { "roles": {
"description": "Contains the roles of the user per app (assuming they are not a builder user).", "description": "Contains the roles of the user per app (assuming they are not a builder user). This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string", "type": "string",
@ -1667,7 +1667,7 @@
"type": "boolean" "type": "boolean"
}, },
"builder": { "builder": {
"description": "Describes if the user is a builder user or not.", "description": "Describes if the user is a builder user or not. This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"properties": { "properties": {
"global": { "global": {
@ -1677,7 +1677,7 @@
} }
}, },
"admin": { "admin": {
"description": "Describes if the user is an admin user or not.", "description": "Describes if the user is an admin user or not. This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"properties": { "properties": {
"global": { "global": {
@ -1687,7 +1687,7 @@
} }
}, },
"roles": { "roles": {
"description": "Contains the roles of the user per app (assuming they are not a builder user).", "description": "Contains the roles of the user per app (assuming they are not a builder user). This field can only be set on a business or enterprise license.",
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string", "type": "string",
@ -1833,6 +1833,137 @@
"required": [ "required": [
"name" "name"
] ]
},
"rolesAssign": {
"type": "object",
"properties": {
"appBuilder": {
"type": "object",
"properties": {
"appId": {
"description": "The app that the users should have app builder privileges granted for.",
"type": "string"
}
},
"description": "Allow setting users to builders per app.",
"required": [
"appId"
]
},
"builder": {
"type": "boolean",
"description": "Add/remove global builder permissions from the list of users."
},
"admin": {
"type": "boolean",
"description": "Add/remove global admin permissions from the list of users."
},
"role": {
"type": "object",
"properties": {
"roleId": {
"description": "The role ID, such as BASIC, ADMIN or a custom role ID.",
"type": "string"
},
"appId": {
"description": "The app that the role relates to.",
"type": "string"
}
},
"description": "Add/remove a per-app role, such as BASIC, ADMIN etc.",
"required": [
"roleId",
"appId"
]
},
"userIds": {
"description": "The user IDs to be updated to add/remove the specified roles.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"userIds"
]
},
"rolesUnAssign": {
"type": "object",
"properties": {
"appBuilder": {
"type": "object",
"properties": {
"appId": {
"description": "The app that the users should have app builder privileges granted for.",
"type": "string"
}
},
"description": "Allow setting users to builders per app.",
"required": [
"appId"
]
},
"builder": {
"type": "boolean",
"description": "Add/remove global builder permissions from the list of users."
},
"admin": {
"type": "boolean",
"description": "Add/remove global admin permissions from the list of users."
},
"role": {
"type": "object",
"properties": {
"roleId": {
"description": "The role ID, such as BASIC, ADMIN or a custom role ID.",
"type": "string"
},
"appId": {
"description": "The app that the role relates to.",
"type": "string"
}
},
"description": "Add/remove a per-app role, such as BASIC, ADMIN etc.",
"required": [
"roleId",
"appId"
]
},
"userIds": {
"description": "The user IDs to be updated to add/remove the specified roles.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"userIds"
]
},
"rolesOutput": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"userIds": {
"description": "The updated users' IDs",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"userIds"
]
}
},
"required": [
"data"
]
} }
} }
}, },
@ -2186,6 +2317,70 @@
} }
} }
}, },
"/roles/assign": {
"post": {
"operationId": "roleAssign",
"summary": "Assign a role to a list of users",
"description": "This is a business/enterprise only endpoint",
"tags": [
"roles"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/rolesAssign"
}
}
}
},
"responses": {
"200": {
"description": "Returns a list of updated user IDs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/rolesOutput"
}
}
}
}
}
}
},
"/roles/unassign": {
"post": {
"operationId": "roleUnAssign",
"summary": "Un-assign a role from a list of users",
"description": "This is a business/enterprise only endpoint",
"tags": [
"roles"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/rolesUnAssign"
}
}
}
},
"responses": {
"200": {
"description": "Returns a list of updated user IDs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/rolesOutput"
}
}
}
}
}
}
},
"/tables/{tableId}/rows": { "/tables/{tableId}/rows": {
"post": { "post": {
"operationId": "rowCreate", "operationId": "rowCreate",

View File

@ -1297,7 +1297,8 @@ components:
login. login.
type: boolean type: boolean
builder: builder:
description: Describes if the user is a builder user or not. description: Describes if the user is a builder user or not. This field can only
be set on a business or enterprise license.
type: object type: object
properties: properties:
global: global:
@ -1305,7 +1306,8 @@ components:
system. system.
type: boolean type: boolean
admin: admin:
description: Describes if the user is an admin user or not. description: Describes if the user is an admin user or not. This field can only
be set on a business or enterprise license.
type: object type: object
properties: properties:
global: global:
@ -1313,7 +1315,8 @@ components:
type: boolean type: boolean
roles: roles:
description: Contains the roles of the user per app (assuming they are not a description: Contains the roles of the user per app (assuming they are not a
builder user). builder user). This field can only be set on a business or
enterprise license.
type: object type: object
additionalProperties: additionalProperties:
type: string type: string
@ -1352,7 +1355,8 @@ components:
login. login.
type: boolean type: boolean
builder: builder:
description: Describes if the user is a builder user or not. description: Describes if the user is a builder user or not. This field can only
be set on a business or enterprise license.
type: object type: object
properties: properties:
global: global:
@ -1360,7 +1364,8 @@ components:
system. system.
type: boolean type: boolean
admin: admin:
description: Describes if the user is an admin user or not. description: Describes if the user is an admin user or not. This field can only
be set on a business or enterprise license.
type: object type: object
properties: properties:
global: global:
@ -1368,7 +1373,8 @@ components:
type: boolean type: boolean
roles: roles:
description: Contains the roles of the user per app (assuming they are not a description: Contains the roles of the user per app (assuming they are not a
builder user). builder user). This field can only be set on a business or
enterprise license.
type: object type: object
additionalProperties: additionalProperties:
type: string type: string
@ -1415,7 +1421,8 @@ components:
login. login.
type: boolean type: boolean
builder: builder:
description: Describes if the user is a builder user or not. description: Describes if the user is a builder user or not. This field can only
be set on a business or enterprise license.
type: object type: object
properties: properties:
global: global:
@ -1423,7 +1430,8 @@ components:
system. system.
type: boolean type: boolean
admin: admin:
description: Describes if the user is an admin user or not. description: Describes if the user is an admin user or not. This field can only
be set on a business or enterprise license.
type: object type: object
properties: properties:
global: global:
@ -1431,7 +1439,8 @@ components:
type: boolean type: boolean
roles: roles:
description: Contains the roles of the user per app (assuming they are not a description: Contains the roles of the user per app (assuming they are not a
builder user). builder user). This field can only be set on a business or
enterprise license.
type: object type: object
additionalProperties: additionalProperties:
type: string type: string
@ -1547,6 +1556,99 @@ components:
insensitive starts with match. insensitive starts with match.
required: required:
- name - name
rolesAssign:
type: object
properties:
appBuilder:
type: object
properties:
appId:
description: The app that the users should have app builder privileges granted
for.
type: string
description: Allow setting users to builders per app.
required:
- appId
builder:
type: boolean
description: Add/remove global builder permissions from the list of users.
admin:
type: boolean
description: Add/remove global admin permissions from the list of users.
role:
type: object
properties:
roleId:
description: The role ID, such as BASIC, ADMIN or a custom role ID.
type: string
appId:
description: The app that the role relates to.
type: string
description: Add/remove a per-app role, such as BASIC, ADMIN etc.
required:
- roleId
- appId
userIds:
description: The user IDs to be updated to add/remove the specified roles.
type: array
items:
type: string
required:
- userIds
rolesUnAssign:
type: object
properties:
appBuilder:
type: object
properties:
appId:
description: The app that the users should have app builder privileges granted
for.
type: string
description: Allow setting users to builders per app.
required:
- appId
builder:
type: boolean
description: Add/remove global builder permissions from the list of users.
admin:
type: boolean
description: Add/remove global admin permissions from the list of users.
role:
type: object
properties:
roleId:
description: The role ID, such as BASIC, ADMIN or a custom role ID.
type: string
appId:
description: The app that the role relates to.
type: string
description: Add/remove a per-app role, such as BASIC, ADMIN etc.
required:
- roleId
- appId
userIds:
description: The user IDs to be updated to add/remove the specified roles.
type: array
items:
type: string
required:
- userIds
rolesOutput:
type: object
properties:
data:
type: object
properties:
userIds:
description: The updated users' IDs
type: array
items:
type: string
required:
- userIds
required:
- data
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
paths: paths:
@ -1757,6 +1859,46 @@ paths:
examples: examples:
queries: queries:
$ref: "#/components/examples/queries" $ref: "#/components/examples/queries"
/roles/assign:
post:
operationId: roleAssign
summary: Assign a role to a list of users
description: This is a business/enterprise only endpoint
tags:
- roles
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/rolesAssign"
responses:
"200":
description: Returns a list of updated user IDs
content:
application/json:
schema:
$ref: "#/components/schemas/rolesOutput"
/roles/unassign:
post:
operationId: roleUnAssign
summary: Un-assign a role from a list of users
description: This is a business/enterprise only endpoint
tags:
- roles
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/rolesUnAssign"
responses:
"200":
description: Returns a list of updated user IDs
content:
application/json:
schema:
$ref: "#/components/schemas/rolesOutput"
"/tables/{tableId}/rows": "/tables/{tableId}/rows":
post: post:
operationId: rowCreate operationId: rowCreate

View File

@ -5,6 +5,7 @@ import query from "./query"
import user from "./user" import user from "./user"
import metrics from "./metrics" import metrics from "./metrics"
import misc from "./misc" import misc from "./misc"
import roles from "./roles"
export const examples = { export const examples = {
...application.getExamples(), ...application.getExamples(),
@ -23,4 +24,5 @@ export const schemas = {
...query.getSchemas(), ...query.getSchemas(),
...user.getSchemas(), ...user.getSchemas(),
...misc.getSchemas(), ...misc.getSchemas(),
...roles.getSchemas(),
} }

View File

@ -0,0 +1,65 @@
import { object } from "./utils"
import Resource from "./utils/Resource"
const roleSchema = object(
{
appBuilder: object(
{
appId: {
description:
"The app that the users should have app builder privileges granted for.",
type: "string",
},
},
{ description: "Allow setting users to builders per app." }
),
builder: {
type: "boolean",
description:
"Add/remove global builder permissions from the list of users.",
},
admin: {
type: "boolean",
description:
"Add/remove global admin permissions from the list of users.",
},
role: object(
{
roleId: {
description: "The role ID, such as BASIC, ADMIN or a custom role ID.",
type: "string",
},
appId: {
description: "The app that the role relates to.",
type: "string",
},
},
{ description: "Add/remove a per-app role, such as BASIC, ADMIN etc." }
),
userIds: {
description:
"The user IDs to be updated to add/remove the specified roles.",
type: "array",
items: {
type: "string",
},
},
},
{ required: ["userIds"] }
)
export default new Resource().setSchemas({
rolesAssign: roleSchema,
rolesUnAssign: roleSchema,
rolesOutput: object({
data: object({
userIds: {
description: "The updated users' IDs",
type: "array",
items: {
type: "string",
},
},
}),
}),
})

View File

@ -58,7 +58,8 @@ const userSchema = object(
type: "boolean", type: "boolean",
}, },
builder: { builder: {
description: "Describes if the user is a builder user or not.", description:
"Describes if the user is a builder user or not. This field can only be set on a business or enterprise license.",
type: "object", type: "object",
properties: { properties: {
global: { global: {
@ -69,7 +70,8 @@ const userSchema = object(
}, },
}, },
admin: { admin: {
description: "Describes if the user is an admin user or not.", description:
"Describes if the user is an admin user or not. This field can only be set on a business or enterprise license.",
type: "object", type: "object",
properties: { properties: {
global: { global: {
@ -81,7 +83,7 @@ const userSchema = object(
}, },
roles: { roles: {
description: description:
"Contains the roles of the user per app (assuming they are not a builder user).", "Contains the roles of the user per app (assuming they are not a builder user). This field can only be set on a business or enterprise license.",
type: "object", type: "object",
additionalProperties: { additionalProperties: {
type: "string", type: "string",

View File

@ -77,18 +77,19 @@ async function initDeployedApp(prodAppId: any) {
) )
).rows.map((row: any) => row.doc) ).rows.map((row: any) => row.doc)
await clearMetadata() await clearMetadata()
console.log("You have " + automations.length + " automations") const { count } = await disableAllCrons(prodAppId)
const promises = [] const promises = []
console.log("Disabling prod crons..")
await disableAllCrons(prodAppId)
console.log("Prod Cron triggers disabled..")
console.log("Enabling cron triggers for deployed app..")
for (let automation of automations) { for (let automation of automations) {
promises.push(enableCronTrigger(prodAppId, automation)) promises.push(enableCronTrigger(prodAppId, automation))
} }
await Promise.all(promises) const results = await Promise.all(promises)
console.log("Enabled cron triggers for deployed app..") const enabledCount = results
// sync the automations back to the dev DB - since there is now cron .map(result => result.enabled)
.filter(result => result).length
console.log(
`Cleared ${count} old CRON, enabled ${enabledCount} new CRON triggers for app deployment`
)
// sync the automations back to the dev DB - since there is now CRON
// information attached // information attached
await sdk.applications.syncApp(dbCore.getDevAppID(prodAppId), { await sdk.applications.syncApp(dbCore.getDevAppID(prodAppId), {
automationOnly: true, automationOnly: true,

View File

@ -3,6 +3,8 @@ import { search as stringSearch, addRev } from "./utils"
import * as controller from "../application" import * as controller from "../application"
import * as deployController from "../deploy" import * as deployController from "../deploy"
import { Application } from "../../../definitions/common" import { Application } from "../../../definitions/common"
import { UserCtx } from "@budibase/types"
import { Next } from "koa"
function fixAppID(app: Application, params: any) { function fixAppID(app: Application, params: any) {
if (!params) { if (!params) {
@ -14,7 +16,7 @@ function fixAppID(app: Application, params: any) {
return app return app
} }
async function setResponseApp(ctx: any) { async function setResponseApp(ctx: UserCtx) {
const appId = ctx.body?.appId const appId = ctx.body?.appId
if (appId && (!ctx.params || !ctx.params.appId)) { if (appId && (!ctx.params || !ctx.params.appId)) {
ctx.params = { appId } ctx.params = { appId }
@ -28,14 +30,14 @@ async function setResponseApp(ctx: any) {
} }
} }
export async function search(ctx: any, next: any) { export async function search(ctx: UserCtx, next: Next) {
const { name } = ctx.request.body const { name } = ctx.request.body
const apps = await dbCore.getAllApps({ all: true }) const apps = await dbCore.getAllApps({ all: true })
ctx.body = stringSearch(apps, name) ctx.body = stringSearch(apps, name)
await next() await next()
} }
export async function create(ctx: any, next: any) { export async function create(ctx: UserCtx, next: Next) {
if (!ctx.request.body || !ctx.request.body.useTemplate) { if (!ctx.request.body || !ctx.request.body.useTemplate) {
ctx.request.body = { ctx.request.body = {
useTemplate: false, useTemplate: false,
@ -47,14 +49,14 @@ export async function create(ctx: any, next: any) {
await next() await next()
} }
export async function read(ctx: any, next: any) { export async function read(ctx: UserCtx, next: Next) {
await context.doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
await setResponseApp(ctx) await setResponseApp(ctx)
await next() await next()
}) })
} }
export async function update(ctx: any, next: any) { export async function update(ctx: UserCtx, next: Next) {
ctx.request.body = await addRev(fixAppID(ctx.request.body, ctx.params)) ctx.request.body = await addRev(fixAppID(ctx.request.body, ctx.params))
await context.doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
await controller.update(ctx) await controller.update(ctx)
@ -63,7 +65,7 @@ export async function update(ctx: any, next: any) {
}) })
} }
export async function destroy(ctx: any, next: any) { export async function destroy(ctx: UserCtx, next: Next) {
await context.doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
// get the app before deleting it // get the app before deleting it
await setResponseApp(ctx) await setResponseApp(ctx)
@ -75,14 +77,14 @@ export async function destroy(ctx: any, next: any) {
}) })
} }
export async function unpublish(ctx: any, next: any) { export async function unpublish(ctx: UserCtx, next: Next) {
await context.doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
await controller.unpublish(ctx) await controller.unpublish(ctx)
await next() await next()
}) })
} }
export async function publish(ctx: any, next: any) { export async function publish(ctx: UserCtx, next: Next) {
await context.doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
await deployController.publishApp(ctx) await deployController.publishApp(ctx)
await next() await next()

View File

@ -16,6 +16,10 @@ export type CreateRowParams = components["schemas"]["row"]
export type User = components["schemas"]["userOutput"]["data"] export type User = components["schemas"]["userOutput"]["data"]
export type CreateUserParams = components["schemas"]["user"] export type CreateUserParams = components["schemas"]["user"]
export type RoleAssignRequest = components["schemas"]["rolesAssign"]
export type RoleUnAssignRequest = components["schemas"]["rolesUnAssign"]
export type RoleAssignmentResponse = components["schemas"]["rolesOutput"]
export type SearchInputParams = export type SearchInputParams =
| components["schemas"]["nameSearch"] | components["schemas"]["nameSearch"]
| components["schemas"]["rowSearch"] | components["schemas"]["rowSearch"]

View File

@ -1,14 +1,16 @@
import { search as stringSearch } from "./utils" import { search as stringSearch } from "./utils"
import * as queryController from "../query" import * as queryController from "../query"
import { UserCtx } from "@budibase/types"
import { Next } from "koa"
export async function search(ctx: any, next: any) { export async function search(ctx: UserCtx, next: Next) {
await queryController.fetch(ctx) await queryController.fetch(ctx)
const { name } = ctx.request.body const { name } = ctx.request.body
ctx.body = stringSearch(ctx.body, name) ctx.body = stringSearch(ctx.body, name)
await next() await next()
} }
export async function execute(ctx: any, next: any) { export async function execute(ctx: UserCtx, next: Next) {
// don't wrap this, already returns "data" // don't wrap this, already returns "data"
await queryController.executeV2(ctx) await queryController.executeV2(ctx)
await next() await next()

View File

@ -0,0 +1,33 @@
import { UserCtx } from "@budibase/types"
import { Next } from "koa"
import { sdk } from "@budibase/pro"
import {
RoleAssignmentResponse,
RoleUnAssignRequest,
RoleAssignRequest,
} from "./mapping/types"
async function assign(
ctx: UserCtx<RoleAssignRequest, RoleAssignmentResponse>,
next: Next
) {
const { userIds, ...assignmentProps } = ctx.request.body
await sdk.publicApi.roles.assign(userIds, assignmentProps)
ctx.body = { data: { userIds } }
await next()
}
async function unAssign(
ctx: UserCtx<RoleUnAssignRequest, RoleAssignmentResponse>,
next: Next
) {
const { userIds, ...unAssignmentProps } = ctx.request.body
await sdk.publicApi.roles.unAssign(userIds, unAssignmentProps)
ctx.body = { data: { userIds } }
await next()
}
export default {
assign,
unAssign,
}

View File

@ -1,7 +1,8 @@
import * as rowController from "../row" import * as rowController from "../row"
import { addRev } from "./utils" import { addRev } from "./utils"
import { Row } from "@budibase/types" import { Row, UserCtx } from "@budibase/types"
import { convertBookmark } from "../../../utilities" import { convertBookmark } from "../../../utilities"
import { Next } from "koa"
// makes sure that the user doesn't need to pass in the type, tableId or _id params for // makes sure that the user doesn't need to pass in the type, tableId or _id params for
// the call to be correct // the call to be correct
@ -21,7 +22,7 @@ export function fixRow(row: Row, params: any) {
return row return row
} }
export async function search(ctx: any, next: any) { export async function search(ctx: UserCtx, next: Next) {
let { sort, paginate, bookmark, limit, query } = ctx.request.body let { sort, paginate, bookmark, limit, query } = ctx.request.body
// update the body to the correct format of the internal search // update the body to the correct format of the internal search
if (!sort) { if (!sort) {
@ -40,25 +41,25 @@ export async function search(ctx: any, next: any) {
await next() await next()
} }
export async function create(ctx: any, next: any) { export async function create(ctx: UserCtx, next: Next) {
ctx.request.body = fixRow(ctx.request.body, ctx.params) ctx.request.body = fixRow(ctx.request.body, ctx.params)
await rowController.save(ctx) await rowController.save(ctx)
await next() await next()
} }
export async function read(ctx: any, next: any) { export async function read(ctx: UserCtx, next: Next) {
await rowController.fetchEnrichedRow(ctx) await rowController.fetchEnrichedRow(ctx)
await next() await next()
} }
export async function update(ctx: any, next: any) { export async function update(ctx: UserCtx, next: Next) {
const { tableId } = ctx.params const { tableId } = ctx.params
ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params), tableId) ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params), tableId)
await rowController.save(ctx) await rowController.save(ctx)
await next() await next()
} }
export async function destroy(ctx: any, next: any) { export async function destroy(ctx: UserCtx, next: Next) {
const { tableId } = ctx.params const { tableId } = ctx.params
// set the body as expected, with the _id and _rev fields // set the body as expected, with the _id and _rev fields
ctx.request.body = await addRev( ctx.request.body = await addRev(

View File

@ -1,6 +1,7 @@
import { search as stringSearch, addRev } from "./utils" import { search as stringSearch, addRev } from "./utils"
import * as controller from "../table" import * as controller from "../table"
import { Table } from "@budibase/types" import { Table, UserCtx } from "@budibase/types"
import { Next } from "koa"
function fixTable(table: Table, params: any) { function fixTable(table: Table, params: any) {
if (!params || !table) { if (!params || !table) {
@ -15,24 +16,24 @@ function fixTable(table: Table, params: any) {
return table return table
} }
export async function search(ctx: any, next: any) { export async function search(ctx: UserCtx, next: Next) {
const { name } = ctx.request.body const { name } = ctx.request.body
await controller.fetch(ctx) await controller.fetch(ctx)
ctx.body = stringSearch(ctx.body, name) ctx.body = stringSearch(ctx.body, name)
await next() await next()
} }
export async function create(ctx: any, next: any) { export async function create(ctx: UserCtx, next: Next) {
await controller.save(ctx) await controller.save(ctx)
await next() await next()
} }
export async function read(ctx: any, next: any) { export async function read(ctx: UserCtx, next: Next) {
await controller.find(ctx) await controller.find(ctx)
await next() await next()
} }
export async function update(ctx: any, next: any) { export async function update(ctx: UserCtx, next: Next) {
ctx.request.body = await addRev( ctx.request.body = await addRev(
fixTable(ctx.request.body, ctx.params), fixTable(ctx.request.body, ctx.params),
ctx.params.tableId ctx.params.tableId
@ -41,7 +42,7 @@ export async function update(ctx: any, next: any) {
await next() await next()
} }
export async function destroy(ctx: any, next: any) { export async function destroy(ctx: UserCtx, next: Next) {
await controller.destroy(ctx) await controller.destroy(ctx)
ctx.body = ctx.table ctx.body = ctx.table
await next() await next()

View File

@ -7,16 +7,18 @@ import {
import { publicApiUserFix } from "../../../utilities/users" import { publicApiUserFix } from "../../../utilities/users"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
import { search as stringSearch } from "./utils" import { search as stringSearch } from "./utils"
import { BBContext, User } from "@budibase/types" import { UserCtx, User } from "@budibase/types"
import { Next } from "koa"
import { sdk } from "@budibase/pro"
function isLoggedInUser(ctx: BBContext, user: User) { function isLoggedInUser(ctx: UserCtx, user: User) {
const loggedInId = ctx.user?._id const loggedInId = ctx.user?._id
const globalUserId = dbCore.getGlobalIDFromUserMetadataID(loggedInId!) const globalUserId = dbCore.getGlobalIDFromUserMetadataID(loggedInId!)
// check both just incase // check both just incase
return globalUserId === user._id || loggedInId === user._id return globalUserId === user._id || loggedInId === user._id
} }
function getUser(ctx: BBContext, userId?: string) { function getUser(ctx: UserCtx, userId?: string) {
if (userId) { if (userId) {
ctx.params = { userId } ctx.params = { userId }
} else if (!ctx.params?.userId) { } else if (!ctx.params?.userId) {
@ -25,42 +27,38 @@ function getUser(ctx: BBContext, userId?: string) {
return readGlobalUser(ctx) return readGlobalUser(ctx)
} }
export async function search(ctx: BBContext, next: any) { export async function search(ctx: UserCtx, next: Next) {
const { name } = ctx.request.body const { name } = ctx.request.body
const users = await allGlobalUsers(ctx) const users = await allGlobalUsers(ctx)
ctx.body = stringSearch(users, name, "email") ctx.body = stringSearch(users, name, "email")
await next() await next()
} }
export async function create(ctx: BBContext, next: any) { export async function create(ctx: UserCtx, next: Next) {
const response = await saveGlobalUser(publicApiUserFix(ctx)) ctx = publicApiUserFix(await sdk.publicApi.users.roleCheck(ctx))
const response = await saveGlobalUser(ctx)
ctx.body = await getUser(ctx, response._id) ctx.body = await getUser(ctx, response._id)
await next() await next()
} }
export async function read(ctx: BBContext, next: any) { export async function read(ctx: UserCtx, next: Next) {
ctx.body = await readGlobalUser(ctx) ctx.body = await readGlobalUser(ctx)
await next() await next()
} }
export async function update(ctx: BBContext, next: any) { export async function update(ctx: UserCtx, next: Next) {
const user = await readGlobalUser(ctx) const user = await readGlobalUser(ctx)
ctx.request.body = { ctx.request.body = {
...ctx.request.body, ...ctx.request.body,
_rev: user._rev, _rev: user._rev,
} }
// disallow updating your own role - always overwrite with DB roles ctx = publicApiUserFix(await sdk.publicApi.users.roleCheck(ctx, user))
if (isLoggedInUser(ctx, user)) { const response = await saveGlobalUser(ctx)
ctx.request.body.builder = user.builder
ctx.request.body.admin = user.admin
ctx.request.body.roles = user.roles
}
const response = await saveGlobalUser(publicApiUserFix(ctx))
ctx.body = await getUser(ctx, response._id) ctx.body = await getUser(ctx, response._id)
await next() await next()
} }
export async function destroy(ctx: BBContext, next: any) { export async function destroy(ctx: UserCtx, next: Next) {
const user = await getUser(ctx) const user = await getUser(ctx)
// disallow deleting yourself // disallow deleting yourself
if (isLoggedInUser(ctx, user)) { if (isLoggedInUser(ctx, user)) {

View File

@ -13,9 +13,11 @@ import {
Row, Row,
Table, Table,
UserCtx, UserCtx,
EmptyFilterOption,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as utils from "./utils" import * as utils from "./utils"
import { dataFilters } from "@budibase/shared-core"
export async function handleRequest( export async function handleRequest(
operation: Operation, operation: Operation,
@ -38,6 +40,13 @@ export async function handleRequest(
} }
} }
if (
!dataFilters.hasFilters(opts?.filters) &&
opts?.filters?.onEmptyFilter === EmptyFilterOption.RETURN_NONE
) {
return []
}
return new ExternalRequest(operation, tableId, opts?.datasource).run( return new ExternalRequest(operation, tableId, opts?.datasource).run(
opts || {} opts || {}
) )

View File

@ -0,0 +1,56 @@
import controller from "../../controllers/public/roles"
import Endpoint from "./utils/Endpoint"
const write = []
/**
* @openapi
* /roles/assign:
* post:
* operationId: roleAssign
* summary: Assign a role to a list of users
* description: This is a business/enterprise only endpoint
* tags:
* - roles
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/rolesAssign'
* responses:
* 200:
* description: Returns a list of updated user IDs
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/rolesOutput'
*/
write.push(new Endpoint("post", "/roles/assign", controller.assign))
/**
* @openapi
* /roles/unassign:
* post:
* operationId: roleUnAssign
* summary: Un-assign a role from a list of users
* description: This is a business/enterprise only endpoint
* tags:
* - roles
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/rolesUnAssign'
* responses:
* 200:
* description: Returns a list of updated user IDs
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/rolesOutput'
*/
write.push(new Endpoint("post", "/roles/unassign", controller.unAssign))
export default { write, read: [] }

View File

@ -1,38 +0,0 @@
const setup = require("../../tests/utilities")
const { generateMakeRequest } = require("./utils")
const workerRequests = require("../../../../utilities/workerRequests")
let config = setup.getConfig()
let apiKey, globalUser, makeRequest
beforeAll(async () => {
await config.init()
globalUser = await config.globalUser()
apiKey = await config.generateApiKey(globalUser._id)
makeRequest = generateMakeRequest(apiKey)
workerRequests.readGlobalUser.mockReturnValue(globalUser)
})
afterAll(setup.afterAll)
describe("check user endpoints", () => {
it("should not allow a user to update their own roles", async () => {
const res = await makeRequest("put", `/users/${globalUser._id}`, {
...globalUser,
roles: {
"app_1": "ADMIN",
}
})
expect(workerRequests.saveGlobalUser.mock.lastCall[0].body.data.roles["app_1"]).toBeUndefined()
expect(res.status).toBe(200)
expect(res.body.data.roles["app_1"]).toBeUndefined()
})
it("should not allow a user to delete themselves", async () => {
const res = await makeRequest("delete", `/users/${globalUser._id}`)
expect(res.status).toBe(405)
expect(workerRequests.deleteGlobalUser.mock.lastCall).toBeUndefined()
})
})

View File

@ -0,0 +1,126 @@
import * as setup from "../../tests/utilities"
import { generateMakeRequest, MakeRequestResponse } from "./utils"
import { User } from "@budibase/types"
import { mocks } from "@budibase/backend-core/tests"
import * as workerRequests from "../../../../utilities/workerRequests"
const mockedWorkerReq = jest.mocked(workerRequests)
let config = setup.getConfig()
let apiKey: string, globalUser: User, makeRequest: MakeRequestResponse
beforeAll(async () => {
await config.init()
globalUser = await config.globalUser()
apiKey = await config.generateApiKey(globalUser._id)
makeRequest = generateMakeRequest(apiKey)
mockedWorkerReq.readGlobalUser.mockImplementation(() =>
Promise.resolve(globalUser)
)
})
afterAll(setup.afterAll)
function base() {
return {
tenantId: config.getTenantId(),
firstName: "Test",
lastName: "Test",
}
}
function updateMock() {
mockedWorkerReq.readGlobalUser.mockImplementation(ctx => ctx.request.body)
}
describe("check user endpoints", () => {
it("should not allow a user to update their own roles", async () => {
const res = await makeRequest("put", `/users/${globalUser._id}`, {
...globalUser,
roles: {
app_1: "ADMIN",
},
})
expect(
mockedWorkerReq.saveGlobalUser.mock.lastCall?.[0].body.data.roles["app_1"]
).toBeUndefined()
expect(res.status).toBe(200)
expect(res.body.data.roles["app_1"]).toBeUndefined()
})
it("should not allow a user to delete themselves", async () => {
const res = await makeRequest("delete", `/users/${globalUser._id}`)
expect(res.status).toBe(405)
expect(mockedWorkerReq.deleteGlobalUser.mock.lastCall).toBeUndefined()
})
})
describe("no user role update in free", () => {
beforeAll(() => {
updateMock()
})
it("should not allow 'roles' to be updated", async () => {
const res = await makeRequest("post", "/users", {
...base(),
roles: { app_a: "BASIC" },
})
expect(res.status).toBe(200)
expect(res.body.data.roles["app_a"]).toBeUndefined()
})
it("should not allow 'admin' to be updated", async () => {
const res = await makeRequest("post", "/users", {
...base(),
admin: { global: true },
})
expect(res.status).toBe(200)
expect(res.body.data.admin).toBeUndefined()
})
it("should not allow 'builder' to be updated", async () => {
const res = await makeRequest("post", "/users", {
...base(),
builder: { global: true },
})
expect(res.status).toBe(200)
expect(res.body.data.builder).toBeUndefined()
})
})
describe("no user role update in business", () => {
beforeAll(() => {
updateMock()
mocks.licenses.usePublicApiUserRoles()
})
it("should allow 'roles' to be updated", async () => {
const res = await makeRequest("post", "/users", {
...base(),
roles: { app_a: "BASIC" },
})
expect(res.status).toBe(200)
expect(res.body.data.roles["app_a"]).toBe("BASIC")
})
it("should allow 'admin' to be updated", async () => {
mocks.licenses.usePublicApiUserRoles()
const res = await makeRequest("post", "/users", {
...base(),
admin: { global: true },
})
expect(res.status).toBe(200)
expect(res.body.data.admin.global).toBe(true)
})
it("should allow 'builder' to be updated", async () => {
mocks.licenses.usePublicApiUserRoles()
const res = await makeRequest("post", "/users", {
...base(),
builder: { global: true },
})
expect(res.status).toBe(200)
expect(res.body.data.builder.global).toBe(true)
})
})

View File

@ -1,120 +1,41 @@
import Sentry from "@sentry/node"
if (process.env.DD_APM_ENABLED) { if (process.env.DD_APM_ENABLED) {
require("./ddApm") require("./ddApm")
} }
// need to load environment first // need to load environment first
import env from "./environment" import env from "./environment"
import { ExtendableContext } from "koa"
import * as db from "./db" import * as db from "./db"
db.init() db.init()
import Koa from "koa"
import koaBody from "koa-body"
import http from "http"
import * as api from "./api"
import * as automations from "./automations"
import { Thread } from "./threads"
import * as redis from "./utilities/redis"
import { ServiceType } from "@budibase/types" import { ServiceType } from "@budibase/types"
import { import { env as coreEnv } from "@budibase/backend-core"
events,
logging,
middleware,
timers,
env as coreEnv,
} from "@budibase/backend-core"
coreEnv._set("SERVICE_TYPE", ServiceType.APPS) coreEnv._set("SERVICE_TYPE", ServiceType.APPS)
import { apiEnabled } from "./features"
import createKoaApp from "./koa"
import Koa from "koa"
import { Server } from "http"
import { startup } from "./startup" import { startup } from "./startup"
const Sentry = require("@sentry/node")
const destroyable = require("server-destroy")
const { userAgent } = require("koa-useragent")
const app = new Koa() let app: Koa, server: Server
let mbNumber = parseInt(env.HTTP_MB_LIMIT || "10") async function start() {
if (!mbNumber || isNaN(mbNumber)) { if (apiEnabled()) {
mbNumber = 10 const koa = createKoaApp()
} app = koa.app
// set up top level koa middleware server = koa.server
app.use(
koaBody({
multipart: true,
formLimit: `${mbNumber}mb`,
jsonLimit: `${mbNumber}mb`,
textLimit: `${mbNumber}mb`,
// @ts-ignore
enableTypes: ["json", "form", "text"],
parsedMethods: ["POST", "PUT", "PATCH", "DELETE"],
})
)
app.use(middleware.correlation)
app.use(middleware.pino)
app.use(userAgent)
if (env.isProd()) {
env._set("NODE_ENV", "production")
Sentry.init()
app.on("error", (err: any, ctx: ExtendableContext) => {
Sentry.withScope(function (scope: any) {
scope.addEventProcessor(function (event: any) {
return Sentry.Handlers.parseRequest(event, ctx.request)
})
Sentry.captureException(err)
})
})
}
const server = http.createServer(app.callback())
destroyable(server)
let shuttingDown = false,
errCode = 0
server.on("close", async () => {
// already in process
if (shuttingDown) {
return
} }
shuttingDown = true
console.log("Server Closed")
timers.cleanup()
await automations.shutdown()
await redis.shutdown()
events.shutdown()
await Thread.shutdown()
api.shutdown()
if (!env.isTest()) {
process.exit(errCode)
}
})
export default server.listen(env.PORT || 0, async () => {
await startup(app, server) await startup(app, server)
}) if (env.isProd()) {
env._set("NODE_ENV", "production")
const shutdown = () => { Sentry.init()
server.close() }
// @ts-ignore
server.destroy()
} }
process.on("uncaughtException", err => { start().catch(err => {
// @ts-ignore console.error(`Failed server startup - ${err.message}`)
// don't worry about this error, comes from zlib isn't important
if (err && err["code"] === "ERR_INVALID_CHAR") {
return
}
errCode = -1
logging.logAlert("Uncaught exception.", err)
shutdown()
}) })
process.on("SIGTERM", () => { export function getServer() {
shutdown() return server
}) }
process.on("SIGINT", () => {
shutdown()
})

View File

@ -2,6 +2,7 @@ import { processEvent } from "./utils"
import { automationQueue } from "./bullboard" import { automationQueue } from "./bullboard"
import { rebootTrigger } from "./triggers" import { rebootTrigger } from "./triggers"
import BullQueue from "bull" import BullQueue from "bull"
import { automationsEnabled } from "../features"
export { automationQueue } from "./bullboard" export { automationQueue } from "./bullboard"
export { shutdown } from "./bullboard" export { shutdown } from "./bullboard"
@ -12,6 +13,9 @@ export { BUILTIN_ACTION_DEFINITIONS, getActionDefinitions } from "./actions"
* This module is built purely to kick off the worker farm and manage the inputs/outputs * This module is built purely to kick off the worker farm and manage the inputs/outputs
*/ */
export async function init() { export async function init() {
if (!automationsEnabled()) {
return
}
// this promise will not complete // this promise will not complete
const promise = automationQueue.process(async job => { const promise = automationQueue.process(async job => {
await processEvent(job) await processEvent(job)

View File

@ -11,6 +11,7 @@ import {
AutomationStepInput, AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
EmptyFilterOption,
SearchFilters, SearchFilters,
Table, Table,
} from "@budibase/types" } from "@budibase/types"
@ -26,16 +27,6 @@ const SortOrderPretty = {
[SortOrder.DESCENDING]: "Descending", [SortOrder.DESCENDING]: "Descending",
} }
enum EmptyFilterOption {
RETURN_ALL = "all",
RETURN_NONE = "none",
}
const EmptyFilterOptionPretty = {
[EmptyFilterOption.RETURN_ALL]: "Return all table rows",
[EmptyFilterOption.RETURN_NONE]: "Return no rows",
}
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
description: "Query rows from the database", description: "Query rows from the database",
icon: "Search", icon: "Search",
@ -77,12 +68,6 @@ export const definition: AutomationStepSchema = {
title: "Limit", title: "Limit",
customType: AutomationCustomIOType.QUERY_LIMIT, customType: AutomationCustomIOType.QUERY_LIMIT,
}, },
onEmptyFilter: {
pretty: Object.values(EmptyFilterOptionPretty),
enum: Object.values(EmptyFilterOption),
type: AutomationIOType.STRING,
title: "When Filter Empty",
},
}, },
required: ["tableId"], required: ["tableId"],
}, },

View File

@ -15,9 +15,13 @@ import {
WebhookActionType, WebhookActionType,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../sdk" import sdk from "../sdk"
import { automationsEnabled } from "../features"
const WH_STEP_ID = definitions.WEBHOOK.stepId const WH_STEP_ID = definitions.WEBHOOK.stepId
const Runner = new Thread(ThreadType.AUTOMATION) let Runner: Thread
if (automationsEnabled()) {
Runner = new Thread(ThreadType.AUTOMATION)
}
function loggingArgs( function loggingArgs(
job: AutomationJob, job: AutomationJob,
@ -130,7 +134,8 @@ export async function disableAllCrons(appId: any) {
} }
} }
} }
return Promise.all(promises) const results = await Promise.all(promises)
return { count: results.length / 2 }
} }
export async function disableCronById(jobId: number | string) { export async function disableCronById(jobId: number | string) {
@ -169,6 +174,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
const needsCreated = const needsCreated =
!sdk.automations.isReboot(automation) && !sdk.automations.isReboot(automation) &&
!sdk.automations.disabled(automation) !sdk.automations.disabled(automation)
let enabled = false
// need to create cron job // need to create cron job
if (validCron && needsCreated) { if (validCron && needsCreated) {
@ -191,8 +197,9 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
automation._id = response.id automation._id = response.id
automation._rev = response.rev automation._rev = response.rev
}) })
enabled = true
} }
return automation return { enabled, automation }
} }
/** /**

View File

@ -34,6 +34,14 @@ export interface paths {
/** Based on query properties (currently only name) search for queries. */ /** Based on query properties (currently only name) search for queries. */
post: operations["querySearch"]; post: operations["querySearch"];
}; };
"/roles/assign": {
/** This is a business/enterprise only endpoint */
post: operations["roleAssign"];
};
"/roles/unassign": {
/** This is a business/enterprise only endpoint */
post: operations["roleUnAssign"];
};
"/tables/{tableId}/rows": { "/tables/{tableId}/rows": {
/** Creates a row within the specified table. */ /** Creates a row within the specified table. */
post: operations["rowCreate"]; post: operations["rowCreate"];
@ -256,7 +264,8 @@ export interface components {
| "auto" | "auto"
| "json" | "json"
| "internal" | "internal"
| "barcodeqr"; | "barcodeqr"
| "bigint";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ /** @enum {string} */
@ -362,7 +371,8 @@ export interface components {
| "auto" | "auto"
| "json" | "json"
| "internal" | "internal"
| "barcodeqr"; | "barcodeqr"
| "bigint";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ /** @enum {string} */
@ -470,7 +480,8 @@ export interface components {
| "auto" | "auto"
| "json" | "json"
| "internal" | "internal"
| "barcodeqr"; | "barcodeqr"
| "bigint";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ /** @enum {string} */
@ -577,17 +588,17 @@ export interface components {
lastName?: string; lastName?: string;
/** @description If set to true forces the user to reset their password on first login. */ /** @description If set to true forces the user to reset their password on first login. */
forceResetPassword?: boolean; forceResetPassword?: boolean;
/** @description Describes if the user is a builder user or not. */ /** @description Describes if the user is a builder user or not. This field can only be set on a business or enterprise license. */
builder?: { builder?: {
/** @description If set to true the user will be able to build any app in the system. */ /** @description If set to true the user will be able to build any app in the system. */
global?: boolean; global?: boolean;
}; };
/** @description Describes if the user is an admin user or not. */ /** @description Describes if the user is an admin user or not. This field can only be set on a business or enterprise license. */
admin?: { admin?: {
/** @description If set to true the user will be able to administrate the system. */ /** @description If set to true the user will be able to administrate the system. */
global?: boolean; global?: boolean;
}; };
/** @description Contains the roles of the user per app (assuming they are not a builder user). */ /** @description Contains the roles of the user per app (assuming they are not a builder user). This field can only be set on a business or enterprise license. */
roles: { [key: string]: string }; roles: { [key: string]: string };
}; };
userOutput: { userOutput: {
@ -607,17 +618,17 @@ export interface components {
lastName?: string; lastName?: string;
/** @description If set to true forces the user to reset their password on first login. */ /** @description If set to true forces the user to reset their password on first login. */
forceResetPassword?: boolean; forceResetPassword?: boolean;
/** @description Describes if the user is a builder user or not. */ /** @description Describes if the user is a builder user or not. This field can only be set on a business or enterprise license. */
builder?: { builder?: {
/** @description If set to true the user will be able to build any app in the system. */ /** @description If set to true the user will be able to build any app in the system. */
global?: boolean; global?: boolean;
}; };
/** @description Describes if the user is an admin user or not. */ /** @description Describes if the user is an admin user or not. This field can only be set on a business or enterprise license. */
admin?: { admin?: {
/** @description If set to true the user will be able to administrate the system. */ /** @description If set to true the user will be able to administrate the system. */
global?: boolean; global?: boolean;
}; };
/** @description Contains the roles of the user per app (assuming they are not a builder user). */ /** @description Contains the roles of the user per app (assuming they are not a builder user). This field can only be set on a business or enterprise license. */
roles: { [key: string]: string }; roles: { [key: string]: string };
/** @description The ID of the user. */ /** @description The ID of the user. */
_id: string; _id: string;
@ -640,17 +651,17 @@ export interface components {
lastName?: string; lastName?: string;
/** @description If set to true forces the user to reset their password on first login. */ /** @description If set to true forces the user to reset their password on first login. */
forceResetPassword?: boolean; forceResetPassword?: boolean;
/** @description Describes if the user is a builder user or not. */ /** @description Describes if the user is a builder user or not. This field can only be set on a business or enterprise license. */
builder?: { builder?: {
/** @description If set to true the user will be able to build any app in the system. */ /** @description If set to true the user will be able to build any app in the system. */
global?: boolean; global?: boolean;
}; };
/** @description Describes if the user is an admin user or not. */ /** @description Describes if the user is an admin user or not. This field can only be set on a business or enterprise license. */
admin?: { admin?: {
/** @description If set to true the user will be able to administrate the system. */ /** @description If set to true the user will be able to administrate the system. */
global?: boolean; global?: boolean;
}; };
/** @description Contains the roles of the user per app (assuming they are not a builder user). */ /** @description Contains the roles of the user per app (assuming they are not a builder user). This field can only be set on a business or enterprise license. */
roles: { [key: string]: string }; roles: { [key: string]: string };
/** @description The ID of the user. */ /** @description The ID of the user. */
_id: string; _id: string;
@ -712,6 +723,52 @@ export interface components {
/** @description The name to be used when searching - this will be used in a case insensitive starts with match. */ /** @description The name to be used when searching - this will be used in a case insensitive starts with match. */
name: string; name: string;
}; };
rolesAssign: {
/** @description Allow setting users to builders per app. */
appBuilder?: {
/** @description The app that the users should have app builder privileges granted for. */
appId: string;
};
/** @description Add/remove global builder permissions from the list of users. */
builder?: boolean;
/** @description Add/remove global admin permissions from the list of users. */
admin?: boolean;
/** @description Add/remove a per-app role, such as BASIC, ADMIN etc. */
role?: {
/** @description The role ID, such as BASIC, ADMIN or a custom role ID. */
roleId: string;
/** @description The app that the role relates to. */
appId: string;
};
/** @description The user IDs to be updated to add/remove the specified roles. */
userIds: string[];
};
rolesUnAssign: {
/** @description Allow setting users to builders per app. */
appBuilder?: {
/** @description The app that the users should have app builder privileges granted for. */
appId: string;
};
/** @description Add/remove global builder permissions from the list of users. */
builder?: boolean;
/** @description Add/remove global admin permissions from the list of users. */
admin?: boolean;
/** @description Add/remove a per-app role, such as BASIC, ADMIN etc. */
role?: {
/** @description The role ID, such as BASIC, ADMIN or a custom role ID. */
roleId: string;
/** @description The app that the role relates to. */
appId: string;
};
/** @description The user IDs to be updated to add/remove the specified roles. */
userIds: string[];
};
rolesOutput: {
data: {
/** @description The updated users' IDs */
userIds: string[];
};
};
}; };
parameters: { parameters: {
/** @description The ID of the table which this request is targeting. */ /** @description The ID of the table which this request is targeting. */
@ -907,6 +964,38 @@ export interface operations {
}; };
}; };
}; };
/** This is a business/enterprise only endpoint */
roleAssign: {
responses: {
/** Returns a list of updated user IDs */
200: {
content: {
"application/json": components["schemas"]["rolesOutput"];
};
};
};
requestBody: {
content: {
"application/json": components["schemas"]["rolesAssign"];
};
};
};
/** This is a business/enterprise only endpoint */
roleUnAssign: {
responses: {
/** Returns a list of updated user IDs */
200: {
content: {
"application/json": components["schemas"]["rolesOutput"];
};
};
};
requestBody: {
content: {
"application/json": components["schemas"]["rolesUnAssign"];
};
};
};
/** Creates a row within the specified table. */ /** Creates a row within the specified table. */
rowCreate: { rowCreate: {
parameters: { parameters: {

View File

@ -38,6 +38,8 @@ function parseIntSafe(number?: string) {
} }
const environment = { const environment = {
// features
APP_FEATURES: process.env.APP_FEATURES,
// important - prefer app port to generic port // important - prefer app port to generic port
PORT: process.env.APP_PORT || process.env.PORT, PORT: process.env.APP_PORT || process.env.PORT,
COUCH_DB_URL: process.env.COUCH_DB_URL, COUCH_DB_URL: process.env.COUCH_DB_URL,

View File

@ -0,0 +1,24 @@
import { features } from "@budibase/backend-core"
import env from "./environment"
enum AppFeature {
API = "api",
AUTOMATIONS = "automations",
}
const featureList = features.processFeatureEnvVar<AppFeature>(
Object.values(AppFeature),
env.APP_FEATURES
)
export function isFeatureEnabled(feature: AppFeature) {
return featureList.includes(feature)
}
export function automationsEnabled() {
return featureList.includes(AppFeature.AUTOMATIONS)
}
export function apiEnabled() {
return featureList.includes(AppFeature.API)
}

View File

@ -341,10 +341,10 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
} }
} }
getDefinitionSQL(tableName: string) { getDefinitionSQL(tableName: string, schemaName: string) {
return `select * return `select *
from INFORMATION_SCHEMA.COLUMNS from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME='${tableName}'` where TABLE_NAME='${tableName}' AND TABLE_SCHEMA='${schemaName}'`
} }
getConstraintsSQL(tableName: string) { getConstraintsSQL(tableName: string) {
@ -388,16 +388,18 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
throw "Unable to get list of tables in database" throw "Unable to get list of tables in database"
} }
const schema = this.config.schema || DEFAULT_SCHEMA const schemaName = this.config.schema || DEFAULT_SCHEMA
const tableNames = tableInfo const tableNames = tableInfo
.filter((record: any) => record.TABLE_SCHEMA === schema) .filter((record: any) => record.TABLE_SCHEMA === schemaName)
.map((record: any) => record.TABLE_NAME) .map((record: any) => record.TABLE_NAME)
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1) .filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
const tables: Record<string, ExternalTable> = {} const tables: Record<string, ExternalTable> = {}
for (let tableName of tableNames) { for (let tableName of tableNames) {
// get the column definition (type) // get the column definition (type)
const definition = await this.runSQL(this.getDefinitionSQL(tableName)) const definition = await this.runSQL(
this.getDefinitionSQL(tableName, schemaName)
)
// find primary key constraints // find primary key constraints
const constraints = await this.runSQL(this.getConstraintsSQL(tableName)) const constraints = await this.runSQL(this.getConstraintsSQL(tableName))
// find the computed and identity columns (auto columns) // find the computed and identity columns (auto columns)

102
packages/server/src/koa.ts Normal file
View File

@ -0,0 +1,102 @@
import env from "./environment"
import { ExtendableContext } from "koa"
import Koa from "koa"
import koaBody from "koa-body"
import http from "http"
import * as api from "./api"
import * as automations from "./automations"
import { Thread } from "./threads"
import * as redis from "./utilities/redis"
import { events, logging, middleware, timers } from "@budibase/backend-core"
const Sentry = require("@sentry/node")
const destroyable = require("server-destroy")
const { userAgent } = require("koa-useragent")
export default function createKoaApp() {
const app = new Koa()
let mbNumber = parseInt(env.HTTP_MB_LIMIT || "10")
if (!mbNumber || isNaN(mbNumber)) {
mbNumber = 10
}
// set up top level koa middleware
app.use(
koaBody({
multipart: true,
formLimit: `${mbNumber}mb`,
jsonLimit: `${mbNumber}mb`,
textLimit: `${mbNumber}mb`,
// @ts-ignore
enableTypes: ["json", "form", "text"],
parsedMethods: ["POST", "PUT", "PATCH", "DELETE"],
})
)
app.use(middleware.correlation)
app.use(middleware.pino)
app.use(userAgent)
if (env.isProd()) {
app.on("error", (err: any, ctx: ExtendableContext) => {
Sentry.withScope(function (scope: any) {
scope.addEventProcessor(function (event: any) {
return Sentry.Handlers.parseRequest(event, ctx.request)
})
Sentry.captureException(err)
})
})
}
const server = http.createServer(app.callback())
destroyable(server)
let shuttingDown = false,
errCode = 0
server.on("close", async () => {
// already in process
if (shuttingDown) {
return
}
shuttingDown = true
console.log("Server Closed")
timers.cleanup()
await automations.shutdown()
await redis.shutdown()
events.shutdown()
await Thread.shutdown()
api.shutdown()
if (!env.isTest()) {
process.exit(errCode)
}
})
const listener = server.listen(env.PORT || 0)
const shutdown = () => {
server.close()
// @ts-ignore
server.destroy()
}
process.on("uncaughtException", err => {
// @ts-ignore
// don't worry about this error, comes from zlib isn't important
if (err && err["code"] === "ERR_INVALID_CHAR") {
return
}
errCode = -1
logging.logAlert("Uncaught exception.", err)
shutdown()
})
process.on("SIGTERM", () => {
shutdown()
})
process.on("SIGINT", () => {
shutdown()
})
return { app, server: listener }
}

View File

@ -17,6 +17,7 @@ import * as pro from "@budibase/pro"
import * as api from "./api" import * as api from "./api"
import sdk from "./sdk" import sdk from "./sdk"
import { initialise as initialiseWebsockets } from "./websockets" import { initialise as initialiseWebsockets } from "./websockets"
import { automationsEnabled } from "./features"
let STARTUP_RAN = false let STARTUP_RAN = false
@ -97,7 +98,9 @@ export async function startup(app?: any, server?: any) {
// configure events to use the pro audit log write // configure events to use the pro audit log write
// can't integrate directly into backend-core due to cyclic issues // can't integrate directly into backend-core due to cyclic issues
queuePromises.push(events.processors.init(pro.sdk.auditLogs.write)) queuePromises.push(events.processors.init(pro.sdk.auditLogs.write))
queuePromises.push(automations.init()) if (automationsEnabled()) {
queuePromises.push(automations.init())
}
queuePromises.push(initPro()) queuePromises.push(initPro())
if (app) { if (app) {
// bring routes online as final step once everything ready // bring routes online as final step once everything ready

View File

@ -87,7 +87,7 @@ class TestConfiguration {
if (openServer) { if (openServer) {
// use a random port because it doesn't matter // use a random port because it doesn't matter
env.PORT = "0" env.PORT = "0"
this.server = require("../../app").default this.server = require("../../app").getServer()
// we need the request for logging in, involves cookies, hard to fake // we need the request for logging in, involves cookies, hard to fake
this.request = supertest(this.server) this.request = supertest(this.server)
this.started = true this.started = true
@ -178,7 +178,7 @@ class TestConfiguration {
if (this.server) { if (this.server) {
this.server.close() this.server.close()
} else { } else {
require("../../app").default.close() require("../../app").getServer().close()
} }
if (this.allApps) { if (this.allApps) {
cleanup(this.allApps.map(app => app.appId)) cleanup(this.allApps.map(app => app.appId))

View File

@ -20,6 +20,7 @@ import {
AutomationMetadata, AutomationMetadata,
AutomationStatus, AutomationStatus,
AutomationStep, AutomationStep,
AutomationStepStatus,
} from "@budibase/types" } from "@budibase/types"
import { import {
AutomationContext, AutomationContext,
@ -452,7 +453,10 @@ class Orchestrator {
this.executionOutput.steps.splice(loopStepNumber + 1, 0, { this.executionOutput.steps.splice(loopStepNumber + 1, 0, {
id: step.id, id: step.id,
stepId: step.stepId, stepId: step.stepId,
outputs: { status: AutomationStatus.NO_ITERATIONS, success: true }, outputs: {
status: AutomationStepStatus.NO_ITERATIONS,
success: true,
},
inputs: {}, inputs: {},
}) })

View File

@ -1,9 +1,9 @@
import { InternalTables } from "../db/utils" import { InternalTables } from "../db/utils"
import { getGlobalUser } from "./global" import { getGlobalUser } from "./global"
import { context, db as dbCore, roles } from "@budibase/backend-core" import { context, roles } from "@budibase/backend-core"
import { BBContext } from "@budibase/types" import { UserCtx } from "@budibase/types"
export async function getFullUser(ctx: BBContext, userId: string) { export async function getFullUser(ctx: UserCtx, userId: string) {
const global = await getGlobalUser(userId) const global = await getGlobalUser(userId)
let metadata: any = {} let metadata: any = {}
@ -29,21 +29,12 @@ export async function getFullUser(ctx: BBContext, userId: string) {
} }
} }
export function publicApiUserFix(ctx: BBContext) { export function publicApiUserFix(ctx: UserCtx) {
if (!ctx.request.body) { if (!ctx.request.body) {
return ctx return ctx
} }
if (!ctx.request.body._id && ctx.params.userId) { if (!ctx.request.body._id && ctx.params.userId) {
ctx.request.body._id = ctx.params.userId ctx.request.body._id = ctx.params.userId
} }
if (!ctx.request.body.roles) {
ctx.request.body.roles = {}
} else {
const newRoles: { [key: string]: any } = {}
for (let [appId, role] of Object.entries(ctx.request.body.roles)) {
newRoles[dbCore.getProdAppID(appId)] = role
}
ctx.request.body.roles = newRoles
}
return ctx return ctx
} }

View File

@ -32,7 +32,18 @@
"target": "build" "target": "build"
} }
] ]
},
"dev:builder": {
"dependsOn": [
{
"projects": [
"@budibase/types"
],
"target": "build"
}
]
} }
} }
} }
} }

View File

@ -1,11 +1,11 @@
import { import {
Datasource, Datasource,
FieldType, FieldType,
SortDirection,
SortType,
SearchFilter, SearchFilter,
SearchQuery, SearchQuery,
SearchQueryFields, SearchQueryFields,
SortDirection,
SortType,
} from "@budibase/types" } from "@budibase/types"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants" import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
import { deepGet } from "./helpers" import { deepGet } from "./helpers"
@ -138,7 +138,8 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
} }
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
filter.forEach(expression => { filter.forEach(expression => {
let { operator, field, type, value, externalType } = expression let { operator, field, type, value, externalType, onEmptyFilter } =
expression
const isHbs = const isHbs =
typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0 typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0
// Parse all values into correct types // Parse all values into correct types
@ -146,6 +147,10 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
query.allOr = true query.allOr = true
return return
} }
if (onEmptyFilter) {
query.onEmptyFilter = onEmptyFilter
return
}
if ( if (
type === "datetime" && type === "datetime" &&
!isHbs && !isHbs &&
@ -203,7 +208,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
) { ) {
query.range[field].high = value query.range[field].high = value
} }
} else if (query[operator]) { } else if (query[operator] && operator !== "onEmptyFilter") {
if (type === "boolean") { if (type === "boolean") {
// Transform boolean filters to cope with null. // Transform boolean filters to cope with null.
// "equals false" needs to be "not equals true" // "equals false" needs to be "not equals true"
@ -418,7 +423,7 @@ export const hasFilters = (query?: SearchQuery) => {
if (!query) { if (!query) {
return false return false
} }
const skipped = ["allOr"] const skipped = ["allOr", "onEmptyFilter"]
for (let [key, value] of Object.entries(query)) { for (let [key, value] of Object.entries(query)) {
if (skipped.includes(key) || typeof value !== "object") { if (skipped.includes(key) || typeof value !== "object") {
continue continue

View File

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "es6",
"moduleResolution": "node", "module": "commonjs",
"lib": ["es2020"], "lib": ["es2020"],
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,

View File

@ -1,4 +1,10 @@
{ {
"extends": "./tsconfig.build.json", "extends": "./tsconfig.build.json",
"compilerOptions": {
"baseUrl": "..",
"rootDir": "src",
"composite": true,
"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"
},
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }

View File

@ -1,7 +1,9 @@
import { FieldType } from "../../documents" import { FieldType } from "../../documents"
import { EmptyFilterOption } from "../../sdk"
export type SearchFilter = { export type SearchFilter = {
operator: keyof SearchQuery operator: keyof SearchQuery
onEmptyFilter?: EmptyFilterOption
field: string field: string
type?: FieldType type?: FieldType
value: any value: any
@ -10,6 +12,7 @@ export type SearchFilter = {
export type SearchQuery = { export type SearchQuery = {
allOr?: boolean allOr?: boolean
onEmptyFilter?: EmptyFilterOption
string?: { string?: {
[key: string]: string [key: string]: string
} }
@ -48,4 +51,4 @@ export type SearchQuery = {
} }
} }
export type SearchQueryFields = Omit<SearchQuery, "allOr"> export type SearchQueryFields = Omit<SearchQuery, "allOr" | "onEmptyFilter">

View File

@ -179,12 +179,15 @@ export interface AutomationTrigger extends AutomationTriggerSchema {
id: string id: string
} }
export enum AutomationStepStatus {
NO_ITERATIONS = "no_iterations",
}
export enum AutomationStatus { export enum AutomationStatus {
SUCCESS = "success", SUCCESS = "success",
ERROR = "error", ERROR = "error",
STOPPED = "stopped", STOPPED = "stopped",
STOPPED_ERROR = "stopped_error", STOPPED_ERROR = "stopped_error",
NO_ITERATIONS = "no_iterations",
} }
export interface AutomationResults { export interface AutomationResults {

View File

@ -11,6 +11,7 @@ export enum Feature {
SYNC_AUTOMATIONS = "syncAutomations", SYNC_AUTOMATIONS = "syncAutomations",
APP_BUILDERS = "appBuilders", APP_BUILDERS = "appBuilders",
OFFLINE = "offline", OFFLINE = "offline",
USER_ROLE_PUBLIC_API = "userRolePublicApi",
} }
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined } export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }

View File

@ -4,6 +4,7 @@ import { SortType } from "../api"
export interface SearchFilters { export interface SearchFilters {
allOr?: boolean allOr?: boolean
onEmptyFilter?: EmptyFilterOption
string?: { string?: {
[key: string]: string [key: string]: string
} }
@ -99,3 +100,8 @@ export interface SqlQuery {
sql: string sql: string
bindings?: string[] bindings?: string[]
} }
export enum EmptyFilterOption {
RETURN_ALL = "all",
RETURN_NONE = "none",
}

View File

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "es6",
"moduleResolution": "node", "module": "commonjs",
"lib": ["es2020"], "lib": ["es2020"],
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,

View File

@ -102,5 +102,20 @@
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3", "typescript": "4.7.3",
"update-dotenv": "1.1.1" "update-dotenv": "1.1.1"
},
"nx": {
"targets": {
"dev:builder": {
"dependsOn": [
{
"comment": "Required for pro usage when submodule not loaded",
"projects": [
"@budibase/backend-core"
],
"target": "build"
}
]
}
}
} }
} }

View File

@ -31,6 +31,8 @@ function parseIntSafe(number: any) {
} }
const environment = { const environment = {
// features
WORKER_FEATURES: process.env.WORKER_FEATURES,
// auth // auth
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,

View File

@ -0,0 +1,13 @@
import { features } from "@budibase/backend-core"
import env from "./environment"
enum WorkerFeature {}
const featureList: WorkerFeature[] = features.processFeatureEnvVar(
Object.values(WorkerFeature),
env.WORKER_FEATURES
)
export function isFeatureEnabled(feature: WorkerFeature) {
return featureList.includes(feature)
}

View File

@ -28,8 +28,8 @@ describe("datasource validators", () => {
8000 8000
)}`, )}`,
} }
env._set("AWS_ACCESS_KEY_ID", "mocked_key") env._set("AWS_ACCESS_KEY_ID", "mockedkey")
env._set("AWS_SECRET_ACCESS_KEY", "mocked_secret") env._set("AWS_SECRET_ACCESS_KEY", "mockedsecret")
}) })
it("test valid connection string", async () => { it("test valid connection string", async () => {

View File

@ -22,13 +22,16 @@ function runBuild(entry, outfile) {
fs.readFileSync(tsconfig, "utf-8") fs.readFileSync(tsconfig, "utf-8")
) )
if (!fs.existsSync("../pro/src")) { if (
!fs.existsSync("../pro/src") &&
tsconfigPathPluginContent.compilerOptions?.paths
) {
// If we don't have pro, we cannot bundle backend-core. // If we don't have pro, we cannot bundle backend-core.
// Otherwise, the main context will not be shared between libraries // Otherwise, the main context will not be shared between libraries
delete tsconfigPathPluginContent.compilerOptions.paths[ delete tsconfigPathPluginContent?.compilerOptions?.paths?.[
"@budibase/backend-core" "@budibase/backend-core"
] ]
delete tsconfigPathPluginContent.compilerOptions.paths[ delete tsconfigPathPluginContent?.compilerOptions?.paths?.[
"@budibase/backend-core/*" "@budibase/backend-core/*"
] ]
} }