Merge branch 'develop' into chore/fix_os_contributor
This commit is contained in:
commit
5dca30986e
|
@ -69,11 +69,7 @@ jobs:
|
|||
# Run build all the projects
|
||||
- name: Build
|
||||
run: |
|
||||
if ${{ env.USE_NX_AFFECTED }}; then
|
||||
yarn build --since=${{ env.NX_BASE_BRANCH }}
|
||||
else
|
||||
yarn build
|
||||
fi
|
||||
yarn build
|
||||
# Check the types of the projects built via esbuild
|
||||
- name: Check types
|
||||
run: |
|
||||
|
|
|
@ -1,42 +1,31 @@
|
|||
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Budibase Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"--nolazy",
|
||||
"-r",
|
||||
"ts-node/register/transpile-only"
|
||||
],
|
||||
"args": [
|
||||
"${workspaceFolder}/packages/server/src/index.ts"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/server"
|
||||
},
|
||||
{
|
||||
"name": "Budibase Worker",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"--nolazy",
|
||||
"-r",
|
||||
"ts-node/register/transpile-only"
|
||||
],
|
||||
"args": [
|
||||
"${workspaceFolder}/packages/worker/src/index.ts"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/worker"
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Start Budibase",
|
||||
"configurations": ["Budibase Server", "Budibase Worker"]
|
||||
}
|
||||
]
|
||||
}
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Budibase Server",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||
"args": ["${workspaceFolder}/packages/server/src/index.ts"],
|
||||
"cwd": "${workspaceFolder}/packages/server"
|
||||
},
|
||||
{
|
||||
"name": "Budibase Worker",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||
"args": ["${workspaceFolder}/packages/worker/src/index.ts"],
|
||||
"cwd": "${workspaceFolder}/packages/worker"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "Start Budibase",
|
||||
"configurations": ["Budibase Server", "Budibase Worker"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -137,7 +137,6 @@ services:
|
|||
path: /health
|
||||
port: 10000
|
||||
scheme: HTTP
|
||||
enabled: true
|
||||
periodSeconds: 3
|
||||
failureThreshold: 1
|
||||
livenessProbe:
|
||||
|
@ -170,7 +169,6 @@ services:
|
|||
path: /health
|
||||
port: 4002
|
||||
scheme: HTTP
|
||||
enabled: true
|
||||
periodSeconds: 3
|
||||
failureThreshold: 1
|
||||
livenessProbe:
|
||||
|
@ -204,7 +202,6 @@ services:
|
|||
path: /health
|
||||
port: 4003
|
||||
scheme: HTTP
|
||||
enabled: true
|
||||
periodSeconds: 3
|
||||
failureThreshold: 1
|
||||
livenessProbe:
|
||||
|
@ -411,14 +408,12 @@ couchdb:
|
|||
## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes
|
||||
# FOR COUCHDB
|
||||
livenessProbe:
|
||||
enabled: true
|
||||
failureThreshold: 3
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
readinessProbe:
|
||||
enabled: true
|
||||
failureThreshold: 3
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 10
|
||||
|
|
|
@ -5,11 +5,11 @@ ENV COUCHDB_PASSWORD admin
|
|||
EXPOSE 5984
|
||||
|
||||
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://archive.debian.org/debian stretch-backports main' && \
|
||||
apt-add-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ && \
|
||||
apt-get update && apt-get install -y --no-install-recommends adoptopenjdk-8-hotspot && \
|
||||
apt-add-repository 'deb https://packages.adoptium.net/artifactory/deb bullseye main' && \
|
||||
apt-get update && apt-get install -y --no-install-recommends temurin-8-jdk && \
|
||||
rm -rf /var/lib/apt/lists/
|
||||
|
||||
# setup clouseau
|
||||
|
|
|
@ -27,6 +27,7 @@ services:
|
|||
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||
OFFLINE_MODE: ${OFFLINE_MODE}
|
||||
depends_on:
|
||||
- worker-service
|
||||
- redis-service
|
||||
|
@ -54,6 +55,7 @@ services:
|
|||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
OFFLINE_MODE: ${OFFLINE_MODE}
|
||||
depends_on:
|
||||
- redis-service
|
||||
- minio-service
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM node:18-slim as build
|
||||
|
||||
# install node-gyp dependencies
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python3
|
||||
|
||||
# add pin script
|
||||
WORKDIR /
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.9.24-alpha.0",
|
||||
"version": "2.9.30-alpha.5",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import fetch from "node-fetch"
|
||||
import { getCouchInfo } from "./couch"
|
||||
import { SearchFilters, Row } from "@budibase/types"
|
||||
import { createUserIndex } from "./searchIndexes/searchIndexes"
|
||||
import { SearchFilters, Row, EmptyFilterOption } from "@budibase/types"
|
||||
|
||||
const QUERY_START_REGEX = /\d[0-9]*:/g
|
||||
|
||||
|
@ -65,6 +64,7 @@ export class QueryBuilder<T> {
|
|||
this.#index = index
|
||||
this.#query = {
|
||||
allOr: false,
|
||||
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
|
||||
string: {},
|
||||
fuzzy: {},
|
||||
range: {},
|
||||
|
@ -218,6 +218,10 @@ export class QueryBuilder<T> {
|
|||
this.#query.allOr = true
|
||||
}
|
||||
|
||||
setOnEmptyFilter(value: EmptyFilterOption) {
|
||||
this.#query.onEmptyFilter = value
|
||||
}
|
||||
|
||||
handleSpaces(input: string) {
|
||||
if (this.#noEscaping) {
|
||||
return input
|
||||
|
@ -289,8 +293,9 @@ export class QueryBuilder<T> {
|
|||
const builder = this
|
||||
let allOr = this.#query && this.#query.allOr
|
||||
let query = allOr ? "" : "*:*"
|
||||
let allFiltersEmpty = true
|
||||
const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
|
||||
let tableId
|
||||
let tableId: string = ""
|
||||
if (this.#query.equal!.tableId) {
|
||||
tableId = 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") => {
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
return null
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
|
@ -384,6 +389,12 @@ export class QueryBuilder<T> {
|
|||
built += ` ${mode} `
|
||||
}
|
||||
built += expression
|
||||
if (
|
||||
(typeof value !== "string" && value != null) ||
|
||||
(typeof value === "string" && value !== tableId && value !== "")
|
||||
) {
|
||||
allFiltersEmpty = false
|
||||
}
|
||||
}
|
||||
if (opts?.returnBuilt) {
|
||||
return built
|
||||
|
@ -463,6 +474,13 @@ export class QueryBuilder<T> {
|
|||
allOr = false
|
||||
build({ tableId }, equal)
|
||||
}
|
||||
if (allFiltersEmpty) {
|
||||
if (this.#query.onEmptyFilter === EmptyFilterOption.RETURN_NONE) {
|
||||
return ""
|
||||
} else if (this.#query?.allOr) {
|
||||
return query.replace("()", "(*:*)")
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { newid } from "../../docIds/newid"
|
||||
import { getDB } from "../db"
|
||||
import { Database } from "@budibase/types"
|
||||
import { Database, EmptyFilterOption } from "@budibase/types"
|
||||
import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene"
|
||||
|
||||
const INDEX_NAME = "main"
|
||||
|
@ -156,6 +156,76 @@ describe("lucene", () => {
|
|||
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", () => {
|
||||
const skipDbName = `db-${newid()}`
|
||||
let docs: {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import env from "../environment"
|
||||
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.
|
|
@ -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[]
|
||||
}
|
|
@ -6,7 +6,8 @@ export * as roles from "./security/roles"
|
|||
export * as permissions from "./security/permissions"
|
||||
export * as accounts from "./accounts"
|
||||
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 platform from "./platform"
|
||||
export * as auth from "./auth"
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
import env from "../environment"
|
||||
import * as eventHelpers from "./events"
|
||||
import * as accounts from "../accounts"
|
||||
import * as accountSdk from "../accounts"
|
||||
import * as cache from "../cache"
|
||||
import { getIdentity, getTenantId, getGlobalDB } from "../context"
|
||||
import { getGlobalDB, getIdentity, getTenantId } from "../context"
|
||||
import * as dbUtils from "../db"
|
||||
import { EmailUnavailableError, HTTPError } from "../errors"
|
||||
import * as platform from "../platform"
|
||||
import * as sessions from "../security/sessions"
|
||||
import * as usersCore from "./users"
|
||||
import {
|
||||
Account,
|
||||
AllDocsResponse,
|
||||
BulkUserCreated,
|
||||
BulkUserDeleted,
|
||||
isSSOAccount,
|
||||
isSSOUser,
|
||||
RowResponse,
|
||||
SaveUserOpts,
|
||||
User,
|
||||
Account,
|
||||
isSSOUser,
|
||||
isSSOAccount,
|
||||
UserStatus,
|
||||
} from "@budibase/types"
|
||||
import * as accountSdk from "../accounts"
|
||||
import {
|
||||
validateUniqueUser,
|
||||
getAccountHolderFromUserIds,
|
||||
isAdmin,
|
||||
validateUniqueUser,
|
||||
} from "./utils"
|
||||
import { searchExistingEmails } from "./lookup"
|
||||
import { hash } from "../utils"
|
||||
|
@ -179,6 +179,14 @@ export class UserDB {
|
|||
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> {
|
||||
// default booleans to true
|
||||
if (opts.hashPassword == null) {
|
||||
|
|
|
@ -86,6 +86,10 @@ export const useAuditLogs = () => {
|
|||
return useFeature(Feature.AUDIT_LOGS)
|
||||
}
|
||||
|
||||
export const usePublicApiUserRoles = () => {
|
||||
return useFeature(Feature.USER_ROLE_PUBLIC_API)
|
||||
}
|
||||
|
||||
export const useScimIntegration = () => {
|
||||
return useFeature(Feature.SCIM)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import { Select, Label, Stepper } from "@budibase/bbui"
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { getActionProviderComponents } from "builderStore/dataBinding"
|
||||
import { onMount } from "svelte"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
|
||||
$: actionProviders = getActionProviderComponents(
|
||||
$currentAsset,
|
||||
|
@ -51,7 +53,11 @@
|
|||
<Select bind:value={parameters.type} options={typeOptions} />
|
||||
{#if parameters.type === "specific"}
|
||||
<Label small>Number</Label>
|
||||
<Stepper bind:value={parameters.number} />
|
||||
<DrawerBindableInput
|
||||
{bindings}
|
||||
value={parameters.number}
|
||||
on:change={e => (parameters.number = e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import { generate } from "shortid"
|
||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||
import { getFields } from "helpers/searchFields"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
|
@ -35,22 +35,28 @@
|
|||
{ value: "and", label: "Match all filters" },
|
||||
{ value: "or", label: "Match any filter" },
|
||||
]
|
||||
const onEmptyOptions = [
|
||||
{ value: "all", label: "Return all table rows" },
|
||||
{ value: "none", label: "Return no rows" },
|
||||
]
|
||||
|
||||
let rawFilters
|
||||
let matchAny = false
|
||||
let onEmptyFilter = "all"
|
||||
|
||||
$: parseFilters(filters)
|
||||
$: dispatch("change", enrichFilters(rawFilters, matchAny))
|
||||
$: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter))
|
||||
$: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true })
|
||||
$: fieldOptions = enrichedSchemaFields.map(field => field.name) || []
|
||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||
|
||||
// Remove field key prefixes and determine whether to use the "match all"
|
||||
// or "match any" behaviour
|
||||
// Remove field key prefixes and determine which behaviours to use
|
||||
const parseFilters = filters => {
|
||||
matchAny = filters?.find(filter => filter.operator === "allOr") != null
|
||||
onEmptyFilter =
|
||||
filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all"
|
||||
rawFilters = (filters || [])
|
||||
.filter(filter => filter.operator !== "allOr")
|
||||
.filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter)
|
||||
.map(filter => {
|
||||
const { field } = filter
|
||||
let newFilter = { ...filter }
|
||||
|
@ -64,9 +70,18 @@
|
|||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
parseFilters(filters)
|
||||
rawFilters.forEach(filter => {
|
||||
filter.type =
|
||||
schemaFields.find(field => field.name === filter.field)?.type ||
|
||||
filter.type
|
||||
})
|
||||
})
|
||||
|
||||
// Add field key prefixes and a special metadata filter object to indicate
|
||||
// whether to use the "match all" or "match any" behaviour
|
||||
const enrichFilters = (rawFilters, matchAny) => {
|
||||
// how to handle filter behaviour
|
||||
const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => {
|
||||
let count = 1
|
||||
return rawFilters
|
||||
.filter(filter => filter.field)
|
||||
|
@ -75,6 +90,7 @@
|
|||
field: `${count++}:${filter.field}`,
|
||||
}))
|
||||
.concat(matchAny ? [{ operator: "allOr" }] : [])
|
||||
.concat([{ onEmptyFilter }])
|
||||
}
|
||||
|
||||
const addFilter = () => {
|
||||
|
@ -186,6 +202,17 @@
|
|||
on:change={e => (matchAny = e.detail === "or")}
|
||||
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 class="filter-label">
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
|
||||
$: isError = !value || value.toLowerCase() === "error"
|
||||
$: isStoppedError = value?.toLowerCase() === "stopped_error"
|
||||
$: isStopped = value?.toLowerCase() === "stopped" || isStoppedError
|
||||
$: info = getInfo(isError, isStopped)
|
||||
$: isStopped = value?.toLowerCase() === "stopped"
|
||||
$: info = getInfo(isError, isStopped, isStoppedError)
|
||||
|
||||
const getInfo = (error, stopped) => {
|
||||
if (error) {
|
||||
function getInfo(error, stopped, stoppedError) {
|
||||
if (stoppedError) {
|
||||
return { color: "red", message: "Stopped - Error" }
|
||||
} else if (error) {
|
||||
return { color: "red", message: "Error" }
|
||||
} else if (stopped) {
|
||||
return { color: "yellow", message: "Stopped" }
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
|
||||
const ERROR = "error",
|
||||
SUCCESS = "success",
|
||||
STOPPED = "stopped"
|
||||
STOPPED = "stopped",
|
||||
STOPPED_ERROR = "stopped_error"
|
||||
const sidePanel = getContext("side-panel")
|
||||
|
||||
let pageInfo = createPaginationStore()
|
||||
|
@ -52,6 +53,7 @@
|
|||
{ value: SUCCESS, label: "Success" },
|
||||
{ value: ERROR, label: "Error" },
|
||||
{ value: STOPPED, label: "Stopped" },
|
||||
{ value: STOPPED_ERROR, label: "Stopped - Error" },
|
||||
]
|
||||
|
||||
const runHistorySchema = {
|
||||
|
|
|
@ -2408,6 +2408,13 @@
|
|||
"label": "Disabled",
|
||||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Initial form step",
|
||||
"key": "initialFormStep",
|
||||
"defaultValue": 1
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
|
@ -2445,6 +2452,7 @@
|
|||
"name": "Form Step",
|
||||
"icon": "AssetsAdded",
|
||||
"hasChildren": true,
|
||||
"requiredAncestors": ["form"],
|
||||
"illegalChildren": ["section", "form", "formstep", "formblock"],
|
||||
"styles": ["size"],
|
||||
"size": {
|
||||
|
@ -2464,6 +2472,7 @@
|
|||
"fieldgroup": {
|
||||
"name": "Field Group",
|
||||
"icon": "Group",
|
||||
"requiredAncestors": ["form"],
|
||||
"illegalChildren": ["section"],
|
||||
"styles": ["size"],
|
||||
"hasChildren": true,
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let size
|
||||
export let disabled = false
|
||||
export let actionType = "Create"
|
||||
export let initialFormStep = 1
|
||||
|
||||
// Not exposed as a builder setting. Used internally to disable validation
|
||||
// for fields rendered in things like search blocks.
|
||||
|
@ -21,10 +22,18 @@
|
|||
const context = getContext("context")
|
||||
const { API, fetchDatasourceSchema } = getContext("sdk")
|
||||
|
||||
const getInitialFormStep = () => {
|
||||
const parsedFormStep = parseInt(initialFormStep)
|
||||
if (isNaN(parsedFormStep)) {
|
||||
return 1
|
||||
}
|
||||
return parsedFormStep
|
||||
}
|
||||
|
||||
let loaded = false
|
||||
let schema
|
||||
let table
|
||||
let currentStep = writable(1)
|
||||
let currentStep = writable(getInitialFormStep())
|
||||
|
||||
$: fetchSchema(dataSource)
|
||||
$: schemaKey = generateSchemaKey(schema)
|
||||
|
|
|
@ -250,7 +250,7 @@
|
|||
} else if (type === "first") {
|
||||
currentStep.set(1)
|
||||
} else if (type === "specific" && number && !isNaN(number)) {
|
||||
currentStep.set(number)
|
||||
currentStep.set(parseInt(number))
|
||||
}
|
||||
},
|
||||
setStep: step => {
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 9b9c8cc08f271bfc5dd401860f344f6eb336ab35
|
||||
Subproject commit 06a28b18a409cc12e9e8a5b69a094adcc6babd5a
|
|
@ -18,7 +18,7 @@ ENV TOP_LEVEL_PATH=/
|
|||
|
||||
# handle node-gyp
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends g++ make python
|
||||
&& apt-get install -y --no-install-recommends g++ make python3
|
||||
RUN yarn global add pm2
|
||||
|
||||
# Install client for oracle datasource
|
||||
|
|
|
@ -12,6 +12,7 @@ const baseConfig: Config.InitialProjectOptions = {
|
|||
},
|
||||
moduleNameMapper: {
|
||||
"@budibase/backend-core/(.*)": "<rootDir>/../backend-core/$1",
|
||||
"@budibase/shared-core/(.*)": "<rootDir>/../shared-core/$1",
|
||||
"@budibase/backend-core": "<rootDir>/../backend-core/src",
|
||||
"@budibase/shared-core": "<rootDir>/../shared-core/src",
|
||||
"@budibase/types": "<rootDir>/../types/src",
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
"memorystream": "0.3.1",
|
||||
"mongodb": "5.7",
|
||||
"mssql": "9.1.1",
|
||||
"mysql2": "2.3.3",
|
||||
"mysql2": "3.5.2",
|
||||
"node-fetch": "2.6.7",
|
||||
"object-sizeof": "2.6.1",
|
||||
"open": "8.4.0",
|
||||
|
|
|
@ -1521,7 +1521,7 @@
|
|||
"type": "boolean"
|
||||
},
|
||||
"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",
|
||||
"properties": {
|
||||
"global": {
|
||||
|
@ -1531,7 +1531,7 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"properties": {
|
||||
"global": {
|
||||
|
@ -1541,7 +1541,7 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
|
@ -1588,7 +1588,7 @@
|
|||
"type": "boolean"
|
||||
},
|
||||
"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",
|
||||
"properties": {
|
||||
"global": {
|
||||
|
@ -1598,7 +1598,7 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"properties": {
|
||||
"global": {
|
||||
|
@ -1608,7 +1608,7 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
|
@ -1667,7 +1667,7 @@
|
|||
"type": "boolean"
|
||||
},
|
||||
"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",
|
||||
"properties": {
|
||||
"global": {
|
||||
|
@ -1677,7 +1677,7 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"properties": {
|
||||
"global": {
|
||||
|
@ -1687,7 +1687,7 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
|
@ -1833,6 +1833,137 @@
|
|||
"required": [
|
||||
"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": {
|
||||
"post": {
|
||||
"operationId": "rowCreate",
|
||||
|
|
|
@ -1297,7 +1297,8 @@ components:
|
|||
login.
|
||||
type: boolean
|
||||
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
|
||||
properties:
|
||||
global:
|
||||
|
@ -1305,7 +1306,8 @@ components:
|
|||
system.
|
||||
type: boolean
|
||||
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
|
||||
properties:
|
||||
global:
|
||||
|
@ -1313,7 +1315,8 @@ components:
|
|||
type: boolean
|
||||
roles:
|
||||
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
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
@ -1352,7 +1355,8 @@ components:
|
|||
login.
|
||||
type: boolean
|
||||
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
|
||||
properties:
|
||||
global:
|
||||
|
@ -1360,7 +1364,8 @@ components:
|
|||
system.
|
||||
type: boolean
|
||||
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
|
||||
properties:
|
||||
global:
|
||||
|
@ -1368,7 +1373,8 @@ components:
|
|||
type: boolean
|
||||
roles:
|
||||
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
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
@ -1415,7 +1421,8 @@ components:
|
|||
login.
|
||||
type: boolean
|
||||
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
|
||||
properties:
|
||||
global:
|
||||
|
@ -1423,7 +1430,8 @@ components:
|
|||
system.
|
||||
type: boolean
|
||||
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
|
||||
properties:
|
||||
global:
|
||||
|
@ -1431,7 +1439,8 @@ components:
|
|||
type: boolean
|
||||
roles:
|
||||
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
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
@ -1547,6 +1556,99 @@ components:
|
|||
insensitive starts with match.
|
||||
required:
|
||||
- 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:
|
||||
- ApiKeyAuth: []
|
||||
paths:
|
||||
|
@ -1757,6 +1859,46 @@ paths:
|
|||
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":
|
||||
post:
|
||||
operationId: rowCreate
|
||||
|
|
|
@ -5,6 +5,7 @@ import query from "./query"
|
|||
import user from "./user"
|
||||
import metrics from "./metrics"
|
||||
import misc from "./misc"
|
||||
import roles from "./roles"
|
||||
|
||||
export const examples = {
|
||||
...application.getExamples(),
|
||||
|
@ -23,4 +24,5 @@ export const schemas = {
|
|||
...query.getSchemas(),
|
||||
...user.getSchemas(),
|
||||
...misc.getSchemas(),
|
||||
...roles.getSchemas(),
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
|
@ -58,7 +58,8 @@ const userSchema = object(
|
|||
type: "boolean",
|
||||
},
|
||||
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",
|
||||
properties: {
|
||||
global: {
|
||||
|
@ -69,7 +70,8 @@ const userSchema = object(
|
|||
},
|
||||
},
|
||||
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",
|
||||
properties: {
|
||||
global: {
|
||||
|
@ -81,7 +83,7 @@ const userSchema = object(
|
|||
},
|
||||
roles: {
|
||||
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",
|
||||
additionalProperties: {
|
||||
type: "string",
|
||||
|
|
|
@ -77,18 +77,19 @@ async function initDeployedApp(prodAppId: any) {
|
|||
)
|
||||
).rows.map((row: any) => row.doc)
|
||||
await clearMetadata()
|
||||
console.log("You have " + automations.length + " automations")
|
||||
const { count } = await disableAllCrons(prodAppId)
|
||||
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) {
|
||||
promises.push(enableCronTrigger(prodAppId, automation))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
console.log("Enabled cron triggers for deployed app..")
|
||||
// sync the automations back to the dev DB - since there is now cron
|
||||
const results = await Promise.all(promises)
|
||||
const enabledCount = results
|
||||
.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
|
||||
await sdk.applications.syncApp(dbCore.getDevAppID(prodAppId), {
|
||||
automationOnly: true,
|
||||
|
|
|
@ -3,6 +3,8 @@ import { search as stringSearch, addRev } from "./utils"
|
|||
import * as controller from "../application"
|
||||
import * as deployController from "../deploy"
|
||||
import { Application } from "../../../definitions/common"
|
||||
import { UserCtx } from "@budibase/types"
|
||||
import { Next } from "koa"
|
||||
|
||||
function fixAppID(app: Application, params: any) {
|
||||
if (!params) {
|
||||
|
@ -14,7 +16,7 @@ function fixAppID(app: Application, params: any) {
|
|||
return app
|
||||
}
|
||||
|
||||
async function setResponseApp(ctx: any) {
|
||||
async function setResponseApp(ctx: UserCtx) {
|
||||
const appId = ctx.body?.appId
|
||||
if (appId && (!ctx.params || !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 apps = await dbCore.getAllApps({ all: true })
|
||||
ctx.body = stringSearch(apps, name)
|
||||
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) {
|
||||
ctx.request.body = {
|
||||
useTemplate: false,
|
||||
|
@ -47,14 +49,14 @@ export async function create(ctx: any, next: any) {
|
|||
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 setResponseApp(ctx)
|
||||
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))
|
||||
await context.doInAppContext(ctx.params.appId, async () => {
|
||||
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 () => {
|
||||
// get the app before deleting it
|
||||
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 controller.unpublish(ctx)
|
||||
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 deployController.publishApp(ctx)
|
||||
await next()
|
||||
|
|
|
@ -16,6 +16,10 @@ export type CreateRowParams = components["schemas"]["row"]
|
|||
export type User = components["schemas"]["userOutput"]["data"]
|
||||
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 =
|
||||
| components["schemas"]["nameSearch"]
|
||||
| components["schemas"]["rowSearch"]
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { search as stringSearch } from "./utils"
|
||||
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)
|
||||
const { name } = ctx.request.body
|
||||
ctx.body = stringSearch(ctx.body, name)
|
||||
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"
|
||||
await queryController.executeV2(ctx)
|
||||
await next()
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import * as rowController from "../row"
|
||||
import { addRev } from "./utils"
|
||||
import { Row } from "@budibase/types"
|
||||
import { Row, UserCtx } from "@budibase/types"
|
||||
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
|
||||
// the call to be correct
|
||||
|
@ -21,7 +22,7 @@ export function fixRow(row: Row, params: any) {
|
|||
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
|
||||
// update the body to the correct format of the internal search
|
||||
if (!sort) {
|
||||
|
@ -40,25 +41,25 @@ export async function search(ctx: any, next: any) {
|
|||
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)
|
||||
await rowController.save(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function read(ctx: any, next: any) {
|
||||
export async function read(ctx: UserCtx, next: Next) {
|
||||
await rowController.fetchEnrichedRow(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function update(ctx: any, next: any) {
|
||||
export async function update(ctx: UserCtx, next: Next) {
|
||||
const { tableId } = ctx.params
|
||||
ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params), tableId)
|
||||
await rowController.save(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function destroy(ctx: any, next: any) {
|
||||
export async function destroy(ctx: UserCtx, next: Next) {
|
||||
const { tableId } = ctx.params
|
||||
// set the body as expected, with the _id and _rev fields
|
||||
ctx.request.body = await addRev(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { search as stringSearch, addRev } from "./utils"
|
||||
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) {
|
||||
if (!params || !table) {
|
||||
|
@ -15,24 +16,24 @@ function fixTable(table: Table, params: any) {
|
|||
return table
|
||||
}
|
||||
|
||||
export async function search(ctx: any, next: any) {
|
||||
export async function search(ctx: UserCtx, next: Next) {
|
||||
const { name } = ctx.request.body
|
||||
await controller.fetch(ctx)
|
||||
ctx.body = stringSearch(ctx.body, name)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function create(ctx: any, next: any) {
|
||||
export async function create(ctx: UserCtx, next: Next) {
|
||||
await controller.save(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function read(ctx: any, next: any) {
|
||||
export async function read(ctx: UserCtx, next: Next) {
|
||||
await controller.find(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function update(ctx: any, next: any) {
|
||||
export async function update(ctx: UserCtx, next: Next) {
|
||||
ctx.request.body = await addRev(
|
||||
fixTable(ctx.request.body, ctx.params),
|
||||
ctx.params.tableId
|
||||
|
@ -41,7 +42,7 @@ export async function update(ctx: any, next: any) {
|
|||
await next()
|
||||
}
|
||||
|
||||
export async function destroy(ctx: any, next: any) {
|
||||
export async function destroy(ctx: UserCtx, next: Next) {
|
||||
await controller.destroy(ctx)
|
||||
ctx.body = ctx.table
|
||||
await next()
|
||||
|
|
|
@ -7,16 +7,18 @@ import {
|
|||
import { publicApiUserFix } from "../../../utilities/users"
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
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 globalUserId = dbCore.getGlobalIDFromUserMetadataID(loggedInId!)
|
||||
// check both just incase
|
||||
return globalUserId === user._id || loggedInId === user._id
|
||||
}
|
||||
|
||||
function getUser(ctx: BBContext, userId?: string) {
|
||||
function getUser(ctx: UserCtx, userId?: string) {
|
||||
if (userId) {
|
||||
ctx.params = { userId }
|
||||
} else if (!ctx.params?.userId) {
|
||||
|
@ -25,42 +27,38 @@ function getUser(ctx: BBContext, userId?: string) {
|
|||
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 users = await allGlobalUsers(ctx)
|
||||
ctx.body = stringSearch(users, name, "email")
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function create(ctx: BBContext, next: any) {
|
||||
const response = await saveGlobalUser(publicApiUserFix(ctx))
|
||||
export async function create(ctx: UserCtx, next: Next) {
|
||||
ctx = publicApiUserFix(await sdk.publicApi.users.roleCheck(ctx))
|
||||
const response = await saveGlobalUser(ctx)
|
||||
ctx.body = await getUser(ctx, response._id)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function read(ctx: BBContext, next: any) {
|
||||
export async function read(ctx: UserCtx, next: Next) {
|
||||
ctx.body = await readGlobalUser(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function update(ctx: BBContext, next: any) {
|
||||
export async function update(ctx: UserCtx, next: Next) {
|
||||
const user = await readGlobalUser(ctx)
|
||||
ctx.request.body = {
|
||||
...ctx.request.body,
|
||||
_rev: user._rev,
|
||||
}
|
||||
// disallow updating your own role - always overwrite with DB roles
|
||||
if (isLoggedInUser(ctx, user)) {
|
||||
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 = publicApiUserFix(await sdk.publicApi.users.roleCheck(ctx, user))
|
||||
const response = await saveGlobalUser(ctx)
|
||||
ctx.body = await getUser(ctx, response._id)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function destroy(ctx: BBContext, next: any) {
|
||||
export async function destroy(ctx: UserCtx, next: Next) {
|
||||
const user = await getUser(ctx)
|
||||
// disallow deleting yourself
|
||||
if (isLoggedInUser(ctx, user)) {
|
||||
|
|
|
@ -127,7 +127,7 @@ export async function preview(ctx: any) {
|
|||
const query = ctx.request.body
|
||||
// preview may not have a queryId as it hasn't been saved, but if it does
|
||||
// this stops dynamic variables from calling the same query
|
||||
const { fields, parameters, queryVerb, transformer, queryId } = query
|
||||
const { fields, parameters, queryVerb, transformer, queryId, schema } = query
|
||||
|
||||
const authConfigCtx: any = getAuthConfig(ctx)
|
||||
|
||||
|
@ -140,6 +140,7 @@ export async function preview(ctx: any) {
|
|||
parameters,
|
||||
transformer,
|
||||
queryId,
|
||||
schema,
|
||||
// have to pass down to the thread runner - can't put into context now
|
||||
environmentVariables: envVars,
|
||||
ctx: {
|
||||
|
@ -235,6 +236,7 @@ async function execute(
|
|||
user: ctx.user,
|
||||
auth: { ...authConfigCtx },
|
||||
},
|
||||
schema: query.schema,
|
||||
}
|
||||
const runFn = () => Runner.run(inputs)
|
||||
|
||||
|
|
|
@ -13,9 +13,11 @@ import {
|
|||
Row,
|
||||
Table,
|
||||
UserCtx,
|
||||
EmptyFilterOption,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
import * as utils from "./utils"
|
||||
import { dataFilters } from "@budibase/shared-core"
|
||||
|
||||
export async function handleRequest(
|
||||
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(
|
||||
opts || {}
|
||||
)
|
||||
|
|
|
@ -22,9 +22,12 @@ import {
|
|||
QueryJson,
|
||||
RelationshipType,
|
||||
RenameColumn,
|
||||
SaveTableRequest,
|
||||
SaveTableResponse,
|
||||
Table,
|
||||
TableRequest,
|
||||
UserCtx,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
import { builderSocket } from "../../../websockets"
|
||||
|
@ -198,8 +201,8 @@ function isRelationshipSetup(column: FieldSchema) {
|
|||
return column.foreignKey || column.through
|
||||
}
|
||||
|
||||
export async function save(ctx: UserCtx) {
|
||||
const inputs: TableRequest = ctx.request.body
|
||||
export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
||||
const inputs = ctx.request.body
|
||||
const renamed = inputs?._rename
|
||||
// can't do this right now
|
||||
delete inputs.rows
|
||||
|
@ -215,7 +218,7 @@ export async function save(ctx: UserCtx) {
|
|||
...inputs,
|
||||
}
|
||||
|
||||
let oldTable
|
||||
let oldTable: Table | undefined
|
||||
if (ctx.request.body && ctx.request.body._id) {
|
||||
oldTable = await sdk.tables.getTable(ctx.request.body._id)
|
||||
}
|
||||
|
@ -224,6 +227,17 @@ export async function save(ctx: UserCtx) {
|
|||
ctx.throw(400, "A column type has changed.")
|
||||
}
|
||||
|
||||
for (let view in tableToSave.views) {
|
||||
const tableView = tableToSave.views[view]
|
||||
if (!tableView || !sdk.views.isV2(tableView)) continue
|
||||
|
||||
tableToSave.views[view] = sdk.views.syncSchema(
|
||||
oldTable!.views![view] as ViewV2,
|
||||
tableToSave.schema,
|
||||
renamed
|
||||
)
|
||||
}
|
||||
|
||||
const db = context.getAppDB()
|
||||
const datasource = await sdk.datasources.get(datasourceId)
|
||||
if (!datasource.entities) {
|
||||
|
|
|
@ -9,6 +9,8 @@ import { isExternalTable, isSQL } from "../../../integrations/utils"
|
|||
import { events } from "@budibase/backend-core"
|
||||
import {
|
||||
FetchTablesResponse,
|
||||
SaveTableResponse,
|
||||
SaveTableRequest,
|
||||
Table,
|
||||
TableResponse,
|
||||
UserCtx,
|
||||
|
@ -60,7 +62,7 @@ export async function find(ctx: UserCtx<void, TableResponse>) {
|
|||
ctx.body = sdk.tables.enrichViewSchemas(table)
|
||||
}
|
||||
|
||||
export async function save(ctx: UserCtx) {
|
||||
export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
||||
const appId = ctx.appId
|
||||
const table = ctx.request.body
|
||||
const isImport = table.rows
|
||||
|
|
|
@ -9,7 +9,15 @@ import {
|
|||
fixAutoColumnSubType,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
import { runStaticFormulaChecks } from "./bulkFormula"
|
||||
import { Table } from "@budibase/types"
|
||||
import {
|
||||
SaveTableRequest,
|
||||
SaveTableResponse,
|
||||
Table,
|
||||
TableRequest,
|
||||
UserCtx,
|
||||
ViewStatisticsSchema,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
@ -33,10 +41,10 @@ function checkAutoColumns(table: Table, oldTable?: Table) {
|
|||
return table
|
||||
}
|
||||
|
||||
export async function save(ctx: any) {
|
||||
export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
||||
const db = context.getAppDB()
|
||||
const { rows, ...rest } = ctx.request.body
|
||||
let tableToSave = {
|
||||
let tableToSave: TableRequest = {
|
||||
type: "table",
|
||||
_id: generateTableID(),
|
||||
views: {},
|
||||
|
@ -44,7 +52,7 @@ export async function save(ctx: any) {
|
|||
}
|
||||
|
||||
// if the table obj had an _id then it will have been retrieved
|
||||
let oldTable
|
||||
let oldTable: Table | undefined
|
||||
if (ctx.request.body && ctx.request.body._id) {
|
||||
oldTable = await sdk.tables.getTable(ctx.request.body._id)
|
||||
}
|
||||
|
@ -80,7 +88,7 @@ export async function save(ctx: any) {
|
|||
let { _rename } = tableToSave
|
||||
/* istanbul ignore next */
|
||||
if (_rename && _rename.old === _rename.updated) {
|
||||
_rename = null
|
||||
_rename = undefined
|
||||
delete tableToSave._rename
|
||||
}
|
||||
|
||||
|
@ -97,7 +105,20 @@ export async function save(ctx: any) {
|
|||
const tableView = tableToSave.views[view]
|
||||
if (!tableView) continue
|
||||
|
||||
if (tableView.schema.group || tableView.schema.field) continue
|
||||
if (sdk.views.isV2(tableView)) {
|
||||
tableToSave.views[view] = sdk.views.syncSchema(
|
||||
oldTable!.views![view] as ViewV2,
|
||||
tableToSave.schema,
|
||||
_rename
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
(tableView.schema as ViewStatisticsSchema).group ||
|
||||
tableView.schema.field
|
||||
)
|
||||
continue
|
||||
tableView.schema = tableToSave.schema
|
||||
}
|
||||
|
||||
|
@ -112,7 +133,7 @@ export async function save(ctx: any) {
|
|||
tableToSave._rev = linkResp._rev
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(400, err)
|
||||
ctx.throw(400, err as string)
|
||||
}
|
||||
|
||||
// don't perform any updates until relationships have been
|
||||
|
|
|
@ -418,7 +418,7 @@ export function areSwitchableTypes(type1: any, type2: any) {
|
|||
return false
|
||||
}
|
||||
|
||||
export function hasTypeChanged(table: any, oldTable: any) {
|
||||
export function hasTypeChanged(table: Table, oldTable: Table | undefined) {
|
||||
if (!oldTable) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -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: [] }
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -1,120 +1,41 @@
|
|||
import Sentry from "@sentry/node"
|
||||
|
||||
if (process.env.DD_APM_ENABLED) {
|
||||
require("./ddApm")
|
||||
}
|
||||
|
||||
// need to load environment first
|
||||
import env from "./environment"
|
||||
|
||||
import { ExtendableContext } from "koa"
|
||||
import * as db from "./db"
|
||||
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 {
|
||||
events,
|
||||
logging,
|
||||
middleware,
|
||||
timers,
|
||||
env as coreEnv,
|
||||
} from "@budibase/backend-core"
|
||||
import { env as coreEnv } from "@budibase/backend-core"
|
||||
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"
|
||||
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")
|
||||
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()) {
|
||||
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
|
||||
async function start() {
|
||||
if (apiEnabled()) {
|
||||
const koa = createKoaApp()
|
||||
app = koa.app
|
||||
server = koa.server
|
||||
}
|
||||
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)
|
||||
})
|
||||
|
||||
const shutdown = () => {
|
||||
server.close()
|
||||
// @ts-ignore
|
||||
server.destroy()
|
||||
if (env.isProd()) {
|
||||
env._set("NODE_ENV", "production")
|
||||
Sentry.init()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
start().catch(err => {
|
||||
console.error(`Failed server startup - ${err.message}`)
|
||||
})
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
shutdown()
|
||||
})
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
shutdown()
|
||||
})
|
||||
export function getServer() {
|
||||
return server
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { processEvent } from "./utils"
|
|||
import { automationQueue } from "./bullboard"
|
||||
import { rebootTrigger } from "./triggers"
|
||||
import BullQueue from "bull"
|
||||
import { automationsEnabled } from "../features"
|
||||
|
||||
export { automationQueue } 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
|
||||
*/
|
||||
export async function init() {
|
||||
if (!automationsEnabled()) {
|
||||
return
|
||||
}
|
||||
// this promise will not complete
|
||||
const promise = automationQueue.process(async job => {
|
||||
await processEvent(job)
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
AutomationStepInput,
|
||||
AutomationStepSchema,
|
||||
AutomationStepType,
|
||||
EmptyFilterOption,
|
||||
SearchFilters,
|
||||
Table,
|
||||
} from "@budibase/types"
|
||||
|
@ -26,16 +27,6 @@ const SortOrderPretty = {
|
|||
[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 = {
|
||||
description: "Query rows from the database",
|
||||
icon: "Search",
|
||||
|
@ -77,12 +68,6 @@ export const definition: AutomationStepSchema = {
|
|||
title: "Limit",
|
||||
customType: AutomationCustomIOType.QUERY_LIMIT,
|
||||
},
|
||||
onEmptyFilter: {
|
||||
pretty: Object.values(EmptyFilterOptionPretty),
|
||||
enum: Object.values(EmptyFilterOption),
|
||||
type: AutomationIOType.STRING,
|
||||
title: "When Filter Empty",
|
||||
},
|
||||
},
|
||||
required: ["tableId"],
|
||||
},
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
const setup = require("./utilities")
|
||||
const { FilterConditions } = require("../steps/filter")
|
||||
import * as setup from "./utilities"
|
||||
import { FilterConditions } from "../steps/filter"
|
||||
|
||||
describe("test the filter logic", () => {
|
||||
async function checkFilter(field, condition, value, pass = true) {
|
||||
let res = await setup.runStep(setup.actions.FILTER.stepId,
|
||||
{ field, condition, value }
|
||||
)
|
||||
async function checkFilter(
|
||||
field: any,
|
||||
condition: string,
|
||||
value: any,
|
||||
pass = true
|
||||
) {
|
||||
let res = await setup.runStep(setup.actions.FILTER.stepId, {
|
||||
field,
|
||||
condition,
|
||||
value,
|
||||
})
|
||||
expect(res.result).toEqual(pass)
|
||||
expect(res.success).toEqual(true)
|
||||
}
|
||||
|
@ -36,9 +43,9 @@ describe("test the filter logic", () => {
|
|||
|
||||
it("check date coercion", async () => {
|
||||
await checkFilter(
|
||||
(new Date()).toISOString(),
|
||||
new Date().toISOString(),
|
||||
FilterConditions.GREATER_THAN,
|
||||
(new Date(-10000)).toISOString(),
|
||||
new Date(-10000).toISOString(),
|
||||
true
|
||||
)
|
||||
})
|
|
@ -15,9 +15,13 @@ import {
|
|||
WebhookActionType,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../sdk"
|
||||
import { automationsEnabled } from "../features"
|
||||
|
||||
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(
|
||||
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) {
|
||||
|
@ -169,6 +174,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
|
|||
const needsCreated =
|
||||
!sdk.automations.isReboot(automation) &&
|
||||
!sdk.automations.disabled(automation)
|
||||
let enabled = false
|
||||
|
||||
// need to create cron job
|
||||
if (validCron && needsCreated) {
|
||||
|
@ -191,8 +197,9 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
|
|||
automation._id = response.id
|
||||
automation._rev = response.rev
|
||||
})
|
||||
enabled = true
|
||||
}
|
||||
return automation
|
||||
return { enabled, automation }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -34,6 +34,14 @@ export interface paths {
|
|||
/** Based on query properties (currently only name) search for queries. */
|
||||
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": {
|
||||
/** Creates a row within the specified table. */
|
||||
post: operations["rowCreate"];
|
||||
|
@ -256,7 +264,8 @@ export interface components {
|
|||
| "auto"
|
||||
| "json"
|
||||
| "internal"
|
||||
| "barcodeqr";
|
||||
| "barcodeqr"
|
||||
| "bigint";
|
||||
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */
|
||||
constraints?: {
|
||||
/** @enum {string} */
|
||||
|
@ -362,7 +371,8 @@ export interface components {
|
|||
| "auto"
|
||||
| "json"
|
||||
| "internal"
|
||||
| "barcodeqr";
|
||||
| "barcodeqr"
|
||||
| "bigint";
|
||||
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */
|
||||
constraints?: {
|
||||
/** @enum {string} */
|
||||
|
@ -470,7 +480,8 @@ export interface components {
|
|||
| "auto"
|
||||
| "json"
|
||||
| "internal"
|
||||
| "barcodeqr";
|
||||
| "barcodeqr"
|
||||
| "bigint";
|
||||
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */
|
||||
constraints?: {
|
||||
/** @enum {string} */
|
||||
|
@ -577,17 +588,17 @@ export interface components {
|
|||
lastName?: string;
|
||||
/** @description If set to true forces the user to reset their password on first login. */
|
||||
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?: {
|
||||
/** @description If set to true the user will be able to build any app in the system. */
|
||||
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?: {
|
||||
/** @description If set to true the user will be able to administrate the system. */
|
||||
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 };
|
||||
};
|
||||
userOutput: {
|
||||
|
@ -607,17 +618,17 @@ export interface components {
|
|||
lastName?: string;
|
||||
/** @description If set to true forces the user to reset their password on first login. */
|
||||
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?: {
|
||||
/** @description If set to true the user will be able to build any app in the system. */
|
||||
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?: {
|
||||
/** @description If set to true the user will be able to administrate the system. */
|
||||
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 };
|
||||
/** @description The ID of the user. */
|
||||
_id: string;
|
||||
|
@ -640,17 +651,17 @@ export interface components {
|
|||
lastName?: string;
|
||||
/** @description If set to true forces the user to reset their password on first login. */
|
||||
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?: {
|
||||
/** @description If set to true the user will be able to build any app in the system. */
|
||||
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?: {
|
||||
/** @description If set to true the user will be able to administrate the system. */
|
||||
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 };
|
||||
/** @description The ID of the user. */
|
||||
_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. */
|
||||
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: {
|
||||
/** @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. */
|
||||
rowCreate: {
|
||||
parameters: {
|
||||
|
|
|
@ -38,6 +38,8 @@ function parseIntSafe(number?: string) {
|
|||
}
|
||||
|
||||
const environment = {
|
||||
// features
|
||||
APP_FEATURES: process.env.APP_FEATURES,
|
||||
// important - prefer app port to generic port
|
||||
PORT: process.env.APP_PORT || process.env.PORT,
|
||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -341,10 +341,10 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|||
}
|
||||
}
|
||||
|
||||
getDefinitionSQL(tableName: string) {
|
||||
getDefinitionSQL(tableName: string, schemaName: string) {
|
||||
return `select *
|
||||
from INFORMATION_SCHEMA.COLUMNS
|
||||
where TABLE_NAME='${tableName}'`
|
||||
where TABLE_NAME='${tableName}' AND TABLE_SCHEMA='${schemaName}'`
|
||||
}
|
||||
|
||||
getConstraintsSQL(tableName: string) {
|
||||
|
@ -388,16 +388,18 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|||
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
|
||||
.filter((record: any) => record.TABLE_SCHEMA === schema)
|
||||
.filter((record: any) => record.TABLE_SCHEMA === schemaName)
|
||||
.map((record: any) => record.TABLE_NAME)
|
||||
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
|
||||
|
||||
const tables: Record<string, ExternalTable> = {}
|
||||
for (let tableName of tableNames) {
|
||||
// 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
|
||||
const constraints = await this.runSQL(this.getConstraintsSQL(tableName))
|
||||
// find the computed and identity columns (auto columns)
|
||||
|
|
|
@ -93,6 +93,21 @@ const SCHEMA: Integration = {
|
|||
},
|
||||
}
|
||||
|
||||
const defaultTypeCasting = function (field: any, next: any) {
|
||||
if (
|
||||
field.type == "DATETIME" ||
|
||||
field.type === "DATE" ||
|
||||
field.type === "TIMESTAMP" ||
|
||||
field.type === "LONGLONG"
|
||||
) {
|
||||
return field.string()
|
||||
}
|
||||
if (field.type === "BIT" && field.length === 1) {
|
||||
return field.buffer()?.[0]
|
||||
}
|
||||
return next()
|
||||
}
|
||||
|
||||
export function bindingTypeCoerce(bindings: any[]) {
|
||||
for (let i = 0; i < bindings.length; i++) {
|
||||
const binding = bindings[i]
|
||||
|
@ -147,21 +162,8 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
|||
delete config.rejectUnauthorized
|
||||
this.config = {
|
||||
...config,
|
||||
typeCast: defaultTypeCasting,
|
||||
multipleStatements: true,
|
||||
typeCast: function (field: any, next: any) {
|
||||
if (
|
||||
field.type == "DATETIME" ||
|
||||
field.type === "DATE" ||
|
||||
field.type === "TIMESTAMP" ||
|
||||
field.type === "LONGLONG"
|
||||
) {
|
||||
return field.string()
|
||||
}
|
||||
if (field.type === "BIT" && field.length === 1) {
|
||||
return field.buffer()?.[0]
|
||||
}
|
||||
return next()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,6 +196,37 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
|||
return `concat(${parts.join(", ")})`
|
||||
}
|
||||
|
||||
defineTypeCastingFromSchema(schema: {
|
||||
[key: string]: { name: string; type: string }
|
||||
}): void {
|
||||
if (!schema) {
|
||||
return
|
||||
}
|
||||
this.config.typeCast = function (field: any, next: any) {
|
||||
if (schema[field.name]?.name === field.name) {
|
||||
if (["LONGLONG", "NEWDECIMAL", "DECIMAL"].includes(field.type)) {
|
||||
if (schema[field.name]?.type === "number") {
|
||||
const value = field.string()
|
||||
return value ? Number(value) : null
|
||||
} else {
|
||||
return field.string()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
field.type == "DATETIME" ||
|
||||
field.type === "DATE" ||
|
||||
field.type === "TIMESTAMP"
|
||||
) {
|
||||
return field.string()
|
||||
}
|
||||
if (field.type === "BIT" && field.length === 1) {
|
||||
return field.buffer()?.[0]
|
||||
}
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this.client = await mysql.createConnection(this.config)
|
||||
}
|
||||
|
@ -204,7 +237,10 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
|
|||
|
||||
async internalQuery(
|
||||
query: SqlQuery,
|
||||
opts: { connect?: boolean; disableCoercion?: boolean } = {
|
||||
opts: {
|
||||
connect?: boolean
|
||||
disableCoercion?: boolean
|
||||
} = {
|
||||
connect: true,
|
||||
disableCoercion: false,
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
import {
|
||||
FieldSchema,
|
||||
RenameColumn,
|
||||
TableSchema,
|
||||
View,
|
||||
ViewV2,
|
||||
} from "@budibase/types"
|
||||
import { context, HTTPError } from "@budibase/backend-core"
|
||||
import { FieldSchema, TableSchema, View, ViewV2 } from "@budibase/types"
|
||||
|
||||
import sdk from "../../../sdk"
|
||||
import * as utils from "../../../db/utils"
|
||||
|
@ -103,3 +109,37 @@ export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) {
|
|||
schema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
export function syncSchema(
|
||||
view: ViewV2,
|
||||
schema: TableSchema,
|
||||
renameColumn: RenameColumn | undefined
|
||||
): ViewV2 {
|
||||
if (renameColumn) {
|
||||
if (view.columns) {
|
||||
view.columns[view.columns.indexOf(renameColumn.old)] =
|
||||
renameColumn.updated
|
||||
}
|
||||
if (view.schemaUI) {
|
||||
view.schemaUI[renameColumn.updated] = view.schemaUI[renameColumn.old]
|
||||
delete view.schemaUI[renameColumn.old]
|
||||
}
|
||||
}
|
||||
|
||||
if (view.schemaUI) {
|
||||
for (const fieldName of Object.keys(view.schemaUI)) {
|
||||
if (!schema[fieldName]) {
|
||||
delete view.schemaUI[fieldName]
|
||||
}
|
||||
}
|
||||
for (const fieldName of Object.keys(schema)) {
|
||||
if (!view.schemaUI[fieldName]) {
|
||||
view.schemaUI[fieldName] = { visible: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.columns = view.columns?.filter(x => schema[x])
|
||||
|
||||
return view
|
||||
}
|
||||
|
|
|
@ -1,53 +1,54 @@
|
|||
import { FieldType, Table, ViewV2 } from "@budibase/types"
|
||||
import _ from "lodash"
|
||||
import { FieldType, Table, TableSchema, ViewV2 } from "@budibase/types"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
import { enrichSchema } from ".."
|
||||
import { enrichSchema, syncSchema } from ".."
|
||||
|
||||
describe("table sdk", () => {
|
||||
describe("enrichViewSchemas", () => {
|
||||
const basicTable: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
type: "table",
|
||||
schema: {
|
||||
name: {
|
||||
type: FieldType.STRING,
|
||||
name: "name",
|
||||
visible: true,
|
||||
width: 80,
|
||||
order: 2,
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: FieldType.STRING,
|
||||
name: "description",
|
||||
visible: true,
|
||||
width: 200,
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
id: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "id",
|
||||
visible: true,
|
||||
order: 1,
|
||||
constraints: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
hiddenField: {
|
||||
type: FieldType.STRING,
|
||||
name: "hiddenField",
|
||||
visible: false,
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
const basicTable: Table = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
type: "table",
|
||||
schema: {
|
||||
name: {
|
||||
type: FieldType.STRING,
|
||||
name: "name",
|
||||
visible: true,
|
||||
width: 80,
|
||||
order: 2,
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
description: {
|
||||
type: FieldType.STRING,
|
||||
name: "description",
|
||||
visible: true,
|
||||
width: 200,
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
id: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "id",
|
||||
visible: true,
|
||||
order: 1,
|
||||
constraints: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
hiddenField: {
|
||||
type: FieldType.STRING,
|
||||
name: "hiddenField",
|
||||
visible: false,
|
||||
constraints: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe("enrichViewSchemas", () => {
|
||||
it("should fetch the default schema if not overriden", async () => {
|
||||
const tableId = basicTable._id!
|
||||
const view: ViewV2 = {
|
||||
|
@ -280,4 +281,294 @@ describe("table sdk", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("syncSchema", () => {
|
||||
const basicView: ViewV2 = {
|
||||
version: 2,
|
||||
id: generator.guid(),
|
||||
name: generator.guid(),
|
||||
tableId: basicTable._id!,
|
||||
}
|
||||
|
||||
describe("view without schema", () => {
|
||||
it("no table schema changes will not amend the view", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
}
|
||||
const result = syncSchema(
|
||||
_.cloneDeep(view),
|
||||
basicTable.schema,
|
||||
undefined
|
||||
)
|
||||
expect(result).toEqual(view)
|
||||
})
|
||||
|
||||
it("adding new columns will not change the view schema", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
}
|
||||
|
||||
const newTableSchema = {
|
||||
...basicTable.schema,
|
||||
newField1: {
|
||||
type: FieldType.STRING,
|
||||
name: "newField1",
|
||||
visible: true,
|
||||
},
|
||||
newField2: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "newField2",
|
||||
visible: false,
|
||||
},
|
||||
}
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||
expect(result).toEqual({
|
||||
...view,
|
||||
schemaUI: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("deleting columns will not change the view schema", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
}
|
||||
const { name, description, ...newTableSchema } = basicTable.schema
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||
expect(result).toEqual({
|
||||
...view,
|
||||
columns: ["id"],
|
||||
schemaUI: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("renaming mapped columns will update the view column mapping", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
}
|
||||
const { description, ...newTableSchema } = {
|
||||
...basicTable.schema,
|
||||
updatedDescription: {
|
||||
...basicTable.schema.description,
|
||||
name: "updatedDescription",
|
||||
},
|
||||
} as TableSchema
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, {
|
||||
old: "description",
|
||||
updated: "updatedDescription",
|
||||
})
|
||||
expect(result).toEqual({
|
||||
...view,
|
||||
columns: ["name", "id", "updatedDescription"],
|
||||
schemaUI: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("view with schema", () => {
|
||||
it("no table schema changes will not amend the view", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
schemaUI: {
|
||||
name: { visible: true, width: 100 },
|
||||
id: { visible: true, width: 20 },
|
||||
description: { visible: false },
|
||||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
const result = syncSchema(
|
||||
_.cloneDeep(view),
|
||||
basicTable.schema,
|
||||
undefined
|
||||
)
|
||||
expect(result).toEqual(view)
|
||||
})
|
||||
|
||||
it("adding new columns will add them as not visible to the view", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
schemaUI: {
|
||||
name: { visible: true, width: 100 },
|
||||
id: { visible: true, width: 20 },
|
||||
description: { visible: false },
|
||||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
|
||||
const newTableSchema = {
|
||||
...basicTable.schema,
|
||||
newField1: {
|
||||
type: FieldType.STRING,
|
||||
name: "newField1",
|
||||
visible: true,
|
||||
},
|
||||
newField2: {
|
||||
type: FieldType.NUMBER,
|
||||
name: "newField2",
|
||||
visible: false,
|
||||
},
|
||||
}
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||
expect(result).toEqual({
|
||||
...view,
|
||||
schemaUI: {
|
||||
...view.schemaUI,
|
||||
newField1: { visible: false },
|
||||
newField2: { visible: false },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("deleting columns will remove them from the UI", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
schemaUI: {
|
||||
name: { visible: true, width: 100 },
|
||||
id: { visible: true, width: 20 },
|
||||
description: { visible: false },
|
||||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
const { name, description, ...newTableSchema } = basicTable.schema
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||
expect(result).toEqual({
|
||||
...view,
|
||||
columns: ["id"],
|
||||
schemaUI: {
|
||||
...view.schemaUI,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("can handle additions and deletions at the same them UI", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
schemaUI: {
|
||||
name: { visible: true, width: 100 },
|
||||
id: { visible: true, width: 20 },
|
||||
description: { visible: false },
|
||||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
const { name, description, ...newTableSchema } = {
|
||||
...basicTable.schema,
|
||||
newField1: {
|
||||
type: FieldType.STRING,
|
||||
name: "newField1",
|
||||
visible: true,
|
||||
},
|
||||
} as TableSchema
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined)
|
||||
expect(result).toEqual({
|
||||
...view,
|
||||
columns: ["id"],
|
||||
schemaUI: {
|
||||
...view.schemaUI,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
newField1: { visible: false },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("renaming mapped columns will update the view column mapping and it's schema", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
schemaUI: {
|
||||
name: { visible: true },
|
||||
id: { visible: true },
|
||||
description: { visible: true, width: 150, icon: "ic-any" },
|
||||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
const { description, ...newTableSchema } = {
|
||||
...basicTable.schema,
|
||||
updatedDescription: {
|
||||
...basicTable.schema.description,
|
||||
name: "updatedDescription",
|
||||
},
|
||||
} as TableSchema
|
||||
|
||||
const result = syncSchema(_.cloneDeep(view), newTableSchema, {
|
||||
old: "description",
|
||||
updated: "updatedDescription",
|
||||
})
|
||||
expect(result).toEqual({
|
||||
...view,
|
||||
columns: ["name", "id", "updatedDescription"],
|
||||
schemaUI: {
|
||||
...view.schemaUI,
|
||||
description: undefined,
|
||||
updatedDescription: { visible: true, width: 150, icon: "ic-any" },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("changing no UI schema will not affect the view", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
schemaUI: {
|
||||
name: { visible: true, width: 100 },
|
||||
id: { visible: true, width: 20 },
|
||||
description: { visible: false },
|
||||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
const result = syncSchema(
|
||||
_.cloneDeep(view),
|
||||
{
|
||||
...basicTable.schema,
|
||||
id: {
|
||||
...basicTable.schema.id,
|
||||
type: FieldType.NUMBER,
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(result).toEqual(view)
|
||||
})
|
||||
|
||||
it("changing table column UI fields will not affect the view schema", () => {
|
||||
const view: ViewV2 = {
|
||||
...basicView,
|
||||
columns: ["name", "id", "description"],
|
||||
schemaUI: {
|
||||
name: { visible: true, width: 100 },
|
||||
id: { visible: true, width: 20 },
|
||||
description: { visible: false },
|
||||
hiddenField: { visible: false },
|
||||
},
|
||||
}
|
||||
const result = syncSchema(
|
||||
_.cloneDeep(view),
|
||||
{
|
||||
...basicTable.schema,
|
||||
id: {
|
||||
...basicTable.schema.id,
|
||||
visible: !basicTable.schema.id.visible,
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(result).toEqual(view)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,6 +17,7 @@ import * as pro from "@budibase/pro"
|
|||
import * as api from "./api"
|
||||
import sdk from "./sdk"
|
||||
import { initialise as initialiseWebsockets } from "./websockets"
|
||||
import { automationsEnabled } from "./features"
|
||||
|
||||
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
|
||||
// can't integrate directly into backend-core due to cyclic issues
|
||||
queuePromises.push(events.processors.init(pro.sdk.auditLogs.write))
|
||||
queuePromises.push(automations.init())
|
||||
if (automationsEnabled()) {
|
||||
queuePromises.push(automations.init())
|
||||
}
|
||||
queuePromises.push(initPro())
|
||||
if (app) {
|
||||
// bring routes online as final step once everything ready
|
||||
|
|
|
@ -87,7 +87,7 @@ class TestConfiguration {
|
|||
if (openServer) {
|
||||
// use a random port because it doesn't matter
|
||||
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
|
||||
this.request = supertest(this.server)
|
||||
this.started = true
|
||||
|
@ -178,7 +178,7 @@ class TestConfiguration {
|
|||
if (this.server) {
|
||||
this.server.close()
|
||||
} else {
|
||||
require("../../app").default.close()
|
||||
require("../../app").getServer().close()
|
||||
}
|
||||
if (this.allApps) {
|
||||
cleanup(this.allApps.map(app => app.appId))
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
AutomationMetadata,
|
||||
AutomationStatus,
|
||||
AutomationStep,
|
||||
AutomationStepStatus,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
AutomationContext,
|
||||
|
@ -452,7 +453,10 @@ class Orchestrator {
|
|||
this.executionOutput.steps.splice(loopStepNumber + 1, 0, {
|
||||
id: step.id,
|
||||
stepId: step.stepId,
|
||||
outputs: { status: AutomationStatus.NO_ITERATIONS, success: true },
|
||||
outputs: {
|
||||
status: AutomationStepStatus.NO_ITERATIONS,
|
||||
success: true,
|
||||
},
|
||||
inputs: {},
|
||||
})
|
||||
|
||||
|
|
|
@ -11,6 +11,12 @@ export interface QueryEvent {
|
|||
queryId: string
|
||||
environmentVariables?: Record<string, string>
|
||||
ctx?: any
|
||||
schema?: {
|
||||
[key: string]: {
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface QueryVariable {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { context, cache, auth } from "@budibase/backend-core"
|
|||
import { getGlobalIDFromUserMetadataID } from "../db/utils"
|
||||
import sdk from "../sdk"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { SourceName } from "@budibase/types"
|
||||
|
||||
import { isSQL } from "../integrations/utils"
|
||||
import { interpolateSQL } from "../integrations/queries/sql"
|
||||
|
@ -28,6 +29,7 @@ class QueryRunner {
|
|||
hasRerun: boolean
|
||||
hasRefreshedOAuth: boolean
|
||||
hasDynamicVariables: boolean
|
||||
schema: any
|
||||
|
||||
constructor(input: QueryEvent, flags = { noRecursiveQuery: false }) {
|
||||
this.datasource = input.datasource
|
||||
|
@ -37,6 +39,7 @@ class QueryRunner {
|
|||
this.pagination = input.pagination
|
||||
this.transformer = input.transformer
|
||||
this.queryId = input.queryId
|
||||
this.schema = input.schema
|
||||
this.noRecursiveQuery = flags.noRecursiveQuery
|
||||
this.cachedVariables = []
|
||||
// Additional context items for enrichment
|
||||
|
@ -51,7 +54,7 @@ class QueryRunner {
|
|||
}
|
||||
|
||||
async execute(): Promise<any> {
|
||||
let { datasource, fields, queryVerb, transformer } = this
|
||||
let { datasource, fields, queryVerb, transformer, schema } = this
|
||||
let datasourceClone = cloneDeep(datasource)
|
||||
let fieldsClone = cloneDeep(fields)
|
||||
|
||||
|
@ -70,6 +73,9 @@ class QueryRunner {
|
|||
|
||||
const integration = new Integration(datasourceClone.config)
|
||||
|
||||
// define the type casting from the schema
|
||||
integration.defineTypeCastingFromSchema?.(schema)
|
||||
|
||||
// pre-query, make sure datasource variables are added to parameters
|
||||
const parameters = await this.addDatasourceVariables()
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { InternalTables } from "../db/utils"
|
||||
import { getGlobalUser } from "./global"
|
||||
import { context, db as dbCore, roles } from "@budibase/backend-core"
|
||||
import { BBContext } from "@budibase/types"
|
||||
import { context, roles } from "@budibase/backend-core"
|
||||
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)
|
||||
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) {
|
||||
return ctx
|
||||
}
|
||||
if (!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
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import {
|
||||
Datasource,
|
||||
FieldType,
|
||||
SortDirection,
|
||||
SortType,
|
||||
SearchFilter,
|
||||
SearchQuery,
|
||||
SearchQueryFields,
|
||||
SortDirection,
|
||||
SortType,
|
||||
} from "@budibase/types"
|
||||
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
||||
import { deepGet } from "./helpers"
|
||||
|
@ -138,7 +138,8 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
|||
}
|
||||
if (Array.isArray(filter)) {
|
||||
filter.forEach(expression => {
|
||||
let { operator, field, type, value, externalType } = expression
|
||||
let { operator, field, type, value, externalType, onEmptyFilter } =
|
||||
expression
|
||||
const isHbs =
|
||||
typeof value === "string" && (value.match(HBS_REGEX) || []).length > 0
|
||||
// Parse all values into correct types
|
||||
|
@ -146,6 +147,10 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
|||
query.allOr = true
|
||||
return
|
||||
}
|
||||
if (onEmptyFilter) {
|
||||
query.onEmptyFilter = onEmptyFilter
|
||||
return
|
||||
}
|
||||
if (
|
||||
type === "datetime" &&
|
||||
!isHbs &&
|
||||
|
@ -203,7 +208,7 @@ export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
|||
) {
|
||||
query.range[field].high = value
|
||||
}
|
||||
} else if (query[operator]) {
|
||||
} else if (query[operator] && operator !== "onEmptyFilter") {
|
||||
if (type === "boolean") {
|
||||
// Transform boolean filters to cope with null.
|
||||
// "equals false" needs to be "not equals true"
|
||||
|
@ -418,7 +423,7 @@ export const hasFilters = (query?: SearchQuery) => {
|
|||
if (!query) {
|
||||
return false
|
||||
}
|
||||
const skipped = ["allOr"]
|
||||
const skipped = ["allOr", "onEmptyFilter"]
|
||||
for (let [key, value] of Object.entries(query)) {
|
||||
if (skipped.includes(key) || typeof value !== "object") {
|
||||
continue
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { Table, TableSchema, View, ViewV2 } from "../../../documents"
|
||||
import {
|
||||
Table,
|
||||
TableRequest,
|
||||
TableSchema,
|
||||
View,
|
||||
ViewV2,
|
||||
} from "../../../documents"
|
||||
|
||||
interface ViewV2Response extends ViewV2 {
|
||||
schema: TableSchema
|
||||
|
@ -11,3 +17,7 @@ export interface TableResponse extends Table {
|
|||
}
|
||||
|
||||
export type FetchTablesResponse = TableResponse[]
|
||||
|
||||
export interface SaveTableRequest extends TableRequest {}
|
||||
|
||||
export type SaveTableResponse = Table
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { FieldType } from "../../documents"
|
||||
import { EmptyFilterOption } from "../../sdk"
|
||||
|
||||
export type SearchFilter = {
|
||||
operator: keyof SearchQuery
|
||||
onEmptyFilter?: EmptyFilterOption
|
||||
field: string
|
||||
type?: FieldType
|
||||
value: any
|
||||
|
@ -10,6 +12,7 @@ export type SearchFilter = {
|
|||
|
||||
export type SearchQuery = {
|
||||
allOr?: boolean
|
||||
onEmptyFilter?: EmptyFilterOption
|
||||
string?: {
|
||||
[key: string]: string
|
||||
}
|
||||
|
@ -48,4 +51,4 @@ export type SearchQuery = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SearchQueryFields = Omit<SearchQuery, "allOr">
|
||||
export type SearchQueryFields = Omit<SearchQuery, "allOr" | "onEmptyFilter">
|
||||
|
|
|
@ -179,12 +179,15 @@ export interface AutomationTrigger extends AutomationTriggerSchema {
|
|||
id: string
|
||||
}
|
||||
|
||||
export enum AutomationStepStatus {
|
||||
NO_ITERATIONS = "no_iterations",
|
||||
}
|
||||
|
||||
export enum AutomationStatus {
|
||||
SUCCESS = "success",
|
||||
ERROR = "error",
|
||||
STOPPED = "stopped",
|
||||
STOPPED_ERROR = "stopped_error",
|
||||
NO_ITERATIONS = "no_iterations",
|
||||
}
|
||||
|
||||
export interface AutomationResults {
|
||||
|
|
|
@ -166,6 +166,12 @@ export interface IntegrationBase {
|
|||
delete?(query: any): Promise<any[] | any>
|
||||
testConnection?(): Promise<ConnectionInfo>
|
||||
getExternalSchema?(): Promise<string>
|
||||
defineTypeCastingFromSchema?(schema: {
|
||||
[key: string]: {
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
}): void
|
||||
}
|
||||
|
||||
export interface DatasourcePlus extends IntegrationBase {
|
||||
|
|
|
@ -11,6 +11,7 @@ export enum Feature {
|
|||
SYNC_AUTOMATIONS = "syncAutomations",
|
||||
APP_BUILDERS = "appBuilders",
|
||||
OFFLINE = "offline",
|
||||
USER_ROLE_PUBLIC_API = "userRolePublicApi",
|
||||
}
|
||||
|
||||
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
|
||||
|
|
|
@ -4,6 +4,7 @@ import { SortType } from "../api"
|
|||
|
||||
export interface SearchFilters {
|
||||
allOr?: boolean
|
||||
onEmptyFilter?: EmptyFilterOption
|
||||
string?: {
|
||||
[key: string]: string
|
||||
}
|
||||
|
@ -99,3 +100,8 @@ export interface SqlQuery {
|
|||
sql: string
|
||||
bindings?: string[]
|
||||
}
|
||||
|
||||
export enum EmptyFilterOption {
|
||||
RETURN_ALL = "all",
|
||||
RETURN_NONE = "none",
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ function parseIntSafe(number: any) {
|
|||
}
|
||||
|
||||
const environment = {
|
||||
// features
|
||||
WORKER_FEATURES: process.env.WORKER_FEATURES,
|
||||
// auth
|
||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -28,8 +28,8 @@ describe("datasource validators", () => {
|
|||
8000
|
||||
)}`,
|
||||
}
|
||||
env._set("AWS_ACCESS_KEY_ID", "mocked_key")
|
||||
env._set("AWS_SECRET_ACCESS_KEY", "mocked_secret")
|
||||
env._set("AWS_ACCESS_KEY_ID", "mockedkey")
|
||||
env._set("AWS_SECRET_ACCESS_KEY", "mockedsecret")
|
||||
})
|
||||
|
||||
it("test valid connection string", async () => {
|
||||
|
|
|
@ -28,10 +28,10 @@ function runBuild(entry, outfile) {
|
|||
) {
|
||||
// If we don't have pro, we cannot bundle backend-core.
|
||||
// Otherwise, the main context will not be shared between libraries
|
||||
delete tsconfigPathPluginContent.compilerOptions.paths[
|
||||
delete tsconfigPathPluginContent?.compilerOptions?.paths?.[
|
||||
"@budibase/backend-core"
|
||||
]
|
||||
delete tsconfigPathPluginContent.compilerOptions.paths[
|
||||
delete tsconfigPathPluginContent?.compilerOptions?.paths?.[
|
||||
"@budibase/backend-core/*"
|
||||
]
|
||||
}
|
||||
|
|
30
yarn.lock
30
yarn.lock
|
@ -10283,7 +10283,7 @@ denque@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf"
|
||||
integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==
|
||||
|
||||
denque@^2.0.1, denque@^2.1.0:
|
||||
denque@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
|
||||
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
|
||||
|
@ -16986,6 +16986,11 @@ long@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/long/-/long-5.2.1.tgz#e27595d0083d103d2fa2c20c7699f8e0c92b897f"
|
||||
integrity sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A==
|
||||
|
||||
long@^5.2.1:
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
|
||||
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
|
||||
|
||||
lookpath@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lookpath/-/lookpath-1.1.0.tgz#932d68371a2f0b4a5644f03d6a2b4728edba96d2"
|
||||
|
@ -17052,6 +17057,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1:
|
|||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
|
||||
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
|
||||
|
||||
lru-cache@^8.0.0:
|
||||
version "8.0.5"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e"
|
||||
integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==
|
||||
|
||||
lru-cache@^9.0.0:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.0.1.tgz#ac061ed291f8b9adaca2b085534bb1d3b61bef83"
|
||||
|
@ -17952,17 +17962,17 @@ mute-stream@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e"
|
||||
integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==
|
||||
|
||||
mysql2@2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.3.3.tgz#944f3deca4b16629052ff8614fbf89d5552545a0"
|
||||
integrity sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==
|
||||
mysql2@3.5.2:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.5.2.tgz#a06050e1514e9ac15711a8b883ffd51cb44b2dc8"
|
||||
integrity sha512-cptobmhYkYeTBIFp2c0piw2+gElpioga1rUw5UidHvo8yaHijMZoo8A3zyBVoo/K71f7ZFvrShA9iMIy9dCzCA==
|
||||
dependencies:
|
||||
denque "^2.0.1"
|
||||
denque "^2.1.0"
|
||||
generate-function "^2.3.1"
|
||||
iconv-lite "^0.6.3"
|
||||
long "^4.0.0"
|
||||
lru-cache "^6.0.0"
|
||||
named-placeholders "^1.1.2"
|
||||
long "^5.2.1"
|
||||
lru-cache "^8.0.0"
|
||||
named-placeholders "^1.1.3"
|
||||
seq-queue "^0.0.5"
|
||||
sqlstring "^2.3.2"
|
||||
|
||||
|
@ -17975,7 +17985,7 @@ mz@^2.4.0, mz@^2.7.0:
|
|||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
named-placeholders@^1.1.2:
|
||||
named-placeholders@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351"
|
||||
integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==
|
||||
|
|
Loading…
Reference in New Issue