Merge branch 'master' into builder-store-conversions-pc
This commit is contained in:
commit
cc053ec190
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.2.37",
|
"version": "3.2.42",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "rimraf dist/",
|
"prebuild": "rimraf dist/",
|
||||||
"prepack": "cp package.json dist",
|
"prepack": "cp package.json dist",
|
||||||
"build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null",
|
"build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null && tsc -p tsconfig.test.json --paths null",
|
||||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||||
"build:oss": "node ./scripts/build.js",
|
"build:oss": "node ./scripts/build.js",
|
||||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||||
|
|
|
@ -32,8 +32,12 @@ export async function errorHandling(ctx: any, next: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) {
|
if (environment.isTest() && ctx.headers["x-budibase-include-stacktrace"]) {
|
||||||
|
let rootErr = err
|
||||||
|
while (rootErr.cause) {
|
||||||
|
rootErr = rootErr.cause
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
error.stack = err.stack
|
error.stack = rootErr.stack
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = error
|
ctx.body = error
|
||||||
|
|
|
@ -272,17 +272,6 @@ class InternalBuilder {
|
||||||
return parts.join(".")
|
return parts.join(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
private isFullSelectStatementRequired(): boolean {
|
|
||||||
for (let column of Object.values(this.table.schema)) {
|
|
||||||
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(column)) {
|
|
||||||
return true
|
|
||||||
} else if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(column)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateSelectStatement(): (string | Knex.Raw)[] | "*" {
|
private generateSelectStatement(): (string | Knex.Raw)[] | "*" {
|
||||||
const { table, resource } = this.query
|
const { table, resource } = this.query
|
||||||
|
|
||||||
|
@ -292,11 +281,9 @@ class InternalBuilder {
|
||||||
|
|
||||||
const alias = this.getTableName(table)
|
const alias = this.getTableName(table)
|
||||||
const schema = this.table.schema
|
const schema = this.table.schema
|
||||||
if (!this.isFullSelectStatementRequired()) {
|
|
||||||
return [this.knex.raw("??", [`${alias}.*`])]
|
|
||||||
}
|
|
||||||
// get just the fields for this table
|
// get just the fields for this table
|
||||||
return resource.fields
|
const tableFields = resource.fields
|
||||||
.map(field => {
|
.map(field => {
|
||||||
const parts = field.split(/\./g)
|
const parts = field.split(/\./g)
|
||||||
let table: string | undefined = undefined
|
let table: string | undefined = undefined
|
||||||
|
@ -311,34 +298,33 @@ class InternalBuilder {
|
||||||
return { table, column, field }
|
return { table, column, field }
|
||||||
})
|
})
|
||||||
.filter(({ table }) => !table || table === alias)
|
.filter(({ table }) => !table || table === alias)
|
||||||
.map(({ table, column, field }) => {
|
|
||||||
const columnSchema = schema[column]
|
|
||||||
|
|
||||||
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
|
return tableFields.map(({ table, column, field }) => {
|
||||||
return this.knex.raw(`??::money::numeric as ??`, [
|
const columnSchema = schema[column]
|
||||||
this.rawQuotedIdentifier([table, column].join(".")),
|
|
||||||
this.knex.raw(this.quote(field)),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
|
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
|
||||||
// Time gets returned as timestamp from mssql, not matching the expected
|
return this.knex.raw(`??::money::numeric as ??`, [
|
||||||
// HH:mm format
|
this.rawQuotedIdentifier([table, column].join(".")),
|
||||||
|
this.knex.raw(this.quote(field)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: figure out how to express this safely without string
|
if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
|
||||||
// interpolation.
|
// Time gets returned as timestamp from mssql, not matching the expected
|
||||||
return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [
|
// HH:mm format
|
||||||
this.rawQuotedIdentifier(field),
|
|
||||||
this.knex.raw(this.quote(field)),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (table) {
|
return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [
|
||||||
return this.rawQuotedIdentifier(`${table}.${column}`)
|
this.rawQuotedIdentifier(field),
|
||||||
} else {
|
this.knex.raw(this.quote(field)),
|
||||||
return this.rawQuotedIdentifier(field)
|
])
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
if (table) {
|
||||||
|
return this.rawQuotedIdentifier(`${table}.${column}`)
|
||||||
|
} else {
|
||||||
|
return this.rawQuotedIdentifier(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
|
// OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
|
||||||
|
@ -816,14 +802,29 @@ class InternalBuilder {
|
||||||
filters.oneOf,
|
filters.oneOf,
|
||||||
ArrayOperator.ONE_OF,
|
ArrayOperator.ONE_OF,
|
||||||
(q, key: string, array) => {
|
(q, key: string, array) => {
|
||||||
|
const schema = this.getFieldSchema(key)
|
||||||
|
const values = Array.isArray(array) ? array : [array]
|
||||||
if (shouldOr) {
|
if (shouldOr) {
|
||||||
q = q.or
|
q = q.or
|
||||||
}
|
}
|
||||||
if (this.client === SqlClient.ORACLE) {
|
if (this.client === SqlClient.ORACLE) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
key = this.convertClobs(key)
|
key = this.convertClobs(key)
|
||||||
|
} else if (
|
||||||
|
this.client === SqlClient.SQL_LITE &&
|
||||||
|
schema?.type === FieldType.DATETIME &&
|
||||||
|
schema.dateOnly
|
||||||
|
) {
|
||||||
|
for (const value of values) {
|
||||||
|
if (value != null) {
|
||||||
|
q = q.or.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||||
|
} else {
|
||||||
|
q = q.or.whereNull(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return q
|
||||||
}
|
}
|
||||||
return q.whereIn(key, Array.isArray(array) ? array : [array])
|
return q.whereIn(key, values)
|
||||||
},
|
},
|
||||||
(q, key: string[], array) => {
|
(q, key: string[], array) => {
|
||||||
if (shouldOr) {
|
if (shouldOr) {
|
||||||
|
@ -882,6 +883,19 @@ class InternalBuilder {
|
||||||
let high = value.high
|
let high = value.high
|
||||||
let low = value.low
|
let low = value.low
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.client === SqlClient.SQL_LITE &&
|
||||||
|
schema?.type === FieldType.DATETIME &&
|
||||||
|
schema.dateOnly
|
||||||
|
) {
|
||||||
|
if (high != null) {
|
||||||
|
high = `${high.toISOString().slice(0, 10)}T23:59:59.999Z`
|
||||||
|
}
|
||||||
|
if (low != null) {
|
||||||
|
low = low.toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.client === SqlClient.ORACLE) {
|
if (this.client === SqlClient.ORACLE) {
|
||||||
rawKey = this.convertClobs(key)
|
rawKey = this.convertClobs(key)
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -914,6 +928,7 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
if (filters.equal) {
|
if (filters.equal) {
|
||||||
iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
|
iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
|
||||||
|
const schema = this.getFieldSchema(key)
|
||||||
if (shouldOr) {
|
if (shouldOr) {
|
||||||
q = q.or
|
q = q.or
|
||||||
}
|
}
|
||||||
|
@ -928,6 +943,16 @@ class InternalBuilder {
|
||||||
// @ts-expect-error knex types are wrong, raw is fine here
|
// @ts-expect-error knex types are wrong, raw is fine here
|
||||||
subq.whereNotNull(identifier).andWhere(identifier, value)
|
subq.whereNotNull(identifier).andWhere(identifier, value)
|
||||||
)
|
)
|
||||||
|
} else if (
|
||||||
|
this.client === SqlClient.SQL_LITE &&
|
||||||
|
schema?.type === FieldType.DATETIME &&
|
||||||
|
schema.dateOnly
|
||||||
|
) {
|
||||||
|
if (value != null) {
|
||||||
|
return q.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||||
|
} else {
|
||||||
|
return q.whereNull(key)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [
|
return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [
|
||||||
this.rawQuotedIdentifier(key),
|
this.rawQuotedIdentifier(key),
|
||||||
|
@ -938,6 +963,7 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
if (filters.notEqual) {
|
if (filters.notEqual) {
|
||||||
iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
|
iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
|
||||||
|
const schema = this.getFieldSchema(key)
|
||||||
if (shouldOr) {
|
if (shouldOr) {
|
||||||
q = q.or
|
q = q.or
|
||||||
}
|
}
|
||||||
|
@ -959,6 +985,18 @@ class InternalBuilder {
|
||||||
// @ts-expect-error knex types are wrong, raw is fine here
|
// @ts-expect-error knex types are wrong, raw is fine here
|
||||||
.or.whereNull(identifier)
|
.or.whereNull(identifier)
|
||||||
)
|
)
|
||||||
|
} else if (
|
||||||
|
this.client === SqlClient.SQL_LITE &&
|
||||||
|
schema?.type === FieldType.DATETIME &&
|
||||||
|
schema.dateOnly
|
||||||
|
) {
|
||||||
|
if (value != null) {
|
||||||
|
return q.not
|
||||||
|
.whereLike(key, `${value.toISOString().slice(0, 10)}%`)
|
||||||
|
.or.whereNull(key)
|
||||||
|
} else {
|
||||||
|
return q.not.whereNull(key)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [
|
return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [
|
||||||
this.rawQuotedIdentifier(key),
|
this.rawQuotedIdentifier(key),
|
||||||
|
@ -1239,6 +1277,7 @@ class InternalBuilder {
|
||||||
if (!toTable || !fromTable) {
|
if (!toTable || !fromTable) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedTable = tables[toTable]
|
const relatedTable = tables[toTable]
|
||||||
if (!relatedTable) {
|
if (!relatedTable) {
|
||||||
throw new Error(`related table "${toTable}" not found in datasource`)
|
throw new Error(`related table "${toTable}" not found in datasource`)
|
||||||
|
@ -1267,6 +1306,10 @@ class InternalBuilder {
|
||||||
const fieldList = relationshipFields.map(field =>
|
const fieldList = relationshipFields.map(field =>
|
||||||
this.buildJsonField(relatedTable, field)
|
this.buildJsonField(relatedTable, field)
|
||||||
)
|
)
|
||||||
|
if (!fieldList.length) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const fieldListFormatted = fieldList
|
const fieldListFormatted = fieldList
|
||||||
.map(f => {
|
.map(f => {
|
||||||
const separator = this.client === SqlClient.ORACLE ? " VALUE " : ","
|
const separator = this.client === SqlClient.ORACLE ? " VALUE " : ","
|
||||||
|
@ -1307,7 +1350,9 @@ class InternalBuilder {
|
||||||
)
|
)
|
||||||
|
|
||||||
const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => {
|
const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => {
|
||||||
subQuery = subQuery.select(`${toAlias}.*`).limit(getRelationshipLimit())
|
subQuery = subQuery
|
||||||
|
.select(relationshipFields)
|
||||||
|
.limit(getRelationshipLimit())
|
||||||
// @ts-ignore - the from alias syntax isn't in Knex typing
|
// @ts-ignore - the from alias syntax isn't in Knex typing
|
||||||
return knex.select(select).from({
|
return knex.select(select).from({
|
||||||
[toAlias]: subQuery,
|
[toAlias]: subQuery,
|
||||||
|
@ -1537,11 +1582,12 @@ class InternalBuilder {
|
||||||
limits?: { base: number; query: number }
|
limits?: { base: number; query: number }
|
||||||
} = {}
|
} = {}
|
||||||
): Knex.QueryBuilder {
|
): Knex.QueryBuilder {
|
||||||
let { operation, filters, paginate, relationships, table } = this.query
|
const { operation, filters, paginate, relationships, table } = this.query
|
||||||
const { limits } = opts
|
const { limits } = opts
|
||||||
|
|
||||||
// start building the query
|
// start building the query
|
||||||
let query = this.qualifiedKnex()
|
let query = this.qualifiedKnex()
|
||||||
|
|
||||||
// handle pagination
|
// handle pagination
|
||||||
let foundOffset: number | null = null
|
let foundOffset: number | null = null
|
||||||
let foundLimit = limits?.query || limits?.base
|
let foundLimit = limits?.query || limits?.base
|
||||||
|
@ -1590,7 +1636,7 @@ class InternalBuilder {
|
||||||
const mainTable = this.query.tableAliases?.[table.name] || table.name
|
const mainTable = this.query.tableAliases?.[table.name] || table.name
|
||||||
const cte = this.addSorting(
|
const cte = this.addSorting(
|
||||||
this.knex
|
this.knex
|
||||||
.with("paginated", query)
|
.with("paginated", query.clone().clearSelect().select("*"))
|
||||||
.select(this.generateSelectStatement())
|
.select(this.generateSelectStatement())
|
||||||
.from({
|
.from({
|
||||||
[mainTable]: "paginated",
|
[mainTable]: "paginated",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import environment from "../environment"
|
||||||
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
||||||
const ROW_ID_REGEX = /^\[.*]$/g
|
const ROW_ID_REGEX = /^\[.*]$/g
|
||||||
const ENCODED_SPACE = encodeURIComponent(" ")
|
const ENCODED_SPACE = encodeURIComponent(" ")
|
||||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/
|
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}.\d{3}Z)?$/
|
||||||
const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/
|
const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/
|
||||||
|
|
||||||
export function isExternalTableID(tableId: string) {
|
export function isExternalTableID(tableId: string) {
|
||||||
|
@ -149,15 +149,7 @@ export function isInvalidISODateString(str: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidISODateString(str: string) {
|
export function isValidISODateString(str: string) {
|
||||||
const trimmedValue = str.trim()
|
return ISO_DATE_REGEX.test(str.trim())
|
||||||
if (!ISO_DATE_REGEX.test(trimmedValue)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let d = new Date(trimmedValue)
|
|
||||||
if (isNaN(d.getTime())) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return d.toISOString() === trimmedValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidFilter(value: any) {
|
export function isValidFilter(value: any) {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["tests/**/*.js", "tests/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
|
@ -49,7 +49,6 @@
|
||||||
import { RowUtils, canBeDisplayColumn } from "@budibase/frontend-core"
|
import { RowUtils, canBeDisplayColumn } from "@budibase/frontend-core"
|
||||||
import ServerBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
import ServerBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
|
||||||
import OptionsEditor from "./OptionsEditor.svelte"
|
import OptionsEditor from "./OptionsEditor.svelte"
|
||||||
import { isEnabled } from "@/helpers/featureFlags"
|
|
||||||
import { getUserBindings } from "@/dataBinding"
|
import { getUserBindings } from "@/dataBinding"
|
||||||
|
|
||||||
export let field
|
export let field
|
||||||
|
@ -168,7 +167,6 @@
|
||||||
// used to select what different options can be displayed for column type
|
// used to select what different options can be displayed for column type
|
||||||
$: canBeDisplay =
|
$: canBeDisplay =
|
||||||
canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn
|
canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn
|
||||||
$: defaultValuesEnabled = isEnabled("DEFAULT_VALUES")
|
|
||||||
$: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type)
|
$: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type)
|
||||||
$: canBeRequired =
|
$: canBeRequired =
|
||||||
editableColumn?.type !== FieldType.LINK &&
|
editableColumn?.type !== FieldType.LINK &&
|
||||||
|
@ -300,7 +298,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we don't have a default value if we can't have one
|
// Ensure we don't have a default value if we can't have one
|
||||||
if (!canHaveDefault || !defaultValuesEnabled) {
|
if (!canHaveDefault) {
|
||||||
delete saveColumn.default
|
delete saveColumn.default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -848,51 +846,49 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if defaultValuesEnabled}
|
{#if editableColumn.type === FieldType.OPTIONS}
|
||||||
{#if editableColumn.type === FieldType.OPTIONS}
|
<Select
|
||||||
<Select
|
disabled={!canHaveDefault}
|
||||||
disabled={!canHaveDefault}
|
options={editableColumn.constraints?.inclusion || []}
|
||||||
options={editableColumn.constraints?.inclusion || []}
|
label="Default value"
|
||||||
label="Default value"
|
value={editableColumn.default}
|
||||||
value={editableColumn.default}
|
on:change={e => (editableColumn.default = e.detail)}
|
||||||
on:change={e => (editableColumn.default = e.detail)}
|
placeholder="None"
|
||||||
placeholder="None"
|
/>
|
||||||
/>
|
{:else if editableColumn.type === FieldType.ARRAY}
|
||||||
{:else if editableColumn.type === FieldType.ARRAY}
|
<Multiselect
|
||||||
<Multiselect
|
disabled={!canHaveDefault}
|
||||||
disabled={!canHaveDefault}
|
options={editableColumn.constraints?.inclusion || []}
|
||||||
options={editableColumn.constraints?.inclusion || []}
|
label="Default value"
|
||||||
label="Default value"
|
value={editableColumn.default}
|
||||||
value={editableColumn.default}
|
on:change={e =>
|
||||||
on:change={e =>
|
(editableColumn.default = e.detail?.length ? e.detail : undefined)}
|
||||||
(editableColumn.default = e.detail?.length ? e.detail : undefined)}
|
placeholder="None"
|
||||||
placeholder="None"
|
/>
|
||||||
/>
|
{:else if editableColumn.subtype === BBReferenceFieldSubType.USER}
|
||||||
{:else if editableColumn.subtype === BBReferenceFieldSubType.USER}
|
{@const defaultValue =
|
||||||
{@const defaultValue =
|
editableColumn.type === FieldType.BB_REFERENCE_SINGLE
|
||||||
editableColumn.type === FieldType.BB_REFERENCE_SINGLE
|
? SingleUserDefault
|
||||||
? SingleUserDefault
|
: MultiUserDefault}
|
||||||
: MultiUserDefault}
|
<Toggle
|
||||||
<Toggle
|
disabled={!canHaveDefault}
|
||||||
disabled={!canHaveDefault}
|
text="Default to current user"
|
||||||
text="Default to current user"
|
value={editableColumn.default === defaultValue}
|
||||||
value={editableColumn.default === defaultValue}
|
on:change={e =>
|
||||||
on:change={e =>
|
(editableColumn.default = e.detail ? defaultValue : undefined)}
|
||||||
(editableColumn.default = e.detail ? defaultValue : undefined)}
|
/>
|
||||||
/>
|
{:else}
|
||||||
{:else}
|
<ModalBindableInput
|
||||||
<ModalBindableInput
|
disabled={!canHaveDefault}
|
||||||
disabled={!canHaveDefault}
|
panel={ServerBindingPanel}
|
||||||
panel={ServerBindingPanel}
|
title="Default value"
|
||||||
title="Default value"
|
label="Default value"
|
||||||
label="Default value"
|
placeholder="None"
|
||||||
placeholder="None"
|
value={editableColumn.default}
|
||||||
value={editableColumn.default}
|
on:change={e => (editableColumn.default = e.detail)}
|
||||||
on:change={e => (editableColumn.default = e.detail)}
|
bindings={defaultValueBindings}
|
||||||
bindings={defaultValueBindings}
|
allowJS
|
||||||
allowJS
|
/>
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,9 @@
|
||||||
let loading = false
|
let loading = false
|
||||||
let deleteConfirmationDialog
|
let deleteConfirmationDialog
|
||||||
|
|
||||||
$: defaultName = getSequentialName($snippets, "MySnippet", x => x.name)
|
$: defaultName = getSequentialName($snippets, "MySnippet", {
|
||||||
|
getName: x => x.name,
|
||||||
|
})
|
||||||
$: key = snippet?.name
|
$: key = snippet?.name
|
||||||
$: name = snippet?.name || defaultName
|
$: name = snippet?.name || defaultName
|
||||||
$: code = snippet?.code ? encodeJSBinding(snippet.code) : ""
|
$: code = snippet?.code ? encodeJSBinding(snippet.code) : ""
|
||||||
|
|
|
@ -16,7 +16,10 @@ export {
|
||||||
|
|
||||||
export const AUTO_COLUMN_SUB_TYPES = AutoFieldSubType
|
export const AUTO_COLUMN_SUB_TYPES = AutoFieldSubType
|
||||||
|
|
||||||
export const AUTO_COLUMN_DISPLAY_NAMES = {
|
export const AUTO_COLUMN_DISPLAY_NAMES: Record<
|
||||||
|
keyof typeof AUTO_COLUMN_SUB_TYPES,
|
||||||
|
string
|
||||||
|
> = {
|
||||||
AUTO_ID: "Auto ID",
|
AUTO_ID: "Auto ID",
|
||||||
CREATED_BY: "Created By",
|
CREATED_BY: "Created By",
|
||||||
CREATED_AT: "Created At",
|
CREATED_AT: "Created At",
|
||||||
|
@ -209,13 +212,6 @@ export const Roles = {
|
||||||
BUILDER: "BUILDER",
|
BUILDER: "BUILDER",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAutoColumnUserRelationship(subtype) {
|
|
||||||
return (
|
|
||||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY ||
|
|
||||||
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrettyRelationshipDefinitions = {
|
export const PrettyRelationshipDefinitions = {
|
||||||
MANY: "Many rows",
|
MANY: "Many rows",
|
||||||
ONE: "One row",
|
ONE: "One row",
|
|
@ -10,13 +10,13 @@
|
||||||
*
|
*
|
||||||
* Repl
|
* Repl
|
||||||
*/
|
*/
|
||||||
export const duplicateName = (name, allNames) => {
|
export const duplicateName = (name: string, allNames: string[]) => {
|
||||||
const duplicatePattern = new RegExp(`\\s(\\d+)$`)
|
const duplicatePattern = new RegExp(`\\s(\\d+)$`)
|
||||||
const baseName = name.split(duplicatePattern)[0]
|
const baseName = name.split(duplicatePattern)[0]
|
||||||
const isDuplicate = new RegExp(`${baseName}\\s(\\d+)$`)
|
const isDuplicate = new RegExp(`${baseName}\\s(\\d+)$`)
|
||||||
|
|
||||||
// get the sequence from matched names
|
// get the sequence from matched names
|
||||||
const sequence = []
|
const sequence: number[] = []
|
||||||
allNames.filter(n => {
|
allNames.filter(n => {
|
||||||
if (n === baseName) {
|
if (n === baseName) {
|
||||||
return true
|
return true
|
||||||
|
@ -70,12 +70,18 @@ export const duplicateName = (name, allNames) => {
|
||||||
* @param getName optional function to extract the name for an item, if not a
|
* @param getName optional function to extract the name for an item, if not a
|
||||||
* flat array of strings
|
* flat array of strings
|
||||||
*/
|
*/
|
||||||
export const getSequentialName = (
|
export const getSequentialName = <T extends any>(
|
||||||
items,
|
items: T[] | null,
|
||||||
prefix,
|
prefix: string | null,
|
||||||
{ getName = x => x, numberFirstItem = false } = {}
|
{
|
||||||
|
getName,
|
||||||
|
numberFirstItem,
|
||||||
|
}: {
|
||||||
|
getName?: (item: T) => string
|
||||||
|
numberFirstItem?: boolean
|
||||||
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
if (!prefix?.length || !getName) {
|
if (!prefix?.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const trimmedPrefix = prefix.trim()
|
const trimmedPrefix = prefix.trim()
|
||||||
|
@ -85,7 +91,7 @@ export const getSequentialName = (
|
||||||
}
|
}
|
||||||
let max = 0
|
let max = 0
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const name = getName(item)
|
const name = getName?.(item) ?? item
|
||||||
if (typeof name !== "string" || !name.startsWith(trimmedPrefix)) {
|
if (typeof name !== "string" || !name.startsWith(trimmedPrefix)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
|
import { FeatureFlag } from "@budibase/types"
|
||||||
import { auth } from "../stores/portal"
|
import { auth } from "../stores/portal"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export const isEnabled = featureFlag => {
|
export const isEnabled = (featureFlag: FeatureFlag | `${FeatureFlag}`) => {
|
||||||
const user = get(auth).user
|
const user = get(auth).user
|
||||||
return !!user?.flags?.[featureFlag]
|
return !!user?.flags?.[featureFlag]
|
||||||
}
|
}
|
|
@ -1,13 +1,21 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
|
|
||||||
export default function (url) {
|
export default function (url: string) {
|
||||||
const store = writable({ status: "LOADING", data: {}, error: {} })
|
const store = writable<{
|
||||||
|
status: "LOADING" | "SUCCESS" | "ERROR"
|
||||||
|
data: object
|
||||||
|
error?: unknown
|
||||||
|
}>({
|
||||||
|
status: "LOADING",
|
||||||
|
data: {},
|
||||||
|
error: {},
|
||||||
|
})
|
||||||
|
|
||||||
async function get() {
|
async function get() {
|
||||||
store.update(u => ({ ...u, status: "LOADING" }))
|
store.update(u => ({ ...u, status: "LOADING" }))
|
||||||
try {
|
try {
|
||||||
const data = await API.get({ url })
|
const data = await API.get<object>({ url })
|
||||||
store.set({ data, status: "SUCCESS" })
|
store.set({ data, status: "SUCCESS" })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
store.set({ data: {}, error: e, status: "ERROR" })
|
store.set({ data: {}, error: e, status: "ERROR" })
|
|
@ -1,46 +0,0 @@
|
||||||
import { last, flow } from "lodash/fp"
|
|
||||||
|
|
||||||
export const buildStyle = styles => {
|
|
||||||
let str = ""
|
|
||||||
for (let s in styles) {
|
|
||||||
if (styles[s]) {
|
|
||||||
let key = convertCamel(s)
|
|
||||||
str += `${key}: ${styles[s]}; `
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
export const convertCamel = str => {
|
|
||||||
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const pipe = (arg, funcs) => flow(funcs)(arg)
|
|
||||||
|
|
||||||
export const capitalise = s => {
|
|
||||||
if (!s) {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return s.substring(0, 1).toUpperCase() + s.substring(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
|
||||||
|
|
||||||
export const lowercaseExceptFirst = s =>
|
|
||||||
s.charAt(0) + s.substring(1).toLowerCase()
|
|
||||||
|
|
||||||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
|
||||||
|
|
||||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
|
||||||
|
|
||||||
export const isBuilderInputFocused = e => {
|
|
||||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
|
||||||
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
|
|
||||||
if (
|
|
||||||
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
|
||||||
e.key !== "Escape"
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { Many } from "lodash"
|
||||||
|
import { last, flow } from "lodash/fp"
|
||||||
|
|
||||||
|
export const buildStyle = (styles: Record<string, any>) => {
|
||||||
|
let str = ""
|
||||||
|
for (let s in styles) {
|
||||||
|
if (styles[s]) {
|
||||||
|
let key = convertCamel(s)
|
||||||
|
str += `${key}: ${styles[s]}; `
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertCamel = (str: string) => {
|
||||||
|
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pipe = (arg: string, funcs: Many<(...args: any[]) => any>) =>
|
||||||
|
flow(funcs)(arg)
|
||||||
|
|
||||||
|
export const capitalise = (s: string) => {
|
||||||
|
if (!s) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lowercase = (s: string) =>
|
||||||
|
s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||||
|
|
||||||
|
export const lowercaseExceptFirst = (s: string) =>
|
||||||
|
s.charAt(0) + s.substring(1).toLowerCase()
|
||||||
|
|
||||||
|
export const get_name = (s: string) => (!s ? "" : last(s.split("/")))
|
||||||
|
|
||||||
|
export const get_capitalised_name = (name: string) =>
|
||||||
|
pipe(name, [get_name, capitalise])
|
||||||
|
|
||||||
|
export const isBuilderInputFocused = (e: KeyboardEvent) => {
|
||||||
|
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||||
|
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
|
||||||
|
if (
|
||||||
|
(inCodeEditor || ["input", "textarea"].indexOf(activeTag!) !== -1) &&
|
||||||
|
e.key !== "Escape"
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
function handleEnter(fnc) {
|
|
||||||
return e => e.key === "Enter" && fnc()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const keyUtils = {
|
|
||||||
handleEnter,
|
|
||||||
}
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
function handleEnter(fnc: () => void) {
|
||||||
|
return (e: KeyboardEvent) => e.key === "Enter" && fnc()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const keyUtils = {
|
||||||
|
handleEnter,
|
||||||
|
}
|
|
@ -1,6 +1,16 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
function defaultValue() {
|
interface PaginationStore {
|
||||||
|
nextPage: string | null | undefined
|
||||||
|
page: string | null | undefined
|
||||||
|
hasPrevPage: boolean
|
||||||
|
hasNextPage: boolean
|
||||||
|
loading: boolean
|
||||||
|
pageNumber: number
|
||||||
|
pages: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultValue(): PaginationStore {
|
||||||
return {
|
return {
|
||||||
nextPage: null,
|
nextPage: null,
|
||||||
page: undefined,
|
page: undefined,
|
||||||
|
@ -29,13 +39,13 @@ export function createPaginationStore() {
|
||||||
update(state => {
|
update(state => {
|
||||||
state.pageNumber++
|
state.pageNumber++
|
||||||
state.page = state.nextPage
|
state.page = state.nextPage
|
||||||
state.pages.push(state.page)
|
state.pages.push(state.page!)
|
||||||
state.hasPrevPage = state.pageNumber > 1
|
state.hasPrevPage = state.pageNumber > 1
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetched(hasNextPage, nextPage) {
|
function fetched(hasNextPage: boolean, nextPage: string) {
|
||||||
update(state => {
|
update(state => {
|
||||||
state.hasNextPage = hasNextPage
|
state.hasNextPage = hasNextPage
|
||||||
state.nextPage = nextPage
|
state.nextPage = nextPage
|
|
@ -1,6 +1,6 @@
|
||||||
import { PlanType } from "@budibase/types"
|
import { PlanType } from "@budibase/types"
|
||||||
|
|
||||||
export function getFormattedPlanName(userPlanType) {
|
export function getFormattedPlanName(userPlanType: PlanType) {
|
||||||
let planName
|
let planName
|
||||||
switch (userPlanType) {
|
switch (userPlanType) {
|
||||||
case PlanType.PRO:
|
case PlanType.PRO:
|
||||||
|
@ -29,6 +29,6 @@ export function getFormattedPlanName(userPlanType) {
|
||||||
return `${planName} Plan`
|
return `${planName} Plan`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPremiumOrAbove(userPlanType) {
|
export function isPremiumOrAbove(userPlanType: PlanType) {
|
||||||
return ![PlanType.PRO, PlanType.TEAM, PlanType.FREE].includes(userPlanType)
|
return ![PlanType.PRO, PlanType.TEAM, PlanType.FREE].includes(userPlanType)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export default function (url) {
|
export default function (url: string) {
|
||||||
return url
|
return url
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(part => {
|
.map(part => {
|
|
@ -1,75 +0,0 @@
|
||||||
import { FieldType } from "@budibase/types"
|
|
||||||
import { ActionStepID } from "@/constants/backend/automations"
|
|
||||||
import { TableNames } from "@/constants"
|
|
||||||
import {
|
|
||||||
AUTO_COLUMN_DISPLAY_NAMES,
|
|
||||||
AUTO_COLUMN_SUB_TYPES,
|
|
||||||
FIELDS,
|
|
||||||
isAutoColumnUserRelationship,
|
|
||||||
} from "@/constants/backend"
|
|
||||||
import { isEnabled } from "@/helpers/featureFlags"
|
|
||||||
|
|
||||||
export function getAutoColumnInformation(enabled = true) {
|
|
||||||
let info = {}
|
|
||||||
for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
|
|
||||||
// Because it's possible to replicate the functionality of CREATED_AT and
|
|
||||||
// CREATED_BY columns, we disable their creation when the DEFAULT_VALUES
|
|
||||||
// feature flag is enabled.
|
|
||||||
if (isEnabled("DEFAULT_VALUES")) {
|
|
||||||
if (
|
|
||||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT ||
|
|
||||||
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info[subtype] = { enabled, name: AUTO_COLUMN_DISPLAY_NAMES[key] }
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildAutoColumn(tableName, name, subtype) {
|
|
||||||
let type, constraints
|
|
||||||
switch (subtype) {
|
|
||||||
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
|
|
||||||
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
|
|
||||||
type = FieldType.LINK
|
|
||||||
constraints = FIELDS.LINK.constraints
|
|
||||||
break
|
|
||||||
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
|
|
||||||
type = FieldType.NUMBER
|
|
||||||
constraints = FIELDS.NUMBER.constraints
|
|
||||||
break
|
|
||||||
case AUTO_COLUMN_SUB_TYPES.UPDATED_AT:
|
|
||||||
case AUTO_COLUMN_SUB_TYPES.CREATED_AT:
|
|
||||||
type = FieldType.DATETIME
|
|
||||||
constraints = FIELDS.DATETIME.constraints
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
type = FieldType.STRING
|
|
||||||
constraints = FIELDS.STRING.constraints
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (Object.values(AUTO_COLUMN_SUB_TYPES).indexOf(subtype) === -1) {
|
|
||||||
throw "Cannot build auto column with supplied subtype"
|
|
||||||
}
|
|
||||||
const base = {
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
subtype,
|
|
||||||
icon: "ri-magic-line",
|
|
||||||
autocolumn: true,
|
|
||||||
constraints,
|
|
||||||
}
|
|
||||||
if (isAutoColumnUserRelationship(subtype)) {
|
|
||||||
base.tableId = TableNames.USERS
|
|
||||||
base.fieldName = `${tableName}-${name}`
|
|
||||||
}
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkForCollectStep(automation) {
|
|
||||||
return automation.definition.steps.some(
|
|
||||||
step => step.stepId === ActionStepID.COLLECT
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import {
|
||||||
|
AutoFieldSubType,
|
||||||
|
Automation,
|
||||||
|
DateFieldMetadata,
|
||||||
|
FieldType,
|
||||||
|
NumberFieldMetadata,
|
||||||
|
RelationshipFieldMetadata,
|
||||||
|
RelationshipType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { ActionStepID } from "@/constants/backend/automations"
|
||||||
|
import { TableNames } from "@/constants"
|
||||||
|
import {
|
||||||
|
AUTO_COLUMN_DISPLAY_NAMES,
|
||||||
|
AUTO_COLUMN_SUB_TYPES,
|
||||||
|
FIELDS,
|
||||||
|
} from "@/constants/backend"
|
||||||
|
import { utils } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
type AutoColumnInformation = Partial<
|
||||||
|
Record<AutoFieldSubType, { enabled: boolean; name: string }>
|
||||||
|
>
|
||||||
|
|
||||||
|
export function getAutoColumnInformation(
|
||||||
|
enabled = true
|
||||||
|
): AutoColumnInformation {
|
||||||
|
const info: AutoColumnInformation = {}
|
||||||
|
for (const [key, subtype] of Object.entries(AUTO_COLUMN_SUB_TYPES)) {
|
||||||
|
// Because it's possible to replicate the functionality of CREATED_AT and
|
||||||
|
// CREATED_BY columns with user column default values, we disable their creation
|
||||||
|
if (
|
||||||
|
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_AT ||
|
||||||
|
subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const typedKey = key as keyof typeof AUTO_COLUMN_SUB_TYPES
|
||||||
|
info[subtype] = {
|
||||||
|
enabled,
|
||||||
|
name: AUTO_COLUMN_DISPLAY_NAMES[typedKey],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAutoColumn(
|
||||||
|
tableName: string,
|
||||||
|
name: string,
|
||||||
|
subtype: AutoFieldSubType
|
||||||
|
): RelationshipFieldMetadata | NumberFieldMetadata | DateFieldMetadata {
|
||||||
|
const base = {
|
||||||
|
name,
|
||||||
|
icon: "ri-magic-line",
|
||||||
|
autocolumn: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (subtype) {
|
||||||
|
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
|
||||||
|
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
type: FieldType.LINK,
|
||||||
|
subtype,
|
||||||
|
constraints: FIELDS.LINK.constraints,
|
||||||
|
tableId: TableNames.USERS,
|
||||||
|
fieldName: `${tableName}-${name}`,
|
||||||
|
relationshipType: RelationshipType.MANY_TO_ONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
subtype,
|
||||||
|
constraints: FIELDS.NUMBER.constraints,
|
||||||
|
}
|
||||||
|
case AUTO_COLUMN_SUB_TYPES.UPDATED_AT:
|
||||||
|
case AUTO_COLUMN_SUB_TYPES.CREATED_AT:
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
type: FieldType.DATETIME,
|
||||||
|
subtype,
|
||||||
|
constraints: FIELDS.DATETIME.constraints,
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw utils.unreachable(subtype, {
|
||||||
|
message: "Cannot build auto column with supplied subtype",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkForCollectStep(automation: Automation) {
|
||||||
|
return automation.definition.steps.some(
|
||||||
|
step => step.stepId === ActionStepID.COLLECT
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export const suppressWarnings = warnings => {
|
export const suppressWarnings = (warnings: string[]) => {
|
||||||
if (!warnings?.length) {
|
if (!warnings?.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -442,13 +442,11 @@
|
||||||
|
|
||||||
const onUpdateUserInvite = async (invite, role) => {
|
const onUpdateUserInvite = async (invite, role) => {
|
||||||
let updateBody = {
|
let updateBody = {
|
||||||
code: invite.code,
|
|
||||||
apps: {
|
apps: {
|
||||||
...invite.apps,
|
...invite.apps,
|
||||||
[prodAppId]: role,
|
[prodAppId]: role,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (role === Constants.Roles.CREATOR) {
|
if (role === Constants.Roles.CREATOR) {
|
||||||
updateBody.builder = updateBody.builder || {}
|
updateBody.builder = updateBody.builder || {}
|
||||||
updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId]
|
updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId]
|
||||||
|
@ -456,7 +454,7 @@
|
||||||
} else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) {
|
} else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) {
|
||||||
invite.builder.apps = []
|
invite.builder.apps = []
|
||||||
}
|
}
|
||||||
await users.updateInvite(updateBody)
|
await users.updateInvite(invite.code, updateBody)
|
||||||
await filterInvites(query)
|
await filterInvites(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,8 +468,7 @@
|
||||||
let updated = { ...invite }
|
let updated = { ...invite }
|
||||||
delete updated.info.apps[prodAppId]
|
delete updated.info.apps[prodAppId]
|
||||||
|
|
||||||
return await users.updateInvite({
|
return await users.updateInvite(updated.code, {
|
||||||
code: updated.code,
|
|
||||||
apps: updated.apps,
|
apps: updated.apps,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,8 +191,14 @@
|
||||||
? "View errors"
|
? "View errors"
|
||||||
: "View error"}
|
: "View error"}
|
||||||
on:dismiss={async () => {
|
on:dismiss={async () => {
|
||||||
await automationStore.actions.clearLogErrors({ appId })
|
const automationId = Object.keys(automationErrors[appId] || {})[0]
|
||||||
await appsStore.load()
|
if (automationId) {
|
||||||
|
await automationStore.actions.clearLogErrors({
|
||||||
|
appId,
|
||||||
|
automationId,
|
||||||
|
})
|
||||||
|
await appsStore.load()
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
message={automationErrorMessage(appId)}
|
message={automationErrorMessage(appId)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
const removeUser = async id => {
|
const removeUser = async id => {
|
||||||
await groups.actions.removeUser(groupId, id)
|
await groups.removeUser(groupId, id)
|
||||||
fetchGroupUsers.refresh()
|
fetchGroupUsers.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -251,6 +251,7 @@
|
||||||
passwordModal.show()
|
passwordModal.show()
|
||||||
await fetch.refresh()
|
await fetch.refresh()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
notifications.error("Error creating user")
|
notifications.error("Error creating user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
|
||||||
import { datasources } from "./datasources"
|
|
||||||
import { integrations } from "./integrations"
|
|
||||||
import { API } from "@/api"
|
|
||||||
import { duplicateName } from "@/helpers/duplicate"
|
|
||||||
|
|
||||||
const sortQueries = queryList => {
|
|
||||||
queryList.sort((q1, q2) => {
|
|
||||||
return q1.name.localeCompare(q2.name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createQueriesStore() {
|
|
||||||
const store = writable({
|
|
||||||
list: [],
|
|
||||||
selectedQueryId: null,
|
|
||||||
})
|
|
||||||
const derivedStore = derived(store, $store => ({
|
|
||||||
...$store,
|
|
||||||
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const fetch = async () => {
|
|
||||||
const queries = await API.getQueries()
|
|
||||||
sortQueries(queries)
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
list: queries,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async (datasourceId, query) => {
|
|
||||||
const _integrations = get(integrations)
|
|
||||||
const dataSource = get(datasources).list.filter(
|
|
||||||
ds => ds._id === datasourceId
|
|
||||||
)
|
|
||||||
// Check if readable attribute is found
|
|
||||||
if (dataSource.length !== 0) {
|
|
||||||
const integration = _integrations[dataSource[0].source]
|
|
||||||
const readable = integration.query[query.queryVerb].readable
|
|
||||||
if (readable) {
|
|
||||||
query.readable = readable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
query.datasourceId = datasourceId
|
|
||||||
const savedQuery = await API.saveQuery(query)
|
|
||||||
store.update(state => {
|
|
||||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
|
||||||
const queries = state.list
|
|
||||||
if (idx >= 0) {
|
|
||||||
queries.splice(idx, 1, savedQuery)
|
|
||||||
} else {
|
|
||||||
queries.push(savedQuery)
|
|
||||||
}
|
|
||||||
sortQueries(queries)
|
|
||||||
return {
|
|
||||||
list: queries,
|
|
||||||
selectedQueryId: savedQuery._id,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return savedQuery
|
|
||||||
}
|
|
||||||
|
|
||||||
const importQueries = async ({ data, datasourceId }) => {
|
|
||||||
return await API.importQueries(datasourceId, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
const select = id => {
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
selectedQueryId: id,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const preview = async query => {
|
|
||||||
const result = await API.previewQuery(query)
|
|
||||||
// Assume all the fields are strings and create a basic schema from the
|
|
||||||
// unique fields returned by the server
|
|
||||||
const schema = {}
|
|
||||||
for (let [field, metadata] of Object.entries(result.schema)) {
|
|
||||||
schema[field] = metadata || { type: "string" }
|
|
||||||
}
|
|
||||||
return { ...result, schema, rows: result.rows || [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteQuery = async query => {
|
|
||||||
await API.deleteQuery(query._id, query._rev)
|
|
||||||
store.update(state => {
|
|
||||||
state.list = state.list.filter(existing => existing._id !== query._id)
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicate = async query => {
|
|
||||||
let list = get(store).list
|
|
||||||
const newQuery = { ...query }
|
|
||||||
const datasourceId = query.datasourceId
|
|
||||||
|
|
||||||
delete newQuery._id
|
|
||||||
delete newQuery._rev
|
|
||||||
newQuery.name = duplicateName(
|
|
||||||
query.name,
|
|
||||||
list.map(q => q.name)
|
|
||||||
)
|
|
||||||
|
|
||||||
return await save(datasourceId, newQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeDatasourceQueries = datasourceId => {
|
|
||||||
store.update(state => ({
|
|
||||||
...state,
|
|
||||||
list: state.list.filter(table => table.datasourceId !== datasourceId),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe: derivedStore.subscribe,
|
|
||||||
fetch,
|
|
||||||
init: fetch,
|
|
||||||
select,
|
|
||||||
save,
|
|
||||||
import: importQueries,
|
|
||||||
delete: deleteQuery,
|
|
||||||
preview,
|
|
||||||
duplicate,
|
|
||||||
removeDatasourceQueries,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const queries = createQueriesStore()
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
import { derived, get, Writable } from "svelte/store"
|
||||||
|
import { datasources } from "./datasources"
|
||||||
|
import { integrations } from "./integrations"
|
||||||
|
import { API } from "@/api"
|
||||||
|
import { duplicateName } from "@/helpers/duplicate"
|
||||||
|
import { DerivedBudiStore } from "@/stores/BudiStore"
|
||||||
|
import {
|
||||||
|
Query,
|
||||||
|
QueryPreview,
|
||||||
|
PreviewQueryResponse,
|
||||||
|
SaveQueryRequest,
|
||||||
|
ImportRestQueryRequest,
|
||||||
|
QuerySchema,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
const sortQueries = (queryList: Query[]) => {
|
||||||
|
queryList.sort((q1, q2) => {
|
||||||
|
return q1.name.localeCompare(q2.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BuilderQueryStore {
|
||||||
|
list: Query[]
|
||||||
|
selectedQueryId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DerivedQueryStore extends BuilderQueryStore {
|
||||||
|
selected?: Query
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryStore extends DerivedBudiStore<
|
||||||
|
BuilderQueryStore,
|
||||||
|
DerivedQueryStore
|
||||||
|
> {
|
||||||
|
constructor() {
|
||||||
|
const makeDerivedStore = (store: Writable<BuilderQueryStore>) => {
|
||||||
|
return derived(store, ($store): DerivedQueryStore => {
|
||||||
|
return {
|
||||||
|
list: $store.list,
|
||||||
|
selectedQueryId: $store.selectedQueryId,
|
||||||
|
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
list: [],
|
||||||
|
selectedQueryId: null,
|
||||||
|
},
|
||||||
|
makeDerivedStore
|
||||||
|
)
|
||||||
|
|
||||||
|
this.select = this.select.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const queries = await API.getQueries()
|
||||||
|
sortQueries(queries)
|
||||||
|
this.store.update(state => ({
|
||||||
|
...state,
|
||||||
|
list: queries,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(datasourceId: string, query: SaveQueryRequest) {
|
||||||
|
const _integrations = get(integrations)
|
||||||
|
const dataSource = get(datasources).list.filter(
|
||||||
|
ds => ds._id === datasourceId
|
||||||
|
)
|
||||||
|
// Check if readable attribute is found
|
||||||
|
if (dataSource.length !== 0) {
|
||||||
|
const integration = _integrations[dataSource[0].source]
|
||||||
|
const readable = integration.query[query.queryVerb].readable
|
||||||
|
if (readable) {
|
||||||
|
query.readable = readable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query.datasourceId = datasourceId
|
||||||
|
const savedQuery = await API.saveQuery(query)
|
||||||
|
this.store.update(state => {
|
||||||
|
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||||
|
const queries = state.list
|
||||||
|
if (idx >= 0) {
|
||||||
|
queries.splice(idx, 1, savedQuery)
|
||||||
|
} else {
|
||||||
|
queries.push(savedQuery)
|
||||||
|
}
|
||||||
|
sortQueries(queries)
|
||||||
|
return {
|
||||||
|
list: queries,
|
||||||
|
selectedQueryId: savedQuery._id || null,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return savedQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
async importQueries(data: ImportRestQueryRequest) {
|
||||||
|
return await API.importQueries(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
select(id: string | null) {
|
||||||
|
this.store.update(state => ({
|
||||||
|
...state,
|
||||||
|
selectedQueryId: id,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async preview(query: QueryPreview): Promise<PreviewQueryResponse> {
|
||||||
|
const result = await API.previewQuery(query)
|
||||||
|
// Assume all the fields are strings and create a basic schema from the
|
||||||
|
// unique fields returned by the server
|
||||||
|
const schema: Record<string, QuerySchema> = {}
|
||||||
|
for (let [field, metadata] of Object.entries(result.schema)) {
|
||||||
|
schema[field] = (metadata as QuerySchema) || { type: "string" }
|
||||||
|
}
|
||||||
|
return { ...result, schema, rows: result.rows || [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(query: Query) {
|
||||||
|
if (!query._id || !query._rev) {
|
||||||
|
throw new Error("Query ID or Revision is missing")
|
||||||
|
}
|
||||||
|
await API.deleteQuery(query._id, query._rev)
|
||||||
|
this.store.update(state => ({
|
||||||
|
...state,
|
||||||
|
list: state.list.filter(existing => existing._id !== query._id),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async duplicate(query: Query) {
|
||||||
|
let list = get(this.store).list
|
||||||
|
const newQuery = { ...query }
|
||||||
|
const datasourceId = query.datasourceId
|
||||||
|
|
||||||
|
delete newQuery._id
|
||||||
|
delete newQuery._rev
|
||||||
|
newQuery.name = duplicateName(
|
||||||
|
query.name,
|
||||||
|
list.map(q => q.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await this.save(datasourceId, newQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDatasourceQueries(datasourceId: string) {
|
||||||
|
this.store.update(state => ({
|
||||||
|
...state,
|
||||||
|
list: state.list.filter(table => table.datasourceId !== datasourceId),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
init = this.fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queries = new QueryStore()
|
|
@ -62,7 +62,7 @@ export class RowActionStore extends BudiStore<RowActionState> {
|
||||||
const existingRowActions = get(this)[tableId] || []
|
const existingRowActions = get(this)[tableId] || []
|
||||||
name = getSequentialName(existingRowActions, "New row action ", {
|
name = getSequentialName(existingRowActions, "New row action ", {
|
||||||
getName: x => x.name,
|
getName: x => x.name,
|
||||||
})
|
})!
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { derived, Readable } from "svelte/store"
|
import { derived, Readable } from "svelte/store"
|
||||||
import { admin } from "./admin"
|
import { admin } from "./admin"
|
||||||
import { auth } from "./auth"
|
import { auth } from "./auth"
|
||||||
import { isEnabled } from "@/helpers/featureFlags"
|
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { FeatureFlag } from "@budibase/types"
|
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
title: string
|
title: string
|
||||||
|
@ -73,13 +71,11 @@ export const menu: Readable<MenuItem[]> = derived(
|
||||||
title: "Environment",
|
title: "Environment",
|
||||||
href: "/builder/portal/settings/environment",
|
href: "/builder/portal/settings/environment",
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
if (isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) {
|
|
||||||
settingsSubPages.push({
|
|
||||||
title: "AI",
|
title: "AI",
|
||||||
href: "/builder/portal/settings/ai",
|
href: "/builder/portal/settings/ai",
|
||||||
})
|
},
|
||||||
}
|
]
|
||||||
|
|
||||||
if (!cloud) {
|
if (!cloud) {
|
||||||
settingsSubPages.push({
|
settingsSubPages.push({
|
||||||
|
|
|
@ -1,41 +1,71 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { API } from "@/api"
|
import { API } from "@/api"
|
||||||
import { update } from "lodash"
|
|
||||||
import { licensing } from "."
|
import { licensing } from "."
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
import {
|
||||||
|
DeleteInviteUsersRequest,
|
||||||
|
InviteUsersRequest,
|
||||||
|
SearchUsersRequest,
|
||||||
|
SearchUsersResponse,
|
||||||
|
UpdateInviteRequest,
|
||||||
|
User,
|
||||||
|
UserIdentifier,
|
||||||
|
UnsavedUser,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { BudiStore } from "../BudiStore"
|
||||||
|
|
||||||
export function createUsersStore() {
|
interface UserInfo {
|
||||||
const { subscribe, set } = writable({})
|
email: string
|
||||||
|
password: string
|
||||||
|
forceResetPassword?: boolean
|
||||||
|
role: keyof typeof Constants.BudibaseRoles
|
||||||
|
}
|
||||||
|
|
||||||
// opts can contain page and search params
|
type UserState = SearchUsersResponse & SearchUsersRequest
|
||||||
async function search(opts = {}) {
|
|
||||||
|
class UserStore extends BudiStore<UserState> {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
data: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(opts: SearchUsersRequest = {}) {
|
||||||
const paged = await API.searchUsers(opts)
|
const paged = await API.searchUsers(opts)
|
||||||
set({
|
this.set({
|
||||||
...paged,
|
...paged,
|
||||||
...opts,
|
...opts,
|
||||||
})
|
})
|
||||||
return paged
|
return paged
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(userId) {
|
async get(userId: string) {
|
||||||
try {
|
try {
|
||||||
return await API.getUser(userId)
|
return await API.getUser(userId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fetch = async () => {
|
|
||||||
|
async fetch() {
|
||||||
return await API.getUsers()
|
return await API.getUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// One or more users.
|
async onboard(payload: InviteUsersRequest) {
|
||||||
async function onboard(payload) {
|
|
||||||
return await API.onboardUsers(payload)
|
return await API.onboardUsers(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invite(payload) {
|
async invite(
|
||||||
const users = payload.map(user => {
|
payload: {
|
||||||
|
admin?: boolean
|
||||||
|
builder?: boolean
|
||||||
|
creator?: boolean
|
||||||
|
email: string
|
||||||
|
apps?: any[]
|
||||||
|
groups?: any[]
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
const users: InviteUsersRequest = payload.map(user => {
|
||||||
let builder = undefined
|
let builder = undefined
|
||||||
if (user.admin || user.builder) {
|
if (user.admin || user.builder) {
|
||||||
builder = { global: true }
|
builder = { global: true }
|
||||||
|
@ -55,11 +85,16 @@ export function createUsersStore() {
|
||||||
return API.inviteUsers(users)
|
return API.inviteUsers(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeInvites(payload) {
|
async removeInvites(payload: DeleteInviteUsersRequest) {
|
||||||
return API.removeUserInvites(payload)
|
return API.removeUserInvites(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function acceptInvite(inviteCode, password, firstName, lastName) {
|
async acceptInvite(
|
||||||
|
inviteCode: string,
|
||||||
|
password: string,
|
||||||
|
firstName: string,
|
||||||
|
lastName?: string
|
||||||
|
) {
|
||||||
return API.acceptInvite({
|
return API.acceptInvite({
|
||||||
inviteCode,
|
inviteCode,
|
||||||
password,
|
password,
|
||||||
|
@ -68,21 +103,25 @@ export function createUsersStore() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchInvite(inviteCode) {
|
async fetchInvite(inviteCode: string) {
|
||||||
return API.getUserInvite(inviteCode)
|
return API.getUserInvite(inviteCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getInvites() {
|
async getInvites() {
|
||||||
return API.getUserInvites()
|
return API.getUserInvites()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateInvite(invite) {
|
async updateInvite(code: string, invite: UpdateInviteRequest) {
|
||||||
return API.updateUserInvite(invite.code, invite)
|
return API.updateUserInvite(code, invite)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(data) {
|
async getUserCountByApp(appId: string) {
|
||||||
let mappedUsers = data.users.map(user => {
|
return await API.getUserCountByApp(appId)
|
||||||
const body = {
|
}
|
||||||
|
|
||||||
|
async create(data: { users: UserInfo[]; groups: any[] }) {
|
||||||
|
let mappedUsers: UnsavedUser[] = data.users.map((user: any) => {
|
||||||
|
const body: UnsavedUser = {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
password: user.password,
|
password: user.password,
|
||||||
roles: {},
|
roles: {},
|
||||||
|
@ -92,17 +131,17 @@ export function createUsersStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (user.role) {
|
switch (user.role) {
|
||||||
case "appUser":
|
case Constants.BudibaseRoles.AppUser:
|
||||||
body.builder = { global: false }
|
body.builder = { global: false }
|
||||||
body.admin = { global: false }
|
body.admin = { global: false }
|
||||||
break
|
break
|
||||||
case "developer":
|
case Constants.BudibaseRoles.Developer:
|
||||||
body.builder = { global: true }
|
body.builder = { global: true }
|
||||||
break
|
break
|
||||||
case "creator":
|
case Constants.BudibaseRoles.Creator:
|
||||||
body.builder = { creator: true, global: false }
|
body.builder = { creator: true, global: false }
|
||||||
break
|
break
|
||||||
case "admin":
|
case Constants.BudibaseRoles.Admin:
|
||||||
body.admin = { global: true }
|
body.admin = { global: true }
|
||||||
body.builder = { global: true }
|
body.builder = { global: true }
|
||||||
break
|
break
|
||||||
|
@ -111,43 +150,47 @@ export function createUsersStore() {
|
||||||
return body
|
return body
|
||||||
})
|
})
|
||||||
const response = await API.createUsers(mappedUsers, data.groups)
|
const response = await API.createUsers(mappedUsers, data.groups)
|
||||||
|
licensing.setQuotaUsage()
|
||||||
|
|
||||||
// re-search from first page
|
// re-search from first page
|
||||||
await search()
|
await this.search()
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del(id) {
|
async delete(id: string) {
|
||||||
await API.deleteUser(id)
|
await API.deleteUser(id)
|
||||||
update(users => users.filter(user => user._id !== id))
|
licensing.setQuotaUsage()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserCountByApp(appId) {
|
async bulkDelete(users: UserIdentifier[]) {
|
||||||
return await API.getUserCountByApp(appId)
|
const res = API.deleteUsers(users)
|
||||||
|
licensing.setQuotaUsage()
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkDelete(users) {
|
async save(user: User) {
|
||||||
return API.deleteUsers(users)
|
const res = await API.saveUser(user)
|
||||||
|
licensing.setQuotaUsage()
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save(user) {
|
async addAppBuilder(userId: string, appId: string) {
|
||||||
return await API.saveUser(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addAppBuilder(userId, appId) {
|
|
||||||
return await API.addAppBuilder(userId, appId)
|
return await API.addAppBuilder(userId, appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeAppBuilder(userId, appId) {
|
async removeAppBuilder(userId: string, appId: string) {
|
||||||
return await API.removeAppBuilder(userId, appId)
|
return await API.removeAppBuilder(userId, appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAccountHolder() {
|
async getAccountHolder() {
|
||||||
return await API.getAccountHolder()
|
return await API.getAccountHolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserRole = user => {
|
getUserRole(user?: User & { tenantOwnerEmail?: string }) {
|
||||||
if (user && user.email === user.tenantOwnerEmail) {
|
if (!user) {
|
||||||
|
return Constants.BudibaseRoles.AppUser
|
||||||
|
}
|
||||||
|
if (user.email === user.tenantOwnerEmail) {
|
||||||
return Constants.BudibaseRoles.Owner
|
return Constants.BudibaseRoles.Owner
|
||||||
} else if (sdk.users.isAdmin(user)) {
|
} else if (sdk.users.isAdmin(user)) {
|
||||||
return Constants.BudibaseRoles.Admin
|
return Constants.BudibaseRoles.Admin
|
||||||
|
@ -159,38 +202,6 @@ export function createUsersStore() {
|
||||||
return Constants.BudibaseRoles.AppUser
|
return Constants.BudibaseRoles.AppUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshUsage =
|
|
||||||
fn =>
|
|
||||||
async (...args) => {
|
|
||||||
const response = await fn(...args)
|
|
||||||
await licensing.setQuotaUsage()
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
search,
|
|
||||||
get,
|
|
||||||
getUserRole,
|
|
||||||
fetch,
|
|
||||||
invite,
|
|
||||||
onboard,
|
|
||||||
fetchInvite,
|
|
||||||
getInvites,
|
|
||||||
removeInvites,
|
|
||||||
updateInvite,
|
|
||||||
getUserCountByApp,
|
|
||||||
addAppBuilder,
|
|
||||||
removeAppBuilder,
|
|
||||||
// any operation that adds or deletes users
|
|
||||||
acceptInvite,
|
|
||||||
create: refreshUsage(create),
|
|
||||||
save: refreshUsage(save),
|
|
||||||
bulkDelete: refreshUsage(bulkDelete),
|
|
||||||
delete: refreshUsage(del),
|
|
||||||
getAccountHolder,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const users = createUsersStore()
|
export const users = new UserStore()
|
|
@ -5139,7 +5139,8 @@
|
||||||
{
|
{
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"label": "File name",
|
"label": "File name",
|
||||||
"key": "key"
|
"key": "key",
|
||||||
|
"nested": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "event",
|
"type": "event",
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { createAPIClient } from "@budibase/frontend-core"
|
import { createAPIClient } from "@budibase/frontend-core"
|
||||||
import { authStore } from "../stores/auth.js"
|
import { authStore } from "../stores/auth"
|
||||||
import { notificationStore, devToolsEnabled, devToolsStore } from "../stores/"
|
import {
|
||||||
|
notificationStore,
|
||||||
|
devToolsEnabled,
|
||||||
|
devToolsStore,
|
||||||
|
} from "../stores/index"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export const API = createAPIClient({
|
export const API = createAPIClient({
|
|
@ -1,5 +1,5 @@
|
||||||
import { API } from "./api.js"
|
import { API } from "./api"
|
||||||
import { patchAPI } from "./patches.js"
|
import { patchAPI } from "./patches"
|
||||||
|
|
||||||
// Certain endpoints which return rows need patched so that they transform
|
// Certain endpoints which return rows need patched so that they transform
|
||||||
// and enrich the row docs, so that they can be correctly handled by the
|
// and enrich the row docs, so that they can be correctly handled by the
|
|
@ -1,19 +1,20 @@
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants, APIClient } from "@budibase/frontend-core"
|
||||||
import { FieldTypes } from "../constants"
|
import { FieldTypes } from "../constants"
|
||||||
|
import { Row, Table } from "@budibase/types"
|
||||||
|
|
||||||
export const patchAPI = API => {
|
export const patchAPI = (API: APIClient) => {
|
||||||
/**
|
/**
|
||||||
* Enriches rows which contain certain field types so that they can
|
* Enriches rows which contain certain field types so that they can
|
||||||
* be properly displayed.
|
* be properly displayed.
|
||||||
* The ability to create these bindings has been removed, but they will still
|
* The ability to create these bindings has been removed, but they will still
|
||||||
* exist in client apps to support backwards compatibility.
|
* exist in client apps to support backwards compatibility.
|
||||||
*/
|
*/
|
||||||
const enrichRows = async (rows, tableId) => {
|
const enrichRows = async (rows: Row[], tableId: string) => {
|
||||||
if (!Array.isArray(rows)) {
|
if (!Array.isArray(rows)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
if (rows.length) {
|
if (rows.length) {
|
||||||
const tables = {}
|
const tables: Record<string, Table> = {}
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
// Fall back to passed in tableId if row doesn't have it specified
|
// Fall back to passed in tableId if row doesn't have it specified
|
||||||
let rowTableId = row.tableId || tableId
|
let rowTableId = row.tableId || tableId
|
||||||
|
@ -54,7 +55,7 @@ export const patchAPI = API => {
|
||||||
const fetchSelf = API.fetchSelf
|
const fetchSelf = API.fetchSelf
|
||||||
API.fetchSelf = async () => {
|
API.fetchSelf = async () => {
|
||||||
const user = await fetchSelf()
|
const user = await fetchSelf()
|
||||||
if (user && user._id) {
|
if (user && "_id" in user && user._id) {
|
||||||
if (user.roleId === "PUBLIC") {
|
if (user.roleId === "PUBLIC") {
|
||||||
// Don't try to enrich a public user as it will 403
|
// Don't try to enrich a public user as it will 403
|
||||||
return user
|
return user
|
||||||
|
@ -90,13 +91,14 @@ export const patchAPI = API => {
|
||||||
return await enrichRows(rows, tableId)
|
return await enrichRows(rows, tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wipe any HBS formulae from table definitions, as these interfere with
|
// Wipe any HBS formulas from table definitions, as these interfere with
|
||||||
// handlebars enrichment
|
// handlebars enrichment
|
||||||
const fetchTableDefinition = API.fetchTableDefinition
|
const fetchTableDefinition = API.fetchTableDefinition
|
||||||
API.fetchTableDefinition = async tableId => {
|
API.fetchTableDefinition = async tableId => {
|
||||||
const definition = await fetchTableDefinition(tableId)
|
const definition = await fetchTableDefinition(tableId)
|
||||||
Object.keys(definition?.schema || {}).forEach(field => {
|
Object.keys(definition?.schema || {}).forEach(field => {
|
||||||
if (definition.schema[field]?.type === "formula") {
|
if (definition.schema[field]?.type === "formula") {
|
||||||
|
// @ts-expect-error TODO check what use case removing that would break
|
||||||
delete definition.schema[field].formula
|
delete definition.schema[field].formula
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onDestroy, onMount, setContext } from "svelte"
|
import { getContext, onDestroy, onMount, setContext } from "svelte"
|
||||||
import { builderStore } from "stores/builder.js"
|
import { builderStore } from "stores/builder.js"
|
||||||
import { blockStore } from "stores/blocks.js"
|
import { blockStore } from "stores/blocks"
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable } = getContext("sdk")
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
getActionContextKey,
|
getActionContextKey,
|
||||||
getActionDependentContextKeys,
|
getActionDependentContextKeys,
|
||||||
} from "../utils/buttonActions.js"
|
} from "../utils/buttonActions.js"
|
||||||
import { gridLayout } from "utils/grid.js"
|
import { gridLayout } from "utils/grid"
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
export let parent = null
|
export let parent = null
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import Block from "components/Block.svelte"
|
import Block from "components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
import { enrichSearchColumns, enrichFilter } from "utils/blocks"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import Block from "components/Block.svelte"
|
import Block from "components/Block.svelte"
|
||||||
import BlockComponent from "components/BlockComponent.svelte"
|
import BlockComponent from "components/BlockComponent.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { enrichSearchColumns, enrichFilter } from "utils/blocks.js"
|
import { enrichSearchColumns, enrichFilter } from "utils/blocks"
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import Field from "./Field.svelte"
|
import Field from "./Field.svelte"
|
||||||
import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui"
|
import { CoreDropzone, ProgressCircle, Helpers } from "@budibase/bbui"
|
||||||
import { getContext, onMount, onDestroy } from "svelte"
|
import { getContext, onMount, onDestroy } from "svelte"
|
||||||
|
import { builderStore } from "stores/builder.js"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let datasourceId
|
export let datasourceId
|
||||||
export let bucket
|
export let bucket
|
||||||
|
@ -12,6 +14,8 @@
|
||||||
export let validation
|
export let validation
|
||||||
export let onChange
|
export let onChange
|
||||||
|
|
||||||
|
const context = getContext("context")
|
||||||
|
|
||||||
let fieldState
|
let fieldState
|
||||||
let fieldApi
|
let fieldApi
|
||||||
let localFiles = []
|
let localFiles = []
|
||||||
|
@ -42,6 +46,9 @@
|
||||||
// Process the file input and return a serializable structure expected by
|
// Process the file input and return a serializable structure expected by
|
||||||
// the dropzone component to display the file
|
// the dropzone component to display the file
|
||||||
const processFiles = async fileList => {
|
const processFiles = async fileList => {
|
||||||
|
if ($builderStore.inBuilder) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
return await new Promise(resolve => {
|
return await new Promise(resolve => {
|
||||||
if (!fileList?.length) {
|
if (!fileList?.length) {
|
||||||
return []
|
return []
|
||||||
|
@ -78,9 +85,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const upload = async () => {
|
const upload = async () => {
|
||||||
|
const processedFileKey = processStringSync(key, $context)
|
||||||
loading = true
|
loading = true
|
||||||
try {
|
try {
|
||||||
const res = await API.externalUpload(datasourceId, bucket, key, data)
|
const res = await API.externalUpload(
|
||||||
|
datasourceId,
|
||||||
|
bucket,
|
||||||
|
processedFileKey,
|
||||||
|
data
|
||||||
|
)
|
||||||
notificationStore.actions.success("File uploaded successfully")
|
notificationStore.actions.success("File uploaded successfully")
|
||||||
loading = false
|
loading = false
|
||||||
return res
|
return res
|
||||||
|
@ -126,7 +139,7 @@
|
||||||
bind:fieldApi
|
bind:fieldApi
|
||||||
defaultValue={[]}
|
defaultValue={[]}
|
||||||
>
|
>
|
||||||
<div class="content">
|
<div class="content" class:builder={$builderStore.inBuilder}>
|
||||||
{#if fieldState}
|
{#if fieldState}
|
||||||
<CoreDropzone
|
<CoreDropzone
|
||||||
value={localFiles}
|
value={localFiles}
|
||||||
|
@ -149,6 +162,9 @@
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.content.builder :global(.spectrum-Dropzone) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
interface Window {
|
||||||
|
"##BUDIBASE_APP_ID##": string
|
||||||
|
"##BUDIBASE_IN_BUILDER##": string
|
||||||
|
MIGRATING_APP: boolean
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ import { ActionTypes } from "./constants"
|
||||||
import {
|
import {
|
||||||
fetchDatasourceSchema,
|
fetchDatasourceSchema,
|
||||||
fetchDatasourceDefinition,
|
fetchDatasourceDefinition,
|
||||||
} from "./utils/schema.js"
|
} from "./utils/schema"
|
||||||
import { getAPIKey } from "./utils/api.js"
|
import { getAPIKey } from "./utils/api.js"
|
||||||
import { enrichButtonActions } from "./utils/buttonActions.js"
|
import { enrichButtonActions } from "./utils/buttonActions.js"
|
||||||
import { processStringSync, makePropSafe } from "@budibase/string-templates"
|
import { processStringSync, makePropSafe } from "@budibase/string-templates"
|
||||||
|
@ -74,6 +74,7 @@ export default {
|
||||||
fetchData,
|
fetchData,
|
||||||
QueryUtils,
|
QueryUtils,
|
||||||
ContextScopes: Constants.ContextScopes,
|
ContextScopes: Constants.ContextScopes,
|
||||||
|
// This is not used internally but exposed to users to be used in plugins
|
||||||
getAPIKey,
|
getAPIKey,
|
||||||
enrichButtonActions,
|
enrichButtonActions,
|
||||||
processStringSync,
|
processStringSync,
|
||||||
|
|
|
@ -2,7 +2,9 @@ import { API } from "api"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
const createAuthStore = () => {
|
const createAuthStore = () => {
|
||||||
const store = writable(null)
|
const store = writable<{
|
||||||
|
csrfToken?: string
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
// Fetches the user object if someone is logged in and has reloaded the page
|
// Fetches the user object if someone is logged in and has reloaded the page
|
||||||
const fetchUser = async () => {
|
const fetchUser = async () => {
|
|
@ -1,7 +1,7 @@
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { devToolsStore } from "../devTools.js"
|
import { devToolsStore } from "../devTools.js"
|
||||||
import { authStore } from "../auth.js"
|
import { authStore } from "../auth"
|
||||||
import { devToolsEnabled } from "./devToolsEnabled.js"
|
import { devToolsEnabled } from "./devToolsEnabled.js"
|
||||||
|
|
||||||
// Derive the current role of the logged-in user
|
// Derive the current role of the logged-in user
|
||||||
|
|
|
@ -6,7 +6,7 @@ const DEFAULT_NOTIFICATION_TIMEOUT = 3000
|
||||||
const createNotificationStore = () => {
|
const createNotificationStore = () => {
|
||||||
let block = false
|
let block = false
|
||||||
|
|
||||||
const store = writable([])
|
const store = writable<{ id: string; message: string; count: number }[]>([])
|
||||||
|
|
||||||
const blockNotifications = (timeout = 1000) => {
|
const blockNotifications = (timeout = 1000) => {
|
||||||
block = true
|
block = true
|
||||||
|
@ -14,11 +14,11 @@ const createNotificationStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const send = (
|
const send = (
|
||||||
message,
|
message: string,
|
||||||
type = "info",
|
type = "info",
|
||||||
icon,
|
icon: string,
|
||||||
autoDismiss = true,
|
autoDismiss = true,
|
||||||
duration,
|
duration?: number,
|
||||||
count = 1
|
count = 1
|
||||||
) => {
|
) => {
|
||||||
if (block) {
|
if (block) {
|
||||||
|
@ -66,7 +66,7 @@ const createNotificationStore = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dismiss = id => {
|
const dismiss = (id: string) => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return state.filter(n => n.id !== id)
|
return state.filter(n => n.id !== id)
|
||||||
})
|
})
|
||||||
|
@ -76,13 +76,13 @@ const createNotificationStore = () => {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
actions: {
|
actions: {
|
||||||
send,
|
send,
|
||||||
info: (msg, autoDismiss, duration) =>
|
info: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||||
send(msg, "info", "Info", autoDismiss ?? true, duration),
|
send(msg, "info", "Info", autoDismiss ?? true, duration),
|
||||||
success: (msg, autoDismiss, duration) =>
|
success: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||||
send(msg, "success", "CheckmarkCircle", autoDismiss ?? true, duration),
|
send(msg, "success", "CheckmarkCircle", autoDismiss ?? true, duration),
|
||||||
warning: (msg, autoDismiss, duration) =>
|
warning: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||||
send(msg, "warning", "Alert", autoDismiss ?? true, duration),
|
send(msg, "warning", "Alert", autoDismiss ?? true, duration),
|
||||||
error: (msg, autoDismiss, duration) =>
|
error: (msg: string, autoDismiss?: boolean, duration?: number) =>
|
||||||
send(msg, "error", "Alert", autoDismiss ?? false, duration),
|
send(msg, "error", "Alert", autoDismiss ?? false, duration),
|
||||||
blockNotifications,
|
blockNotifications,
|
||||||
dismiss,
|
dismiss,
|
|
@ -4,8 +4,24 @@ import { API } from "api"
|
||||||
import { peekStore } from "./peek"
|
import { peekStore } from "./peek"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
|
|
||||||
|
interface Route {
|
||||||
|
path: string
|
||||||
|
screenId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoreType {
|
||||||
|
routes: Route[]
|
||||||
|
routeParams: {}
|
||||||
|
activeRoute?: Route | null
|
||||||
|
routeSessionId: number
|
||||||
|
routerLoaded: boolean
|
||||||
|
queryParams?: {
|
||||||
|
peek?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createRouteStore = () => {
|
const createRouteStore = () => {
|
||||||
const initialState = {
|
const initialState: StoreType = {
|
||||||
routes: [],
|
routes: [],
|
||||||
routeParams: {},
|
routeParams: {},
|
||||||
activeRoute: null,
|
activeRoute: null,
|
||||||
|
@ -22,7 +38,7 @@ const createRouteStore = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
routeConfig = null
|
routeConfig = null
|
||||||
}
|
}
|
||||||
let routes = []
|
const routes: Route[] = []
|
||||||
Object.values(routeConfig?.routes || {}).forEach(route => {
|
Object.values(routeConfig?.routes || {}).forEach(route => {
|
||||||
Object.entries(route.subpaths || {}).forEach(([path, config]) => {
|
Object.entries(route.subpaths || {}).forEach(([path, config]) => {
|
||||||
routes.push({
|
routes.push({
|
||||||
|
@ -43,13 +59,13 @@ const createRouteStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const setRouteParams = routeParams => {
|
const setRouteParams = (routeParams: StoreType["routeParams"]) => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.routeParams = routeParams
|
state.routeParams = routeParams
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const setQueryParams = queryParams => {
|
const setQueryParams = (queryParams: { peek?: boolean }) => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.queryParams = {
|
state.queryParams = {
|
||||||
...queryParams,
|
...queryParams,
|
||||||
|
@ -60,13 +76,13 @@ const createRouteStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const setActiveRoute = route => {
|
const setActiveRoute = (route: string) => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.activeRoute = state.routes.find(x => x.path === route)
|
state.activeRoute = state.routes.find(x => x.path === route)
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const navigate = (url, peek, externalNewTab) => {
|
const navigate = (url: string, peek: boolean, externalNewTab: boolean) => {
|
||||||
if (get(builderStore).inBuilder) {
|
if (get(builderStore).inBuilder) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -93,7 +109,7 @@ const createRouteStore = () => {
|
||||||
const setRouterLoaded = () => {
|
const setRouterLoaded = () => {
|
||||||
store.update(state => ({ ...state, routerLoaded: true }))
|
store.update(state => ({ ...state, routerLoaded: true }))
|
||||||
}
|
}
|
||||||
const createFullURL = relativeURL => {
|
const createFullURL = (relativeURL: string) => {
|
||||||
if (!relativeURL?.startsWith("/")) {
|
if (!relativeURL?.startsWith("/")) {
|
||||||
return relativeURL
|
return relativeURL
|
||||||
}
|
}
|
|
@ -1,10 +1,16 @@
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { API } from "../api/index.js"
|
import { API } from "../api"
|
||||||
import { UILogicalOperator } from "@budibase/types"
|
import {
|
||||||
|
BasicOperator,
|
||||||
|
LegacyFilter,
|
||||||
|
UIColumn,
|
||||||
|
UILogicalOperator,
|
||||||
|
UISearchFilter,
|
||||||
|
} from "@budibase/types"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
// Map of data types to component types for search fields inside blocks
|
// Map of data types to component types for search fields inside blocks
|
||||||
const schemaComponentMap = {
|
const schemaComponentMap: Record<string, string> = {
|
||||||
string: "stringfield",
|
string: "stringfield",
|
||||||
options: "optionsfield",
|
options: "optionsfield",
|
||||||
number: "numberfield",
|
number: "numberfield",
|
||||||
|
@ -19,7 +25,16 @@ const schemaComponentMap = {
|
||||||
* @param searchColumns the search columns to use
|
* @param searchColumns the search columns to use
|
||||||
* @param schema the datasource schema
|
* @param schema the datasource schema
|
||||||
*/
|
*/
|
||||||
export const enrichSearchColumns = async (searchColumns, schema) => {
|
export const enrichSearchColumns = async (
|
||||||
|
searchColumns: string[],
|
||||||
|
schema: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
tableId: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) => {
|
||||||
if (!searchColumns?.length || !schema) {
|
if (!searchColumns?.length || !schema) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -61,12 +76,16 @@ export const enrichSearchColumns = async (searchColumns, schema) => {
|
||||||
* @param columns the enriched search column structure
|
* @param columns the enriched search column structure
|
||||||
* @param formId the ID of the form containing the search fields
|
* @param formId the ID of the form containing the search fields
|
||||||
*/
|
*/
|
||||||
export const enrichFilter = (filter, columns, formId) => {
|
export const enrichFilter = (
|
||||||
|
filter: UISearchFilter,
|
||||||
|
columns: UIColumn[],
|
||||||
|
formId: string
|
||||||
|
) => {
|
||||||
if (!columns?.length) {
|
if (!columns?.length) {
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
|
||||||
let newFilters = []
|
const newFilters: LegacyFilter[] = []
|
||||||
columns?.forEach(column => {
|
columns?.forEach(column => {
|
||||||
const safePath = column.name.split(".").map(safe).join(".")
|
const safePath = column.name.split(".").map(safe).join(".")
|
||||||
const stringType = column.type === "string" || column.type === "formula"
|
const stringType = column.type === "string" || column.type === "formula"
|
||||||
|
@ -99,7 +118,7 @@ export const enrichFilter = (filter, columns, formId) => {
|
||||||
newFilters.push({
|
newFilters.push({
|
||||||
field: column.name,
|
field: column.name,
|
||||||
type: column.type,
|
type: column.type,
|
||||||
operator: stringType ? "string" : "equal",
|
operator: stringType ? BasicOperator.STRING : BasicOperator.EQUAL,
|
||||||
valueType: "Binding",
|
valueType: "Binding",
|
||||||
value: `{{ ${binding} }}`,
|
value: `{{ ${binding} }}`,
|
||||||
})
|
})
|
|
@ -1,7 +1,27 @@
|
||||||
import { GridSpacing, GridRowHeight } from "constants"
|
import { GridSpacing, GridRowHeight } from "@/constants"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
import { buildStyleString } from "utils/styleable.js"
|
import { buildStyleString } from "utils/styleable.js"
|
||||||
|
|
||||||
|
interface GridMetadata {
|
||||||
|
id: string
|
||||||
|
styles: Record<string, string | number> & {
|
||||||
|
"--default-width"?: number
|
||||||
|
"--default-height"?: number
|
||||||
|
}
|
||||||
|
interactive: boolean
|
||||||
|
errored: boolean
|
||||||
|
definition?: {
|
||||||
|
size?: {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
grid?: { hAlign: string; vAlign: string }
|
||||||
|
}
|
||||||
|
draggable: boolean
|
||||||
|
insideGrid: boolean
|
||||||
|
ignoresLayout: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We use CSS variables on components to control positioning and layout of
|
* We use CSS variables on components to control positioning and layout of
|
||||||
* components inside grids.
|
* components inside grids.
|
||||||
|
@ -44,14 +64,17 @@ export const GridDragModes = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds a CSS variable name for a certain piece of grid metadata
|
// Builds a CSS variable name for a certain piece of grid metadata
|
||||||
export const getGridVar = (device, param) => `--grid-${device}-${param}`
|
export const getGridVar = (device: string, param: string) =>
|
||||||
|
`--grid-${device}-${param}`
|
||||||
|
|
||||||
// Determines whether a JS event originated from immediately within a grid
|
// Determines whether a JS event originated from immediately within a grid
|
||||||
export const isGridEvent = e => {
|
export const isGridEvent = (e: Event & { target: HTMLElement }): boolean => {
|
||||||
return (
|
return (
|
||||||
e.target.dataset?.indicator === "true" ||
|
e.target.dataset?.indicator === "true" ||
|
||||||
|
// @ts-expect-error: api is not properly typed
|
||||||
e.target
|
e.target
|
||||||
.closest?.(".component")
|
.closest?.(".component")
|
||||||
|
// @ts-expect-error
|
||||||
?.parentNode.closest(".component")
|
?.parentNode.closest(".component")
|
||||||
?.childNodes[0]?.classList?.contains("grid")
|
?.childNodes[0]?.classList?.contains("grid")
|
||||||
)
|
)
|
||||||
|
@ -59,11 +82,11 @@ export const isGridEvent = e => {
|
||||||
|
|
||||||
// Svelte action to apply required class names and styles to our component
|
// Svelte action to apply required class names and styles to our component
|
||||||
// wrappers
|
// wrappers
|
||||||
export const gridLayout = (node, metadata) => {
|
export const gridLayout = (node: HTMLDivElement, metadata: GridMetadata) => {
|
||||||
let selectComponent
|
let selectComponent: ((e: Event) => void) | null
|
||||||
|
|
||||||
// Applies the required listeners, CSS and classes to a component DOM node
|
// Applies the required listeners, CSS and classes to a component DOM node
|
||||||
const applyMetadata = metadata => {
|
const applyMetadata = (metadata: GridMetadata) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
styles,
|
styles,
|
||||||
|
@ -86,7 +109,7 @@ export const gridLayout = (node, metadata) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback to select the component when clicking on the wrapper
|
// Callback to select the component when clicking on the wrapper
|
||||||
selectComponent = e => {
|
selectComponent = (e: Event) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
builderStore.actions.selectComponent(id)
|
builderStore.actions.selectComponent(id)
|
||||||
}
|
}
|
||||||
|
@ -100,7 +123,7 @@ export const gridLayout = (node, metadata) => {
|
||||||
}
|
}
|
||||||
width += 2 * GridSpacing
|
width += 2 * GridSpacing
|
||||||
height += 2 * GridSpacing
|
height += 2 * GridSpacing
|
||||||
let vars = {
|
const vars: Record<string, string | number> = {
|
||||||
"--default-width": width,
|
"--default-width": width,
|
||||||
"--default-height": height,
|
"--default-height": height,
|
||||||
}
|
}
|
||||||
|
@ -135,7 +158,7 @@ export const gridLayout = (node, metadata) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply some metadata to data attributes to speed up lookups
|
// Apply some metadata to data attributes to speed up lookups
|
||||||
const addDataTag = (tagName, device, param) => {
|
const addDataTag = (tagName: string, device: string, param: string) => {
|
||||||
const val = `${vars[getGridVar(device, param)]}`
|
const val = `${vars[getGridVar(device, param)]}`
|
||||||
if (node.dataset[tagName] !== val) {
|
if (node.dataset[tagName] !== val) {
|
||||||
node.dataset[tagName] = val
|
node.dataset[tagName] = val
|
||||||
|
@ -147,11 +170,12 @@ export const gridLayout = (node, metadata) => {
|
||||||
addDataTag("gridMobileHAlign", Devices.Mobile, GridParams.HAlign)
|
addDataTag("gridMobileHAlign", Devices.Mobile, GridParams.HAlign)
|
||||||
addDataTag("gridDesktopVAlign", Devices.Desktop, GridParams.VAlign)
|
addDataTag("gridDesktopVAlign", Devices.Desktop, GridParams.VAlign)
|
||||||
addDataTag("gridMobileVAlign", Devices.Mobile, GridParams.VAlign)
|
addDataTag("gridMobileVAlign", Devices.Mobile, GridParams.VAlign)
|
||||||
if (node.dataset.insideGrid !== true) {
|
if (node.dataset.insideGrid !== "true") {
|
||||||
node.dataset.insideGrid = true
|
node.dataset.insideGrid = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all CSS variables to the wrapper
|
// Apply all CSS variables to the wrapper
|
||||||
|
// @ts-expect-error TODO
|
||||||
node.style = buildStyleString(vars)
|
node.style = buildStyleString(vars)
|
||||||
|
|
||||||
// Add a listener to select this node on click
|
// Add a listener to select this node on click
|
||||||
|
@ -160,7 +184,7 @@ export const gridLayout = (node, metadata) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add draggable attribute
|
// Add draggable attribute
|
||||||
node.setAttribute("draggable", !!draggable)
|
node.setAttribute("draggable", (!!draggable).toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removes the previously set up listeners
|
// Removes the previously set up listeners
|
||||||
|
@ -176,7 +200,7 @@ export const gridLayout = (node, metadata) => {
|
||||||
applyMetadata(metadata)
|
applyMetadata(metadata)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update(newMetadata) {
|
update(newMetadata: GridMetadata) {
|
||||||
removeListeners()
|
removeListeners()
|
||||||
applyMetadata(newMetadata)
|
applyMetadata(newMetadata)
|
||||||
},
|
},
|
|
@ -1,8 +1,8 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { link } from "svelte-spa-router"
|
import { link, LinkActionOpts } from "svelte-spa-router"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
|
|
||||||
export const linkable = (node, href) => {
|
export const linkable = (node: HTMLElement, href?: LinkActionOpts) => {
|
||||||
if (get(builderStore).inBuilder) {
|
if (get(builderStore).inBuilder) {
|
||||||
node.onclick = e => {
|
node.onclick = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
|
@ -1,13 +1,5 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import TableFetch from "@budibase/frontend-core/src/fetch/TableFetch"
|
import { DataFetchMap, DataFetchType } from "@budibase/frontend-core"
|
||||||
import ViewFetch from "@budibase/frontend-core/src/fetch/ViewFetch"
|
|
||||||
import QueryFetch from "@budibase/frontend-core/src/fetch/QueryFetch"
|
|
||||||
import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFetch"
|
|
||||||
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch"
|
|
||||||
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch"
|
|
||||||
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch"
|
|
||||||
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch"
|
|
||||||
import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a fetch instance for a given datasource.
|
* Constructs a fetch instance for a given datasource.
|
||||||
|
@ -16,22 +8,20 @@ import QueryArrayFetch from "@budibase/frontend-core/src/fetch/QueryArrayFetch"
|
||||||
* @param datasource the datasource
|
* @param datasource the datasource
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getDatasourceFetchInstance = datasource => {
|
const getDatasourceFetchInstance = <
|
||||||
const handler = {
|
TDatasource extends { type: DataFetchType }
|
||||||
table: TableFetch,
|
>(
|
||||||
view: ViewFetch,
|
datasource: TDatasource
|
||||||
viewV2: ViewV2Fetch,
|
) => {
|
||||||
query: QueryFetch,
|
const handler = DataFetchMap[datasource?.type]
|
||||||
link: RelationshipFetch,
|
|
||||||
provider: NestedProviderFetch,
|
|
||||||
field: FieldFetch,
|
|
||||||
jsonarray: JSONArrayFetch,
|
|
||||||
queryarray: QueryArrayFetch,
|
|
||||||
}[datasource?.type]
|
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return new handler({ API })
|
return new handler({
|
||||||
|
API,
|
||||||
|
datasource: datasource as never,
|
||||||
|
query: null as any,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,21 +29,23 @@ const getDatasourceFetchInstance = datasource => {
|
||||||
* @param datasource the datasource to fetch the schema for
|
* @param datasource the datasource to fetch the schema for
|
||||||
* @param options options for enriching the schema
|
* @param options options for enriching the schema
|
||||||
*/
|
*/
|
||||||
export const fetchDatasourceSchema = async (
|
export const fetchDatasourceSchema = async <
|
||||||
datasource,
|
TDatasource extends { type: DataFetchType }
|
||||||
|
>(
|
||||||
|
datasource: TDatasource,
|
||||||
options = { enrichRelationships: false, formSchema: false }
|
options = { enrichRelationships: false, formSchema: false }
|
||||||
) => {
|
) => {
|
||||||
const instance = getDatasourceFetchInstance(datasource)
|
const instance = getDatasourceFetchInstance(datasource)
|
||||||
const definition = await instance?.getDefinition(datasource)
|
const definition = await instance?.getDefinition()
|
||||||
if (!definition) {
|
if (!instance || !definition) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the normal schema as long as we aren't wanting a form schema
|
// Get the normal schema as long as we aren't wanting a form schema
|
||||||
let schema
|
let schema: any
|
||||||
if (datasource?.type !== "query" || !options?.formSchema) {
|
if (datasource?.type !== "query" || !options?.formSchema) {
|
||||||
schema = instance.getSchema(datasource, definition)
|
schema = instance.getSchema(definition as any)
|
||||||
} else if (definition.parameters?.length) {
|
} else if ("parameters" in definition && definition.parameters?.length) {
|
||||||
schema = {}
|
schema = {}
|
||||||
definition.parameters.forEach(param => {
|
definition.parameters.forEach(param => {
|
||||||
schema[param.name] = { ...param, type: "string" }
|
schema[param.name] = { ...param, type: "string" }
|
||||||
|
@ -73,7 +65,12 @@ export const fetchDatasourceSchema = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enrich schema with relationships if required
|
// Enrich schema with relationships if required
|
||||||
if (definition?.sql && options?.enrichRelationships) {
|
if (
|
||||||
|
definition &&
|
||||||
|
"sql" in definition &&
|
||||||
|
definition.sql &&
|
||||||
|
options?.enrichRelationships
|
||||||
|
) {
|
||||||
const relationshipAdditions = await getRelationshipSchemaAdditions(schema)
|
const relationshipAdditions = await getRelationshipSchemaAdditions(schema)
|
||||||
schema = {
|
schema = {
|
||||||
...schema,
|
...schema,
|
||||||
|
@ -89,20 +86,26 @@ export const fetchDatasourceSchema = async (
|
||||||
* Fetches the definition of any kind of datasource.
|
* Fetches the definition of any kind of datasource.
|
||||||
* @param datasource the datasource to fetch the schema for
|
* @param datasource the datasource to fetch the schema for
|
||||||
*/
|
*/
|
||||||
export const fetchDatasourceDefinition = async datasource => {
|
export const fetchDatasourceDefinition = async <
|
||||||
|
TDatasource extends { type: DataFetchType }
|
||||||
|
>(
|
||||||
|
datasource: TDatasource
|
||||||
|
) => {
|
||||||
const instance = getDatasourceFetchInstance(datasource)
|
const instance = getDatasourceFetchInstance(datasource)
|
||||||
return await instance?.getDefinition(datasource)
|
return await instance?.getDefinition()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the schema of relationship fields for a SQL table schema
|
* Fetches the schema of relationship fields for a SQL table schema
|
||||||
* @param schema the schema to enrich
|
* @param schema the schema to enrich
|
||||||
*/
|
*/
|
||||||
export const getRelationshipSchemaAdditions = async schema => {
|
export const getRelationshipSchemaAdditions = async (
|
||||||
|
schema: Record<string, any>
|
||||||
|
) => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let relationshipAdditions = {}
|
let relationshipAdditions: Record<string, any> = {}
|
||||||
for (let fieldKey of Object.keys(schema)) {
|
for (let fieldKey of Object.keys(schema)) {
|
||||||
const fieldSchema = schema[fieldKey]
|
const fieldSchema = schema[fieldKey]
|
||||||
if (fieldSchema?.type === "link") {
|
if (fieldSchema?.type === "link") {
|
||||||
|
@ -110,7 +113,10 @@ export const getRelationshipSchemaAdditions = async schema => {
|
||||||
type: "table",
|
type: "table",
|
||||||
tableId: fieldSchema?.tableId,
|
tableId: fieldSchema?.tableId,
|
||||||
})
|
})
|
||||||
Object.keys(linkSchema || {}).forEach(linkKey => {
|
if (!linkSchema) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Object.keys(linkSchema).forEach(linkKey => {
|
||||||
relationshipAdditions[`${fieldKey}.${linkKey}`] = {
|
relationshipAdditions[`${fieldKey}.${linkKey}`] = {
|
||||||
type: linkSchema[linkKey].type,
|
type: linkSchema[linkKey].type,
|
||||||
externalType: linkSchema[linkKey].externalType,
|
externalType: linkSchema[linkKey].externalType,
|
|
@ -14,6 +14,7 @@
|
||||||
"../*",
|
"../*",
|
||||||
"../../node_modules/@budibase/*"
|
"../../node_modules/@budibase/*"
|
||||||
],
|
],
|
||||||
|
"@/*": ["./src/*"],
|
||||||
"*": ["./src/*"]
|
"*": ["./src/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,10 @@ export default defineConfig(({ mode }) => {
|
||||||
find: "constants",
|
find: "constants",
|
||||||
replacement: path.resolve("./src/constants"),
|
replacement: path.resolve("./src/constants"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
find: "@/constants",
|
||||||
|
replacement: path.resolve("./src/constants"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
find: "sdk",
|
find: "sdk",
|
||||||
replacement: path.resolve("./src/sdk"),
|
replacement: path.resolve("./src/sdk"),
|
||||||
|
|
|
@ -100,6 +100,7 @@ export const buildAttachmentEndpoints = (
|
||||||
body: data,
|
body: data,
|
||||||
json: false,
|
json: false,
|
||||||
external: true,
|
external: true,
|
||||||
|
parseResponse: response => response as any,
|
||||||
})
|
})
|
||||||
return { publicUrl }
|
return { publicUrl }
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,6 +46,8 @@ import { buildLogsEndpoints } from "./logs"
|
||||||
import { buildMigrationEndpoints } from "./migrations"
|
import { buildMigrationEndpoints } from "./migrations"
|
||||||
import { buildRowActionEndpoints } from "./rowActions"
|
import { buildRowActionEndpoints } from "./rowActions"
|
||||||
|
|
||||||
|
export type { APIClient } from "./types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Random identifier to uniquely identify a session in a tab. This is
|
* Random identifier to uniquely identify a session in a tab. This is
|
||||||
* used to determine the originator of calls to the API, which is in
|
* used to determine the originator of calls to the API, which is in
|
||||||
|
@ -68,13 +70,13 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
||||||
): Promise<APIError> => {
|
): Promise<APIError> => {
|
||||||
// Try to read a message from the error
|
// Try to read a message from the error
|
||||||
let message = response.statusText
|
let message = response.statusText
|
||||||
let json: any = null
|
let json = null
|
||||||
try {
|
try {
|
||||||
json = await response.json()
|
json = await response.json()
|
||||||
if (json?.message) {
|
if (json?.message) {
|
||||||
message = json.message
|
message = json.message
|
||||||
} else if (json?.error) {
|
} else if (json?.error) {
|
||||||
message = json.error
|
message = JSON.stringify(json.error)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -93,7 +95,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
||||||
// Generates an error object from a string
|
// Generates an error object from a string
|
||||||
const makeError = (
|
const makeError = (
|
||||||
message: string,
|
message: string,
|
||||||
url?: string,
|
url: string,
|
||||||
method?: HTTPMethod
|
method?: HTTPMethod
|
||||||
): APIError => {
|
): APIError => {
|
||||||
return {
|
return {
|
||||||
|
@ -226,7 +228,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
||||||
return await handler(callConfig)
|
return await handler(callConfig)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (config?.onError) {
|
if (config?.onError) {
|
||||||
config.onError(error)
|
config.onError(error as APIError)
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
@ -239,13 +241,9 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
||||||
patch: requestApiCall(HTTPMethod.PATCH),
|
patch: requestApiCall(HTTPMethod.PATCH),
|
||||||
delete: requestApiCall(HTTPMethod.DELETE),
|
delete: requestApiCall(HTTPMethod.DELETE),
|
||||||
put: requestApiCall(HTTPMethod.PUT),
|
put: requestApiCall(HTTPMethod.PUT),
|
||||||
error: (message: string) => {
|
|
||||||
throw makeError(message)
|
|
||||||
},
|
|
||||||
invalidateCache: () => {
|
invalidateCache: () => {
|
||||||
cache = {}
|
cache = {}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Generic utility to extract the current app ID. Assumes that any client
|
// Generic utility to extract the current app ID. Assumes that any client
|
||||||
// that exists in an app context will be attaching our app ID header.
|
// that exists in an app context will be attaching our app ID header.
|
||||||
getAppID: (): string => {
|
getAppID: (): string => {
|
||||||
|
|
|
@ -13,7 +13,7 @@ export interface SelfEndpoints {
|
||||||
generateAPIKey: () => Promise<string | undefined>
|
generateAPIKey: () => Promise<string | undefined>
|
||||||
fetchDeveloperInfo: () => Promise<FetchAPIKeyResponse>
|
fetchDeveloperInfo: () => Promise<FetchAPIKeyResponse>
|
||||||
fetchBuilderSelf: () => Promise<GetGlobalSelfResponse>
|
fetchBuilderSelf: () => Promise<GetGlobalSelfResponse>
|
||||||
fetchSelf: () => Promise<AppSelfResponse>
|
fetchSelf: () => Promise<AppSelfResponse | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildSelfEndpoints = (API: BaseAPIClient): SelfEndpoints => ({
|
export const buildSelfEndpoints = (API: BaseAPIClient): SelfEndpoints => ({
|
||||||
|
|
|
@ -46,7 +46,7 @@ export type Headers = Record<string, string>
|
||||||
export type APIClientConfig = {
|
export type APIClientConfig = {
|
||||||
enableCaching?: boolean
|
enableCaching?: boolean
|
||||||
attachHeaders?: (headers: Headers) => void
|
attachHeaders?: (headers: Headers) => void
|
||||||
onError?: (error: any) => void
|
onError?: (error: APIError) => void
|
||||||
onMigrationDetected?: (migration: string) => void
|
onMigrationDetected?: (migration: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,14 +86,13 @@ export type BaseAPIClient = {
|
||||||
patch: <RequestT = null, ResponseT = void>(
|
patch: <RequestT = null, ResponseT = void>(
|
||||||
params: APICallParams<RequestT, ResponseT>
|
params: APICallParams<RequestT, ResponseT>
|
||||||
) => Promise<ResponseT>
|
) => Promise<ResponseT>
|
||||||
error: (message: string) => void
|
|
||||||
invalidateCache: () => void
|
invalidateCache: () => void
|
||||||
getAppID: () => string
|
getAppID: () => string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type APIError = {
|
export type APIError = {
|
||||||
message?: string
|
message?: string
|
||||||
url?: string
|
url: string
|
||||||
method?: HTTPMethod
|
method?: HTTPMethod
|
||||||
json: any
|
json: any
|
||||||
status: number
|
status: number
|
||||||
|
|
|
@ -21,11 +21,12 @@ import {
|
||||||
SaveUserResponse,
|
SaveUserResponse,
|
||||||
SearchUsersRequest,
|
SearchUsersRequest,
|
||||||
SearchUsersResponse,
|
SearchUsersResponse,
|
||||||
|
UnsavedUser,
|
||||||
UpdateInviteRequest,
|
UpdateInviteRequest,
|
||||||
UpdateInviteResponse,
|
UpdateInviteResponse,
|
||||||
UpdateSelfMetadataRequest,
|
UpdateSelfMetadataRequest,
|
||||||
UpdateSelfMetadataResponse,
|
UpdateSelfMetadataResponse,
|
||||||
User,
|
UserIdentifier,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { BaseAPIClient } from "./types"
|
import { BaseAPIClient } from "./types"
|
||||||
|
|
||||||
|
@ -38,14 +39,9 @@ export interface UserEndpoints {
|
||||||
createAdminUser: (
|
createAdminUser: (
|
||||||
user: CreateAdminUserRequest
|
user: CreateAdminUserRequest
|
||||||
) => Promise<CreateAdminUserResponse>
|
) => Promise<CreateAdminUserResponse>
|
||||||
saveUser: (user: User) => Promise<SaveUserResponse>
|
saveUser: (user: UnsavedUser) => Promise<SaveUserResponse>
|
||||||
deleteUser: (userId: string) => Promise<DeleteUserResponse>
|
deleteUser: (userId: string) => Promise<DeleteUserResponse>
|
||||||
deleteUsers: (
|
deleteUsers: (users: UserIdentifier[]) => Promise<BulkUserDeleted | undefined>
|
||||||
users: Array<{
|
|
||||||
userId: string
|
|
||||||
email: string
|
|
||||||
}>
|
|
||||||
) => Promise<BulkUserDeleted | undefined>
|
|
||||||
onboardUsers: (data: InviteUsersRequest) => Promise<InviteUsersResponse>
|
onboardUsers: (data: InviteUsersRequest) => Promise<InviteUsersResponse>
|
||||||
getUserInvite: (code: string) => Promise<CheckInviteResponse>
|
getUserInvite: (code: string) => Promise<CheckInviteResponse>
|
||||||
getUserInvites: () => Promise<GetUserInvitesResponse>
|
getUserInvites: () => Promise<GetUserInvitesResponse>
|
||||||
|
@ -60,7 +56,7 @@ export interface UserEndpoints {
|
||||||
getAccountHolder: () => Promise<LookupAccountHolderResponse>
|
getAccountHolder: () => Promise<LookupAccountHolderResponse>
|
||||||
searchUsers: (data: SearchUsersRequest) => Promise<SearchUsersResponse>
|
searchUsers: (data: SearchUsersRequest) => Promise<SearchUsersResponse>
|
||||||
createUsers: (
|
createUsers: (
|
||||||
users: User[],
|
users: UnsavedUser[],
|
||||||
groups: any[]
|
groups: any[]
|
||||||
) => Promise<BulkUserCreated | undefined>
|
) => Promise<BulkUserCreated | undefined>
|
||||||
updateUserInvite: (
|
updateUserInvite: (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { FieldType } from "@budibase/types"
|
import { FieldType, UIColumn } from "@budibase/types"
|
||||||
|
|
||||||
import OptionsCell from "../cells/OptionsCell.svelte"
|
import OptionsCell from "../cells/OptionsCell.svelte"
|
||||||
import DateCell from "../cells/DateCell.svelte"
|
import DateCell from "../cells/DateCell.svelte"
|
||||||
|
@ -40,13 +40,23 @@ const TypeComponentMap = {
|
||||||
// Custom types for UI only
|
// Custom types for UI only
|
||||||
role: RoleCell,
|
role: RoleCell,
|
||||||
}
|
}
|
||||||
export const getCellRenderer = column => {
|
|
||||||
|
function getCellRendererByType(type: FieldType | "role" | undefined) {
|
||||||
|
if (!type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeComponentMap[type as keyof typeof TypeComponentMap]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCellRenderer = (column: UIColumn) => {
|
||||||
if (column.calculationType) {
|
if (column.calculationType) {
|
||||||
return NumberCell
|
return NumberCell
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
TypeComponentMap[column?.schema?.cellRenderType] ||
|
getCellRendererByType(column.schema?.cellRenderType) ||
|
||||||
TypeComponentMap[column?.schema?.type] ||
|
getCellRendererByType(column.schema?.type) ||
|
||||||
TextCell
|
TextCell
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
// TODO: remove when all stores are typed
|
|
||||||
|
|
||||||
import { GeneratedIDPrefix, CellIDSeparator } from "./constants"
|
|
||||||
import { Helpers } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export const parseCellID = cellId => {
|
|
||||||
if (!cellId) {
|
|
||||||
return { rowId: undefined, field: undefined }
|
|
||||||
}
|
|
||||||
const parts = cellId.split(CellIDSeparator)
|
|
||||||
const field = parts.pop()
|
|
||||||
return { rowId: parts.join(CellIDSeparator), field }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCellID = (rowId, fieldName) => {
|
|
||||||
return `${rowId}${CellIDSeparator}${fieldName}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseEventLocation = e => {
|
|
||||||
return {
|
|
||||||
x: e.clientX ?? e.touches?.[0]?.clientX,
|
|
||||||
y: e.clientY ?? e.touches?.[0]?.clientY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const generateRowID = () => {
|
|
||||||
return `${GeneratedIDPrefix}${Helpers.uuid()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isGeneratedRowID = id => {
|
|
||||||
return id?.startsWith(GeneratedIDPrefix)
|
|
||||||
}
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { createWebsocket } from "../../../utils"
|
import { createWebsocket } from "../../../utils"
|
||||||
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
||||||
|
import { Store } from "../stores"
|
||||||
|
import { UIDatasource, UIUser } from "@budibase/types"
|
||||||
|
|
||||||
export const createGridWebsocket = context => {
|
export const createGridWebsocket = (context: Store) => {
|
||||||
const { rows, datasource, users, focusedCellId, definition, API } = context
|
const { rows, datasource, users, focusedCellId, definition, API } = context
|
||||||
const socket = createWebsocket("/socket/grid")
|
const socket = createWebsocket("/socket/grid")
|
||||||
|
|
||||||
const connectToDatasource = datasource => {
|
const connectToDatasource = (datasource: UIDatasource) => {
|
||||||
if (!socket.connected) {
|
if (!socket.connected) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -18,7 +20,7 @@ export const createGridWebsocket = context => {
|
||||||
datasource,
|
datasource,
|
||||||
appId,
|
appId,
|
||||||
},
|
},
|
||||||
({ users: gridUsers }) => {
|
({ users: gridUsers }: { users: UIUser[] }) => {
|
||||||
users.set(gridUsers)
|
users.set(gridUsers)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -65,7 +67,7 @@ export const createGridWebsocket = context => {
|
||||||
GridSocketEvent.DatasourceChange,
|
GridSocketEvent.DatasourceChange,
|
||||||
({ datasource: newDatasource }) => {
|
({ datasource: newDatasource }) => {
|
||||||
// Listen builder renames, as these aren't handled otherwise
|
// Listen builder renames, as these aren't handled otherwise
|
||||||
if (newDatasource?.name !== get(definition).name) {
|
if (newDatasource?.name !== get(definition)?.name) {
|
||||||
definition.set(newDatasource)
|
definition.set(newDatasource)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import DataFetch from "./DataFetch"
|
import DataFetch from "./DataFetch"
|
||||||
|
|
||||||
interface CustomDatasource {
|
interface CustomDatasource {
|
||||||
|
type: "custom"
|
||||||
data: any
|
data: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
UISearchFilter,
|
UISearchFilter,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { APIClient } from "../api/types"
|
import { APIClient } from "../api/types"
|
||||||
|
import { DataFetchType } from "."
|
||||||
|
|
||||||
const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
|
const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ export interface DataFetchParams<
|
||||||
* For other types of datasource, this class is overridden and extended.
|
* For other types of datasource, this class is overridden and extended.
|
||||||
*/
|
*/
|
||||||
export default abstract class DataFetch<
|
export default abstract class DataFetch<
|
||||||
TDatasource extends {},
|
TDatasource extends { type: DataFetchType },
|
||||||
TDefinition extends {
|
TDefinition extends {
|
||||||
schema?: Record<string, any> | null
|
schema?: Record<string, any> | null
|
||||||
primaryDisplay?: string
|
primaryDisplay?: string
|
||||||
|
@ -179,9 +180,6 @@ export default abstract class DataFetch<
|
||||||
this.store.update($store => ({ ...$store, loaded: true }))
|
this.store.update($store => ({ ...$store, loaded: true }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initially fetch data but don't bother waiting for the result
|
|
||||||
this.getInitialData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -371,7 +369,7 @@ export default abstract class DataFetch<
|
||||||
* @param schema the datasource schema
|
* @param schema the datasource schema
|
||||||
* @return {object} the enriched datasource schema
|
* @return {object} the enriched datasource schema
|
||||||
*/
|
*/
|
||||||
private enrichSchema(schema: TableSchema): TableSchema {
|
enrichSchema(schema: TableSchema): TableSchema {
|
||||||
// Check for any JSON fields so we can add any top level properties
|
// Check for any JSON fields so we can add any top level properties
|
||||||
let jsonAdditions: Record<string, { type: string; nestedJSON: true }> = {}
|
let jsonAdditions: Record<string, { type: string; nestedJSON: true }> = {}
|
||||||
for (const fieldKey of Object.keys(schema)) {
|
for (const fieldKey of Object.keys(schema)) {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { Row } from "@budibase/types"
|
import { Row } from "@budibase/types"
|
||||||
import DataFetch from "./DataFetch"
|
import DataFetch from "./DataFetch"
|
||||||
|
|
||||||
export interface FieldDatasource {
|
type Types = "field" | "queryarray" | "jsonarray"
|
||||||
|
|
||||||
|
export interface FieldDatasource<TType extends Types> {
|
||||||
|
type: TType
|
||||||
tableId: string
|
tableId: string
|
||||||
fieldType: "attachment" | "array"
|
fieldType: "attachment" | "array"
|
||||||
value: string[] | Row[]
|
value: string[] | Row[]
|
||||||
|
@ -15,8 +18,8 @@ function isArrayOfStrings(value: string[] | Row[]): value is string[] {
|
||||||
return Array.isArray(value) && !!value[0] && typeof value[0] !== "object"
|
return Array.isArray(value) && !!value[0] && typeof value[0] !== "object"
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FieldFetch extends DataFetch<
|
export default class FieldFetch<TType extends Types> extends DataFetch<
|
||||||
FieldDatasource,
|
FieldDatasource<TType>,
|
||||||
FieldDefinition
|
FieldDefinition
|
||||||
> {
|
> {
|
||||||
async getDefinition(): Promise<FieldDefinition | null> {
|
async getDefinition(): Promise<FieldDefinition | null> {
|
||||||
|
|
|
@ -8,6 +8,7 @@ interface GroupUserQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupUserDatasource {
|
interface GroupUserDatasource {
|
||||||
|
type: "groupUser"
|
||||||
tableId: TableNames.USERS
|
tableId: TableNames.USERS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ export default class GroupUserFetch extends DataFetch<
|
||||||
super({
|
super({
|
||||||
...opts,
|
...opts,
|
||||||
datasource: {
|
datasource: {
|
||||||
|
type: "groupUser",
|
||||||
tableId: TableNames.USERS,
|
tableId: TableNames.USERS,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import FieldFetch from "./FieldFetch"
|
import FieldFetch from "./FieldFetch"
|
||||||
import { getJSONArrayDatasourceSchema } from "../utils/json"
|
import { getJSONArrayDatasourceSchema } from "../utils/json"
|
||||||
|
|
||||||
export default class JSONArrayFetch extends FieldFetch {
|
export default class JSONArrayFetch extends FieldFetch<"jsonarray"> {
|
||||||
async getDefinition() {
|
async getDefinition() {
|
||||||
const { datasource } = this.options
|
const { datasource } = this.options
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Row, TableSchema } from "@budibase/types"
|
||||||
import DataFetch from "./DataFetch"
|
import DataFetch from "./DataFetch"
|
||||||
|
|
||||||
interface NestedProviderDatasource {
|
interface NestedProviderDatasource {
|
||||||
|
type: "provider"
|
||||||
value?: {
|
value?: {
|
||||||
schema: TableSchema
|
schema: TableSchema
|
||||||
primaryDisplay: string
|
primaryDisplay: string
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
generateQueryArraySchemas,
|
generateQueryArraySchemas,
|
||||||
} from "../utils/json"
|
} from "../utils/json"
|
||||||
|
|
||||||
export default class QueryArrayFetch extends FieldFetch {
|
export default class QueryArrayFetch extends FieldFetch<"queryarray"> {
|
||||||
async getDefinition() {
|
async getDefinition() {
|
||||||
const { datasource } = this.options
|
const { datasource } = this.options
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { ExecuteQueryRequest, Query } from "@budibase/types"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
interface QueryDatasource {
|
interface QueryDatasource {
|
||||||
|
type: "query"
|
||||||
_id: string
|
_id: string
|
||||||
fields: Record<string, any> & {
|
fields: Record<string, any> & {
|
||||||
pagination?: {
|
pagination?: {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Table } from "@budibase/types"
|
||||||
import DataFetch from "./DataFetch"
|
import DataFetch from "./DataFetch"
|
||||||
|
|
||||||
interface RelationshipDatasource {
|
interface RelationshipDatasource {
|
||||||
|
type: "link"
|
||||||
tableId: string
|
tableId: string
|
||||||
rowId: string
|
rowId: string
|
||||||
rowTableId: string
|
rowTableId: string
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import DataFetch from "./DataFetch"
|
import DataFetch from "./DataFetch"
|
||||||
import { SortOrder, Table, UITable } from "@budibase/types"
|
import { SortOrder, Table } from "@budibase/types"
|
||||||
|
|
||||||
export default class TableFetch extends DataFetch<UITable, Table> {
|
interface TableDatasource {
|
||||||
|
type: "table"
|
||||||
|
tableId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TableFetch extends DataFetch<TableDatasource, Table> {
|
||||||
async determineFeatureFlags() {
|
async determineFeatureFlags() {
|
||||||
return {
|
return {
|
||||||
supportsSearch: true,
|
supportsSearch: true,
|
||||||
|
|
|
@ -2,11 +2,7 @@ import { get } from "svelte/store"
|
||||||
import DataFetch, { DataFetchParams } from "./DataFetch"
|
import DataFetch, { DataFetchParams } from "./DataFetch"
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import {
|
import { SearchFilters, SearchUsersRequest } from "@budibase/types"
|
||||||
BasicOperator,
|
|
||||||
SearchFilters,
|
|
||||||
SearchUsersRequest,
|
|
||||||
} from "@budibase/types"
|
|
||||||
|
|
||||||
interface UserFetchQuery {
|
interface UserFetchQuery {
|
||||||
appId: string
|
appId: string
|
||||||
|
@ -14,18 +10,22 @@ interface UserFetchQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDatasource {
|
interface UserDatasource {
|
||||||
tableId: string
|
type: "user"
|
||||||
|
tableId: TableNames.USERS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserDefinition {}
|
||||||
|
|
||||||
export default class UserFetch extends DataFetch<
|
export default class UserFetch extends DataFetch<
|
||||||
UserDatasource,
|
UserDatasource,
|
||||||
{},
|
UserDefinition,
|
||||||
UserFetchQuery
|
UserFetchQuery
|
||||||
> {
|
> {
|
||||||
constructor(opts: DataFetchParams<UserDatasource, UserFetchQuery>) {
|
constructor(opts: DataFetchParams<UserDatasource, UserFetchQuery>) {
|
||||||
super({
|
super({
|
||||||
...opts,
|
...opts,
|
||||||
datasource: {
|
datasource: {
|
||||||
|
type: "user",
|
||||||
tableId: TableNames.USERS,
|
tableId: TableNames.USERS,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -52,7 +52,7 @@ export default class UserFetch extends DataFetch<
|
||||||
|
|
||||||
const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest)
|
const finalQuery: SearchFilters = utils.isSupportedUserSearch(rest)
|
||||||
? rest
|
? rest
|
||||||
: { [BasicOperator.EMPTY]: { email: null } }
|
: {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const opts: SearchUsersRequest = {
|
const opts: SearchUsersRequest = {
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import { Table, View } from "@budibase/types"
|
import { Table } from "@budibase/types"
|
||||||
import DataFetch from "./DataFetch"
|
import DataFetch from "./DataFetch"
|
||||||
|
|
||||||
type ViewV1 = View & { name: string }
|
type ViewV1Datasource = {
|
||||||
|
type: "view"
|
||||||
|
name: string
|
||||||
|
tableId: string
|
||||||
|
calculation: string
|
||||||
|
field: string
|
||||||
|
groupBy: string
|
||||||
|
}
|
||||||
|
|
||||||
export default class ViewFetch extends DataFetch<ViewV1, Table> {
|
export default class ViewFetch extends DataFetch<ViewV1Datasource, Table> {
|
||||||
async getDefinition() {
|
async getDefinition() {
|
||||||
const { datasource } = this.options
|
const { datasource } = this.options
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import { SortOrder, UIView, ViewV2, ViewV2Type } from "@budibase/types"
|
import { SortOrder, ViewV2Enriched, ViewV2Type } from "@budibase/types"
|
||||||
import DataFetch from "./DataFetch"
|
import DataFetch from "./DataFetch"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
|
||||||
export default class ViewV2Fetch extends DataFetch<UIView, ViewV2> {
|
interface ViewDatasource {
|
||||||
|
type: "viewV2"
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ViewV2Fetch extends DataFetch<
|
||||||
|
ViewDatasource,
|
||||||
|
ViewV2Enriched
|
||||||
|
> {
|
||||||
async determineFeatureFlags() {
|
async determineFeatureFlags() {
|
||||||
return {
|
return {
|
||||||
supportsSearch: true,
|
supportsSearch: true,
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import TableFetch from "./TableFetch.js"
|
import TableFetch from "./TableFetch"
|
||||||
import ViewFetch from "./ViewFetch.js"
|
import ViewFetch from "./ViewFetch"
|
||||||
import ViewV2Fetch from "./ViewV2Fetch.js"
|
import ViewV2Fetch from "./ViewV2Fetch"
|
||||||
import QueryFetch from "./QueryFetch"
|
import QueryFetch from "./QueryFetch"
|
||||||
import RelationshipFetch from "./RelationshipFetch"
|
import RelationshipFetch from "./RelationshipFetch"
|
||||||
import NestedProviderFetch from "./NestedProviderFetch"
|
import NestedProviderFetch from "./NestedProviderFetch"
|
||||||
import FieldFetch from "./FieldFetch"
|
import FieldFetch from "./FieldFetch"
|
||||||
import JSONArrayFetch from "./JSONArrayFetch"
|
import JSONArrayFetch from "./JSONArrayFetch"
|
||||||
import UserFetch from "./UserFetch.js"
|
import UserFetch from "./UserFetch"
|
||||||
import GroupUserFetch from "./GroupUserFetch"
|
import GroupUserFetch from "./GroupUserFetch"
|
||||||
import CustomFetch from "./CustomFetch"
|
import CustomFetch from "./CustomFetch"
|
||||||
import QueryArrayFetch from "./QueryArrayFetch.js"
|
import QueryArrayFetch from "./QueryArrayFetch"
|
||||||
import { APIClient } from "../api/types.js"
|
import { APIClient } from "../api/types"
|
||||||
|
|
||||||
const DataFetchMap = {
|
export type DataFetchType = keyof typeof DataFetchMap
|
||||||
|
|
||||||
|
export const DataFetchMap = {
|
||||||
table: TableFetch,
|
table: TableFetch,
|
||||||
view: ViewFetch,
|
view: ViewFetch,
|
||||||
viewV2: ViewV2Fetch,
|
viewV2: ViewV2Fetch,
|
||||||
|
@ -24,43 +26,45 @@ const DataFetchMap = {
|
||||||
|
|
||||||
// Client specific datasource types
|
// Client specific datasource types
|
||||||
provider: NestedProviderFetch,
|
provider: NestedProviderFetch,
|
||||||
field: FieldFetch,
|
field: FieldFetch<"field">,
|
||||||
jsonarray: JSONArrayFetch,
|
jsonarray: JSONArrayFetch,
|
||||||
queryarray: QueryArrayFetch,
|
queryarray: QueryArrayFetch,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructs a new fetch model for a certain datasource
|
// Constructs a new fetch model for a certain datasource
|
||||||
export const fetchData = ({ API, datasource, options }: any) => {
|
export const fetchData = ({ API, datasource, options }: any) => {
|
||||||
const Fetch =
|
const Fetch = DataFetchMap[datasource?.type as DataFetchType] || TableFetch
|
||||||
DataFetchMap[datasource?.type as keyof typeof DataFetchMap] || TableFetch
|
const fetch = new Fetch({ API, datasource, ...options })
|
||||||
return new Fetch({ API, datasource, ...options })
|
|
||||||
|
// Initially fetch data but don't bother waiting for the result
|
||||||
|
fetch.getInitialData()
|
||||||
|
|
||||||
|
return fetch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an empty fetch instance with no datasource configured, so no data
|
// Creates an empty fetch instance with no datasource configured, so no data
|
||||||
// will initially be loaded
|
// will initially be loaded
|
||||||
const createEmptyFetchInstance = <
|
const createEmptyFetchInstance = <TDatasource extends { type: DataFetchType }>({
|
||||||
TDatasource extends {
|
|
||||||
type: keyof typeof DataFetchMap
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
API,
|
API,
|
||||||
datasource,
|
datasource,
|
||||||
}: {
|
}: {
|
||||||
API: APIClient
|
API: APIClient
|
||||||
datasource: TDatasource
|
datasource: TDatasource
|
||||||
}) => {
|
}) => {
|
||||||
const handler = DataFetchMap[datasource?.type as keyof typeof DataFetchMap]
|
const handler = DataFetchMap[datasource?.type as DataFetchType]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return new handler({ API, datasource: null as any, query: null as any })
|
return new handler({
|
||||||
|
API,
|
||||||
|
datasource: null as never,
|
||||||
|
query: null as any,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the definition of any type of datasource
|
// Fetches the definition of any type of datasource
|
||||||
export const getDatasourceDefinition = async <
|
export const getDatasourceDefinition = async <
|
||||||
TDatasource extends {
|
TDatasource extends { type: DataFetchType }
|
||||||
type: keyof typeof DataFetchMap
|
|
||||||
}
|
|
||||||
>({
|
>({
|
||||||
API,
|
API,
|
||||||
datasource,
|
datasource,
|
||||||
|
@ -74,9 +78,7 @@ export const getDatasourceDefinition = async <
|
||||||
|
|
||||||
// Fetches the schema of any type of datasource
|
// Fetches the schema of any type of datasource
|
||||||
export const getDatasourceSchema = <
|
export const getDatasourceSchema = <
|
||||||
TDatasource extends {
|
TDatasource extends { type: DataFetchType }
|
||||||
type: keyof typeof DataFetchMap
|
|
||||||
}
|
|
||||||
>({
|
>({
|
||||||
API,
|
API,
|
||||||
datasource,
|
datasource,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
export { createAPIClient } from "./api"
|
export { createAPIClient } from "./api"
|
||||||
export { fetchData } from "./fetch"
|
export type { APIClient } from "./api"
|
||||||
|
export { fetchData, DataFetchMap } from "./fetch"
|
||||||
|
export type { DataFetchType } from "./fetch"
|
||||||
export * as Constants from "./constants"
|
export * as Constants from "./constants"
|
||||||
export * from "./stores"
|
export * from "./stores"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
|
|
|
@ -209,6 +209,9 @@ export const buildFormBlockButtonConfig = props => {
|
||||||
{
|
{
|
||||||
"##eventHandlerType": "Close Side Panel",
|
"##eventHandlerType": "Close Side Panel",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Close Modal",
|
||||||
|
},
|
||||||
|
|
||||||
...(actionUrl
|
...(actionUrl
|
||||||
? [
|
? [
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 32d84f109d4edc526145472a7446327312151442
|
Subproject commit a4f63b22675e16dcdcaa4d9e83b298eee6466a07
|
|
@ -45,6 +45,9 @@ export async function handleRequest<T extends Operation>(
|
||||||
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
const source = await utils.getSource(ctx)
|
const source = await utils.getSource(ctx)
|
||||||
|
|
||||||
|
const { viewId, tableId } = utils.getSourceId(ctx)
|
||||||
|
const sourceId = viewId || tableId
|
||||||
|
|
||||||
if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {
|
if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {
|
||||||
ctx.throw(400, "Cannot update rows through a calculation view")
|
ctx.throw(400, "Cannot update rows through a calculation view")
|
||||||
}
|
}
|
||||||
|
@ -86,7 +89,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
// The id might have been changed, so the refetching would fail. Recalculating the id just in case
|
// The id might have been changed, so the refetching would fail. Recalculating the id just in case
|
||||||
const updatedId =
|
const updatedId =
|
||||||
generateIdForRow({ ...beforeRow, ...dataToUpdate }, table) || _id
|
generateIdForRow({ ...beforeRow, ...dataToUpdate }, table) || _id
|
||||||
const row = await sdk.rows.external.getRow(table._id!, updatedId, {
|
const row = await sdk.rows.external.getRow(sourceId, updatedId, {
|
||||||
relationships: true,
|
relationships: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,8 @@ import {
|
||||||
processAIColumns,
|
processAIColumns,
|
||||||
processFormulas,
|
processFormulas,
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
import { context, features } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import {
|
import { Table, Row, FormulaType, FieldType, ViewV2 } from "@budibase/types"
|
||||||
Table,
|
|
||||||
Row,
|
|
||||||
FeatureFlag,
|
|
||||||
FormulaType,
|
|
||||||
FieldType,
|
|
||||||
ViewV2,
|
|
||||||
} from "@budibase/types"
|
|
||||||
import * as linkRows from "../../../db/linkedRows"
|
import * as linkRows from "../../../db/linkedRows"
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
import { cloneDeep, merge } from "lodash/fp"
|
import { cloneDeep, merge } from "lodash/fp"
|
||||||
|
@ -162,11 +155,10 @@ export async function finaliseRow(
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
contextRows: [enrichedRow],
|
contextRows: [enrichedRow],
|
||||||
})
|
})
|
||||||
|
|
||||||
const aiEnabled =
|
const aiEnabled =
|
||||||
((await features.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
(await pro.features.isBudibaseAIEnabled()) ||
|
||||||
(await pro.features.isBudibaseAIEnabled())) ||
|
(await pro.features.isAICustomConfigsEnabled())
|
||||||
((await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
|
||||||
(await pro.features.isAICustomConfigsEnabled()))
|
|
||||||
if (aiEnabled) {
|
if (aiEnabled) {
|
||||||
row = await processAIColumns(table, row, {
|
row = await processAIColumns(table, row, {
|
||||||
contextRows: [enrichedRow],
|
contextRows: [enrichedRow],
|
||||||
|
@ -184,11 +176,6 @@ export async function finaliseRow(
|
||||||
enrichedRow = await processFormulas(table, enrichedRow, {
|
enrichedRow = await processFormulas(table, enrichedRow, {
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
})
|
})
|
||||||
if (aiEnabled) {
|
|
||||||
enrichedRow = await processAIColumns(table, enrichedRow, {
|
|
||||||
contextRows: [enrichedRow],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// this updates the related formulas in other rows based on the relations to this row
|
// this updates the related formulas in other rows based on the relations to this row
|
||||||
if (updateFormula) {
|
if (updateFormula) {
|
||||||
|
|
|
@ -14,7 +14,8 @@ import {
|
||||||
import { breakExternalTableId } from "../../../../integrations/utils"
|
import { breakExternalTableId } from "../../../../integrations/utils"
|
||||||
import { generateJunctionTableID } from "../../../../db/utils"
|
import { generateJunctionTableID } from "../../../../db/utils"
|
||||||
import sdk from "../../../../sdk"
|
import sdk from "../../../../sdk"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers, PROTECTED_INTERNAL_COLUMNS } from "@budibase/shared-core"
|
||||||
|
import { sql } from "@budibase/backend-core"
|
||||||
|
|
||||||
type TableMap = Record<string, Table>
|
type TableMap = Record<string, Table>
|
||||||
|
|
||||||
|
@ -118,45 +119,131 @@ export async function buildSqlFieldList(
|
||||||
opts?: { relationships: boolean }
|
opts?: { relationships: boolean }
|
||||||
) {
|
) {
|
||||||
const { relationships } = opts || {}
|
const { relationships } = opts || {}
|
||||||
|
|
||||||
|
const nonMappedColumns = [FieldType.LINK, FieldType.FORMULA, FieldType.AI]
|
||||||
|
|
||||||
function extractRealFields(table: Table, existing: string[] = []) {
|
function extractRealFields(table: Table, existing: string[] = []) {
|
||||||
return Object.entries(table.schema)
|
return Object.entries(table.schema)
|
||||||
.filter(
|
.filter(
|
||||||
([columnName, column]) =>
|
([columnName, column]) =>
|
||||||
column.type !== FieldType.LINK &&
|
!nonMappedColumns.includes(column.type) &&
|
||||||
column.type !== FieldType.FORMULA &&
|
!existing.find((field: string) => field === columnName)
|
||||||
column.type !== FieldType.AI &&
|
|
||||||
!existing.find(
|
|
||||||
(field: string) => field === `${table.name}.${columnName}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.map(([columnName]) => `${table.name}.${columnName}`)
|
.map(([columnName]) => columnName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequiredFields(table: Table, existing: string[] = []) {
|
||||||
|
const requiredFields: string[] = []
|
||||||
|
if (table.primary) {
|
||||||
|
requiredFields.push(...table.primary)
|
||||||
|
}
|
||||||
|
if (table.primaryDisplay) {
|
||||||
|
requiredFields.push(table.primaryDisplay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sql.utils.isExternalTable(table)) {
|
||||||
|
requiredFields.push(...PROTECTED_INTERNAL_COLUMNS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return requiredFields.filter(
|
||||||
|
column =>
|
||||||
|
!existing.find((field: string) => field === column) &&
|
||||||
|
table.schema[column] &&
|
||||||
|
!nonMappedColumns.includes(table.schema[column].type)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let fields: string[] = []
|
let fields: string[] = []
|
||||||
if (sdk.views.isView(source)) {
|
|
||||||
fields = Object.keys(helpers.views.basicFields(source))
|
const isView = sdk.views.isView(source)
|
||||||
} else {
|
|
||||||
fields = extractRealFields(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
let table: Table
|
let table: Table
|
||||||
if (sdk.views.isView(source)) {
|
if (isView) {
|
||||||
table = await sdk.views.getTable(source.id)
|
table = await sdk.views.getTable(source.id)
|
||||||
|
|
||||||
|
fields = Object.keys(helpers.views.basicFields(source)).filter(
|
||||||
|
f => table.schema[f].type !== FieldType.LINK
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
table = source
|
table = source
|
||||||
|
fields = extractRealFields(source).filter(
|
||||||
|
f => table.schema[f].visible !== false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let field of Object.values(table.schema)) {
|
const containsFormula = (isView ? fields : Object.keys(table.schema)).some(
|
||||||
|
f => table.schema[f]?.type === FieldType.FORMULA
|
||||||
|
)
|
||||||
|
// If are requesting for a formula field, we need to retrieve all fields
|
||||||
|
if (containsFormula) {
|
||||||
|
fields = extractRealFields(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isView || !helpers.views.isCalculationView(source)) {
|
||||||
|
fields.push(
|
||||||
|
...getRequiredFields(
|
||||||
|
{
|
||||||
|
...table,
|
||||||
|
primaryDisplay: source.primaryDisplay || table.primaryDisplay,
|
||||||
|
},
|
||||||
|
fields
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = fields.map(c => `${table.name}.${c}`)
|
||||||
|
|
||||||
|
for (const field of Object.values(table.schema)) {
|
||||||
if (field.type !== FieldType.LINK || !relationships || !field.tableId) {
|
if (field.type !== FieldType.LINK || !relationships || !field.tableId) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const { tableName } = breakExternalTableId(field.tableId)
|
|
||||||
if (tables[tableName]) {
|
if (
|
||||||
fields = fields.concat(extractRealFields(tables[tableName], fields))
|
isView &&
|
||||||
|
(!source.schema?.[field.name] ||
|
||||||
|
!helpers.views.isVisible(source.schema[field.name])) &&
|
||||||
|
!containsFormula
|
||||||
|
) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { tableName } = breakExternalTableId(field.tableId)
|
||||||
|
const relatedTable = tables[tableName]
|
||||||
|
if (!relatedTable) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewFields = new Set<string>()
|
||||||
|
if (containsFormula) {
|
||||||
|
extractRealFields(relatedTable).forEach(f => viewFields.add(f))
|
||||||
|
} else {
|
||||||
|
relatedTable.primary?.forEach(f => viewFields.add(f))
|
||||||
|
if (relatedTable.primaryDisplay) {
|
||||||
|
viewFields.add(relatedTable.primaryDisplay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isView) {
|
||||||
|
Object.entries(source.schema?.[field.name]?.columns || {})
|
||||||
|
.filter(
|
||||||
|
([columnName, columnConfig]) =>
|
||||||
|
relatedTable.schema[columnName] &&
|
||||||
|
helpers.views.isVisible(columnConfig) &&
|
||||||
|
![FieldType.LINK, FieldType.FORMULA].includes(
|
||||||
|
relatedTable.schema[columnName].type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.forEach(([field]) => viewFields.add(field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldsToAdd = Array.from(viewFields)
|
||||||
|
.filter(f => !nonMappedColumns.includes(relatedTable.schema[f].type))
|
||||||
|
.map(f => `${relatedTable.name}.${f}`)
|
||||||
|
.filter(f => !fields.includes(f))
|
||||||
|
fields.push(...fieldsToAdd)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields
|
return [...new Set(fields)]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isKnexEmptyReadResponse(resp: DatasourcePlusQueryResponse) {
|
export function isKnexEmptyReadResponse(resp: DatasourcePlusQueryResponse) {
|
||||||
|
|
|
@ -0,0 +1,511 @@
|
||||||
|
import {
|
||||||
|
AIOperationEnum,
|
||||||
|
CalculationType,
|
||||||
|
FieldType,
|
||||||
|
RelationshipType,
|
||||||
|
SourceName,
|
||||||
|
Table,
|
||||||
|
ViewV2,
|
||||||
|
ViewV2Type,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { buildSqlFieldList } from "../sqlUtils"
|
||||||
|
import { structures } from "../../../../routes/tests/utilities"
|
||||||
|
import { sql } from "@budibase/backend-core"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import { generateViewID } from "../../../../../db/utils"
|
||||||
|
|
||||||
|
import sdk from "../../../../../sdk"
|
||||||
|
import { cloneDeep } from "lodash"
|
||||||
|
import { utils } from "@budibase/shared-core"
|
||||||
|
|
||||||
|
jest.mock("../../../../../sdk/app/views", () => ({
|
||||||
|
...jest.requireActual("../../../../../sdk/app/views"),
|
||||||
|
getTable: jest.fn(),
|
||||||
|
}))
|
||||||
|
const getTableMock = sdk.views.getTable as jest.MockedFunction<
|
||||||
|
typeof sdk.views.getTable
|
||||||
|
>
|
||||||
|
|
||||||
|
describe("buildSqlFieldList", () => {
|
||||||
|
let allTables: Record<string, Table>
|
||||||
|
|
||||||
|
class TableConfig {
|
||||||
|
private _table: Table & { _id: string }
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this._table = {
|
||||||
|
...structures.tableForDatasource({
|
||||||
|
type: "datasource",
|
||||||
|
source: SourceName.POSTGRES,
|
||||||
|
}),
|
||||||
|
name,
|
||||||
|
_id: sql.utils.buildExternalTableId("ds_id", name),
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
name: "description",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
name: "amount",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
allTables[name] = this._table
|
||||||
|
}
|
||||||
|
|
||||||
|
withHiddenField(field: string) {
|
||||||
|
this._table.schema[field].visible = false
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withField(
|
||||||
|
name: string,
|
||||||
|
type:
|
||||||
|
| FieldType.STRING
|
||||||
|
| FieldType.NUMBER
|
||||||
|
| FieldType.FORMULA
|
||||||
|
| FieldType.AI,
|
||||||
|
options?: { visible: boolean }
|
||||||
|
) {
|
||||||
|
switch (type) {
|
||||||
|
case FieldType.NUMBER:
|
||||||
|
case FieldType.STRING:
|
||||||
|
this._table.schema[name] = {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case FieldType.FORMULA:
|
||||||
|
this._table.schema[name] = {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
formula: "any",
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case FieldType.AI:
|
||||||
|
this._table.schema[name] = {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
operation: AIOperationEnum.PROMPT,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
utils.unreachable(type)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withRelation(name: string, toTableId: string) {
|
||||||
|
this._table.schema[name] = {
|
||||||
|
name,
|
||||||
|
type: FieldType.LINK,
|
||||||
|
relationshipType: RelationshipType.ONE_TO_MANY,
|
||||||
|
fieldName: "link",
|
||||||
|
tableId: toTableId,
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withPrimary(field: string) {
|
||||||
|
this._table.primary = [field]
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withDisplay(field: string) {
|
||||||
|
this._table.primaryDisplay = field
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
return cloneDeep(this._table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewConfig {
|
||||||
|
private _table: Table
|
||||||
|
private _view: ViewV2
|
||||||
|
|
||||||
|
constructor(table: Table) {
|
||||||
|
this._table = table
|
||||||
|
this._view = {
|
||||||
|
version: 2,
|
||||||
|
id: generateViewID(table._id!),
|
||||||
|
name: generator.word(),
|
||||||
|
tableId: table._id!,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withVisible(field: string) {
|
||||||
|
this._view.schema ??= {}
|
||||||
|
this._view.schema[field] ??= {}
|
||||||
|
this._view.schema[field].visible = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withHidden(field: string) {
|
||||||
|
this._view.schema ??= {}
|
||||||
|
this._view.schema[field] ??= {}
|
||||||
|
this._view.schema[field].visible = false
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withRelationshipColumns(
|
||||||
|
field: string,
|
||||||
|
columns: Record<string, { visible: boolean }>
|
||||||
|
) {
|
||||||
|
this._view.schema ??= {}
|
||||||
|
this._view.schema[field] ??= {}
|
||||||
|
this._view.schema[field].columns = columns
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
withCalculation(
|
||||||
|
name: string,
|
||||||
|
field: string,
|
||||||
|
calculationType: CalculationType
|
||||||
|
) {
|
||||||
|
this._view.type = ViewV2Type.CALCULATION
|
||||||
|
this._view.schema ??= {}
|
||||||
|
this._view.schema[name] = {
|
||||||
|
field,
|
||||||
|
calculationType,
|
||||||
|
visible: true,
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
create() {
|
||||||
|
getTableMock.mockResolvedValueOnce(this._table)
|
||||||
|
return cloneDeep(this._view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
allTables = {}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("table", () => {
|
||||||
|
it("extracts fields from table schema", async () => {
|
||||||
|
const table = new TableConfig("table").create()
|
||||||
|
const result = await buildSqlFieldList(table, {})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"table.description",
|
||||||
|
"table.amount",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("excludes hidden fields", async () => {
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withHiddenField("description")
|
||||||
|
.create()
|
||||||
|
const result = await buildSqlFieldList(table, {})
|
||||||
|
expect(result).toEqual(["table.name", "table.amount"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("excludes non-sql fields fields", async () => {
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.withField("ai", FieldType.AI)
|
||||||
|
.withRelation("link", "otherTableId")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(table, {})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"table.description",
|
||||||
|
"table.amount",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes hidden fields if there is a formula column", async () => {
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withHiddenField("description")
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(table, {})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"table.description",
|
||||||
|
"table.amount",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes relationships fields when flagged", async () => {
|
||||||
|
const otherTable = new TableConfig("linkedTable")
|
||||||
|
.withField("id", FieldType.NUMBER)
|
||||||
|
.withPrimary("id")
|
||||||
|
.withDisplay("name")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withRelation("link", otherTable._id)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(table, allTables, {
|
||||||
|
relationships: true,
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"table.description",
|
||||||
|
"table.amount",
|
||||||
|
"linkedTable.id",
|
||||||
|
"linkedTable.name",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes all relationship fields if there is a formula column", async () => {
|
||||||
|
const otherTable = new TableConfig("linkedTable")
|
||||||
|
.withField("hidden", FieldType.STRING, { visible: false })
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withRelation("link", otherTable._id)
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(table, allTables, {
|
||||||
|
relationships: true,
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"table.description",
|
||||||
|
"table.amount",
|
||||||
|
"linkedTable.name",
|
||||||
|
"linkedTable.description",
|
||||||
|
"linkedTable.amount",
|
||||||
|
"linkedTable.hidden",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("never includes non-sql columns from relationships", async () => {
|
||||||
|
const otherTable = new TableConfig("linkedTable")
|
||||||
|
.withField("id", FieldType.NUMBER)
|
||||||
|
.withField("hidden", FieldType.STRING, { visible: false })
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.withField("ai", FieldType.AI)
|
||||||
|
.withRelation("link", "otherTableId")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withRelation("link", otherTable._id)
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(table, allTables, {
|
||||||
|
relationships: true,
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"table.description",
|
||||||
|
"table.amount",
|
||||||
|
"linkedTable.name",
|
||||||
|
"linkedTable.description",
|
||||||
|
"linkedTable.amount",
|
||||||
|
"linkedTable.id",
|
||||||
|
"linkedTable.hidden",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("view", () => {
|
||||||
|
it("extracts fields from table schema", async () => {
|
||||||
|
const view = new ViewConfig(new TableConfig("table").create())
|
||||||
|
.withVisible("amount")
|
||||||
|
.withHidden("name")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, {})
|
||||||
|
expect(result).toEqual(["table.amount"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes all fields if there is a formula column", async () => {
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.create()
|
||||||
|
const view = new ViewConfig(table)
|
||||||
|
.withHidden("name")
|
||||||
|
.withVisible("amount")
|
||||||
|
.withVisible("formula")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, {})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"table.description",
|
||||||
|
"table.amount",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not includes all fields if the formula column is not included", async () => {
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.create()
|
||||||
|
const view = new ViewConfig(table)
|
||||||
|
.withHidden("name")
|
||||||
|
.withVisible("amount")
|
||||||
|
.withHidden("formula")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, {})
|
||||||
|
expect(result).toEqual(["table.amount"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes relationships columns", async () => {
|
||||||
|
const otherTable = new TableConfig("linkedTable")
|
||||||
|
.withField("id", FieldType.NUMBER)
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.withPrimary("id")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withRelation("link", otherTable._id)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const view = new ViewConfig(table)
|
||||||
|
.withVisible("name")
|
||||||
|
.withVisible("link")
|
||||||
|
.withRelationshipColumns("link", {
|
||||||
|
name: { visible: false },
|
||||||
|
amount: { visible: true },
|
||||||
|
formula: { visible: false },
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, allTables, {
|
||||||
|
relationships: true,
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"linkedTable.id",
|
||||||
|
"linkedTable.amount",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("excludes relationships fields when view is not included in the view", async () => {
|
||||||
|
const otherTable = new TableConfig("linkedTable")
|
||||||
|
.withField("id", FieldType.NUMBER)
|
||||||
|
.withPrimary("id")
|
||||||
|
.withDisplay("name")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withRelation("link", otherTable._id)
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const view = new ViewConfig(table)
|
||||||
|
.withVisible("name")
|
||||||
|
.withHidden("amount")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, allTables, {
|
||||||
|
relationships: true,
|
||||||
|
})
|
||||||
|
expect(result).toEqual(["table.name"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not include relationships columns for hidden links", async () => {
|
||||||
|
const otherTable = new TableConfig("linkedTable")
|
||||||
|
.withField("id", FieldType.NUMBER)
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.withPrimary("id")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withRelation("link", otherTable._id)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const view = new ViewConfig(table)
|
||||||
|
.withVisible("name")
|
||||||
|
.withHidden("link")
|
||||||
|
.withRelationshipColumns("link", {
|
||||||
|
name: { visible: false },
|
||||||
|
amount: { visible: true },
|
||||||
|
formula: { visible: false },
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, allTables, {
|
||||||
|
relationships: true,
|
||||||
|
})
|
||||||
|
expect(result).toEqual(["table.name"])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes all relationship fields if there is a formula column", async () => {
|
||||||
|
const otherTable = new TableConfig("linkedTable")
|
||||||
|
.withField("id", FieldType.NUMBER)
|
||||||
|
.withField("hidden", FieldType.STRING, { visible: false })
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.withField("ai", FieldType.AI)
|
||||||
|
.withRelation("link", "otherTableId")
|
||||||
|
.withPrimary("id")
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const table = new TableConfig("table")
|
||||||
|
.withRelation("link", otherTable._id)
|
||||||
|
.withField("formula", FieldType.FORMULA)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const view = new ViewConfig(table)
|
||||||
|
.withVisible("name")
|
||||||
|
.withVisible("formula")
|
||||||
|
.withHidden("link")
|
||||||
|
.withRelationshipColumns("link", {
|
||||||
|
name: { visible: false },
|
||||||
|
amount: { visible: true },
|
||||||
|
formula: { visible: false },
|
||||||
|
})
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, allTables, {
|
||||||
|
relationships: true,
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
"table.name",
|
||||||
|
"table.description",
|
||||||
|
"table.amount",
|
||||||
|
"linkedTable.name",
|
||||||
|
"linkedTable.description",
|
||||||
|
"linkedTable.amount",
|
||||||
|
"linkedTable.id",
|
||||||
|
"linkedTable.hidden",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("calculation view", () => {
|
||||||
|
it("does not include calculation fields", async () => {
|
||||||
|
const view = new ViewConfig(new TableConfig("table").create())
|
||||||
|
.withCalculation("average", "amount", CalculationType.AVG)
|
||||||
|
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, {})
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("includes visible fields calculation fields", async () => {
|
||||||
|
const view = new ViewConfig(new TableConfig("table").create())
|
||||||
|
.withCalculation("average", "amount", CalculationType.AVG)
|
||||||
|
.withHidden("name")
|
||||||
|
.withVisible("amount")
|
||||||
|
|
||||||
|
.create()
|
||||||
|
|
||||||
|
const result = await buildSqlFieldList(view, {})
|
||||||
|
expect(result).toEqual(["table.amount"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -8,7 +8,13 @@ import {
|
||||||
import tk from "timekeeper"
|
import tk from "timekeeper"
|
||||||
import emitter from "../../../../src/events"
|
import emitter from "../../../../src/events"
|
||||||
import { outputProcessing } from "../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../utilities/rowProcessor"
|
||||||
import { context, InternalTable, tenancy, utils } from "@budibase/backend-core"
|
import {
|
||||||
|
context,
|
||||||
|
setEnv,
|
||||||
|
InternalTable,
|
||||||
|
tenancy,
|
||||||
|
utils,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
AIOperationEnum,
|
AIOperationEnum,
|
||||||
|
@ -42,19 +48,8 @@ import { InternalTables } from "../../../db/utils"
|
||||||
import { withEnv } from "../../../environment"
|
import { withEnv } from "../../../environment"
|
||||||
import { JsTimeoutError } from "@budibase/string-templates"
|
import { JsTimeoutError } from "@budibase/string-templates"
|
||||||
import { isDate } from "../../../utilities"
|
import { isDate } from "../../../utilities"
|
||||||
|
import nock from "nock"
|
||||||
jest.mock("@budibase/pro", () => ({
|
import { mockChatGPTResponse } from "../../../tests/utilities/mocks/openai"
|
||||||
...jest.requireActual("@budibase/pro"),
|
|
||||||
ai: {
|
|
||||||
LargeLanguageModel: {
|
|
||||||
forCurrentTenant: async () => ({
|
|
||||||
llm: {},
|
|
||||||
run: jest.fn(() => `Mock LLM Response`),
|
|
||||||
buildPromptFromAIOperation: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
|
||||||
tk.freeze(timestamp)
|
tk.freeze(timestamp)
|
||||||
|
@ -99,6 +94,8 @@ if (descriptions.length) {
|
||||||
const ds = await dsProvider()
|
const ds = await dsProvider()
|
||||||
datasource = ds.datasource
|
datasource = ds.datasource
|
||||||
client = ds.client
|
client = ds.client
|
||||||
|
|
||||||
|
mocks.licenses.useCloudFree()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -172,10 +169,6 @@ if (descriptions.length) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
mocks.licenses.useCloudFree()
|
|
||||||
})
|
|
||||||
|
|
||||||
const getRowUsage = async () => {
|
const getRowUsage = async () => {
|
||||||
const { total } = await config.doInContext(undefined, () =>
|
const { total } = await config.doInContext(undefined, () =>
|
||||||
quotas.getCurrentUsageValues(
|
quotas.getCurrentUsageValues(
|
||||||
|
@ -2348,7 +2341,7 @@ if (descriptions.length) {
|
||||||
[FieldType.ARRAY]: ["options 2", "options 4"],
|
[FieldType.ARRAY]: ["options 2", "options 4"],
|
||||||
[FieldType.NUMBER]: generator.natural(),
|
[FieldType.NUMBER]: generator.natural(),
|
||||||
[FieldType.BOOLEAN]: generator.bool(),
|
[FieldType.BOOLEAN]: generator.bool(),
|
||||||
[FieldType.DATETIME]: generator.date().toISOString(),
|
[FieldType.DATETIME]: generator.date().toISOString().slice(0, 10),
|
||||||
[FieldType.ATTACHMENTS]: [setup.structures.basicAttachment()],
|
[FieldType.ATTACHMENTS]: [setup.structures.basicAttachment()],
|
||||||
[FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(),
|
[FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(),
|
||||||
[FieldType.FORMULA]: undefined, // generated field
|
[FieldType.FORMULA]: undefined, // generated field
|
||||||
|
@ -3224,10 +3217,17 @@ if (descriptions.length) {
|
||||||
isInternal &&
|
isInternal &&
|
||||||
describe("AI fields", () => {
|
describe("AI fields", () => {
|
||||||
let table: Table
|
let table: Table
|
||||||
|
let envCleanup: () => void
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mocks.licenses.useBudibaseAI()
|
mocks.licenses.useBudibaseAI()
|
||||||
mocks.licenses.useAICustomConfigs()
|
mocks.licenses.useAICustomConfigs()
|
||||||
|
envCleanup = setEnv({
|
||||||
|
OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd",
|
||||||
|
})
|
||||||
|
|
||||||
|
mockChatGPTResponse("Mock LLM Response")
|
||||||
|
|
||||||
table = await config.api.table.save(
|
table = await config.api.table.save(
|
||||||
saveTableRequest({
|
saveTableRequest({
|
||||||
schema: {
|
schema: {
|
||||||
|
@ -3251,7 +3251,9 @@ if (descriptions.length) {
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
jest.unmock("@budibase/pro")
|
nock.cleanAll()
|
||||||
|
envCleanup()
|
||||||
|
mocks.licenses.useCloudFree()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to save a row with an AI column", async () => {
|
it("should be able to save a row with an AI column", async () => {
|
||||||
|
|
|
@ -1683,6 +1683,151 @@ if (descriptions.length) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("datetime - date only", () => {
|
||||||
|
describe.each([true, false])(
|
||||||
|
"saved with timestamp: %s",
|
||||||
|
saveWithTimestamp => {
|
||||||
|
describe.each([true, false])(
|
||||||
|
"search with timestamp: %s",
|
||||||
|
searchWithTimestamp => {
|
||||||
|
const SAVE_SUFFIX = saveWithTimestamp
|
||||||
|
? "T00:00:00.000Z"
|
||||||
|
: ""
|
||||||
|
const SEARCH_SUFFIX = searchWithTimestamp
|
||||||
|
? "T00:00:00.000Z"
|
||||||
|
: ""
|
||||||
|
|
||||||
|
const JAN_1ST = `2020-01-01`
|
||||||
|
const JAN_10TH = `2020-01-10`
|
||||||
|
const JAN_30TH = `2020-01-30`
|
||||||
|
const UNEXISTING_DATE = `2020-01-03`
|
||||||
|
const NULL_DATE__ID = `null_date__id`
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
tableOrViewId = await createTableOrView({
|
||||||
|
dateid: { name: "dateid", type: FieldType.STRING },
|
||||||
|
date: {
|
||||||
|
name: "date",
|
||||||
|
type: FieldType.DATETIME,
|
||||||
|
dateOnly: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await createRows([
|
||||||
|
{ dateid: NULL_DATE__ID, date: null },
|
||||||
|
{ date: `${JAN_1ST}${SAVE_SUFFIX}` },
|
||||||
|
{ date: `${JAN_10TH}${SAVE_SUFFIX}` },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("equal", () => {
|
||||||
|
it("successfully finds a row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
|
||||||
|
}).toContainExactly([{ date: JAN_1ST }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully finds an ISO8601 row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` },
|
||||||
|
}).toContainExactly([{ date: JAN_10TH }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("finds a row with ISO8601 timestamp", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
|
||||||
|
}).toContainExactly([{ date: JAN_1ST }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
equal: {
|
||||||
|
date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`,
|
||||||
|
},
|
||||||
|
}).toFindNothing()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("notEqual", () => {
|
||||||
|
it("successfully finds a row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
notEqual: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
|
||||||
|
}).toContainExactly([
|
||||||
|
{ date: JAN_10TH },
|
||||||
|
{ dateid: NULL_DATE__ID },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
notEqual: { date: `${JAN_30TH}${SEARCH_SUFFIX}` },
|
||||||
|
}).toContainExactly([
|
||||||
|
{ date: JAN_1ST },
|
||||||
|
{ date: JAN_10TH },
|
||||||
|
{ dateid: NULL_DATE__ID },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("oneOf", () => {
|
||||||
|
it("successfully finds a row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] },
|
||||||
|
}).toContainExactly([{ date: JAN_1ST }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
oneOf: {
|
||||||
|
date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`],
|
||||||
|
},
|
||||||
|
}).toFindNothing()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("range", () => {
|
||||||
|
it("successfully finds a row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
range: {
|
||||||
|
date: {
|
||||||
|
low: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||||||
|
high: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).toContainExactly([{ date: JAN_1ST }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully finds multiple rows", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
range: {
|
||||||
|
date: {
|
||||||
|
low: `${JAN_1ST}${SEARCH_SUFFIX}`,
|
||||||
|
high: `${JAN_10TH}${SEARCH_SUFFIX}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).toContainExactly([
|
||||||
|
{ date: JAN_1ST },
|
||||||
|
{ date: JAN_10TH },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully finds no rows", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
range: {
|
||||||
|
date: {
|
||||||
|
low: `${JAN_30TH}${SEARCH_SUFFIX}`,
|
||||||
|
high: `${JAN_30TH}${SEARCH_SUFFIX}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).toFindNothing()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
isInternal &&
|
isInternal &&
|
||||||
!isInMemory &&
|
!isInMemory &&
|
||||||
describe("AI Column", () => {
|
describe("AI Column", () => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
AIOperationEnum,
|
||||||
ArrayOperator,
|
ArrayOperator,
|
||||||
BasicOperator,
|
BasicOperator,
|
||||||
BBReferenceFieldSubType,
|
BBReferenceFieldSubType,
|
||||||
|
@ -42,7 +43,9 @@ import {
|
||||||
} from "../../../integrations/tests/utils"
|
} from "../../../integrations/tests/utils"
|
||||||
import merge from "lodash/merge"
|
import merge from "lodash/merge"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { context, db, events, roles } from "@budibase/backend-core"
|
import { context, db, events, roles, setEnv } from "@budibase/backend-core"
|
||||||
|
import { mockChatGPTResponse } from "../../../tests/utilities/mocks/openai"
|
||||||
|
import nock from "nock"
|
||||||
|
|
||||||
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
|
const descriptions = datasourceDescribe({ exclude: [DatabaseName.MONGODB] })
|
||||||
|
|
||||||
|
@ -100,6 +103,7 @@ if (descriptions.length) {
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
|
mocks.licenses.useCloudFree()
|
||||||
|
|
||||||
const ds = await dsProvider()
|
const ds = await dsProvider()
|
||||||
rawDatasource = ds.rawDatasource
|
rawDatasource = ds.rawDatasource
|
||||||
|
@ -109,7 +113,6 @@ if (descriptions.length) {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
mocks.licenses.useCloudFree()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("view crud", () => {
|
describe("view crud", () => {
|
||||||
|
@ -507,7 +510,6 @@ if (descriptions.length) {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("readonly fields can be used on free license", async () => {
|
it("readonly fields can be used on free license", async () => {
|
||||||
mocks.licenses.useCloudFree()
|
|
||||||
const table = await config.api.table.save(
|
const table = await config.api.table.save(
|
||||||
saveTableRequest({
|
saveTableRequest({
|
||||||
schema: {
|
schema: {
|
||||||
|
@ -933,6 +935,95 @@ if (descriptions.length) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
isInternal &&
|
||||||
|
describe("AI fields", () => {
|
||||||
|
let envCleanup: () => void
|
||||||
|
beforeAll(() => {
|
||||||
|
mocks.licenses.useBudibaseAI()
|
||||||
|
mocks.licenses.useAICustomConfigs()
|
||||||
|
envCleanup = setEnv({
|
||||||
|
OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd",
|
||||||
|
})
|
||||||
|
|
||||||
|
mockChatGPTResponse(prompt => {
|
||||||
|
if (prompt.includes("elephant")) {
|
||||||
|
return "big"
|
||||||
|
}
|
||||||
|
if (prompt.includes("mouse")) {
|
||||||
|
return "small"
|
||||||
|
}
|
||||||
|
if (prompt.includes("whale")) {
|
||||||
|
return "big"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
nock.cleanAll()
|
||||||
|
envCleanup()
|
||||||
|
mocks.licenses.useCloudFree()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can use AI fields in view calculations", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
animal: {
|
||||||
|
name: "animal",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
bigOrSmall: {
|
||||||
|
name: "bigOrSmall",
|
||||||
|
type: FieldType.AI,
|
||||||
|
operation: AIOperationEnum.CATEGORISE_TEXT,
|
||||||
|
categories: "big,small",
|
||||||
|
columns: ["animal"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
type: ViewV2Type.CALCULATION,
|
||||||
|
schema: {
|
||||||
|
bigOrSmall: {
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
visible: true,
|
||||||
|
calculationType: CalculationType.COUNT,
|
||||||
|
field: "animal",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
animal: "elephant",
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
animal: "mouse",
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
animal: "whale",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows } = await config.api.row.search(view.id, {
|
||||||
|
sort: "bigOrSmall",
|
||||||
|
sortOrder: SortOrder.ASCENDING,
|
||||||
|
})
|
||||||
|
expect(rows).toHaveLength(2)
|
||||||
|
expect(rows[0].bigOrSmall).toEqual("big")
|
||||||
|
expect(rows[1].bigOrSmall).toEqual("small")
|
||||||
|
expect(rows[0].count).toEqual(2)
|
||||||
|
expect(rows[1].count).toEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
|
@ -1836,7 +1927,6 @@ if (descriptions.length) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
mocks.licenses.useCloudFree()
|
|
||||||
const view = await getDelegate(res)
|
const view = await getDelegate(res)
|
||||||
expect(view.schema?.one).toEqual(
|
expect(view.schema?.one).toEqual(
|
||||||
expect.objectContaining({ visible: true, readonly: true })
|
expect.objectContaining({ visible: true, readonly: true })
|
||||||
|
|
|
@ -27,11 +27,9 @@ import {
|
||||||
Hosting,
|
Hosting,
|
||||||
ActionImplementation,
|
ActionImplementation,
|
||||||
AutomationStepDefinition,
|
AutomationStepDefinition,
|
||||||
FeatureFlag,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../sdk"
|
import sdk from "../sdk"
|
||||||
import { getAutomationPlugin } from "../utilities/fileSystem"
|
import { getAutomationPlugin } from "../utilities/fileSystem"
|
||||||
import { features } from "@budibase/backend-core"
|
|
||||||
|
|
||||||
type ActionImplType = ActionImplementations<
|
type ActionImplType = ActionImplementations<
|
||||||
typeof env.SELF_HOSTED extends "true" ? Hosting.SELF : Hosting.CLOUD
|
typeof env.SELF_HOSTED extends "true" ? Hosting.SELF : Hosting.CLOUD
|
||||||
|
@ -78,6 +76,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record<
|
||||||
LOOP: loop.definition,
|
LOOP: loop.definition,
|
||||||
COLLECT: collect.definition,
|
COLLECT: collect.definition,
|
||||||
TRIGGER_AUTOMATION_RUN: triggerAutomationRun.definition,
|
TRIGGER_AUTOMATION_RUN: triggerAutomationRun.definition,
|
||||||
|
BRANCH: branch.definition,
|
||||||
// these used to be lowercase step IDs, maintain for backwards compat
|
// these used to be lowercase step IDs, maintain for backwards compat
|
||||||
discord: discord.definition,
|
discord: discord.definition,
|
||||||
slack: slack.definition,
|
slack: slack.definition,
|
||||||
|
@ -105,14 +104,7 @@ if (env.SELF_HOSTED) {
|
||||||
export async function getActionDefinitions(): Promise<
|
export async function getActionDefinitions(): Promise<
|
||||||
Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
|
Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
|
||||||
> {
|
> {
|
||||||
if (await features.isEnabled(FeatureFlag.AUTOMATION_BRANCHING)) {
|
if (env.SELF_HOSTED) {
|
||||||
BUILTIN_ACTION_DEFINITIONS["BRANCH"] = branch.definition
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
env.SELF_HOSTED ||
|
|
||||||
(await features.isEnabled(FeatureFlag.BUDIBASE_AI)) ||
|
|
||||||
(await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS))
|
|
||||||
) {
|
|
||||||
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition
|
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ export async function run({
|
||||||
const ctx: any = buildCtx(appId, emitter, {
|
const ctx: any = buildCtx(appId, emitter, {
|
||||||
body: inputs.row,
|
body: inputs.row,
|
||||||
params: {
|
params: {
|
||||||
tableId: inputs.row.tableId,
|
tableId: decodeURIComponent(inputs.row.tableId),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -85,7 +85,7 @@ export async function run({
|
||||||
_rev: inputs.revision,
|
_rev: inputs.revision,
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
tableId: inputs.tableId,
|
tableId: decodeURIComponent(inputs.tableId),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,8 @@ import {
|
||||||
AutomationIOType,
|
AutomationIOType,
|
||||||
OpenAIStepInputs,
|
OpenAIStepInputs,
|
||||||
OpenAIStepOutputs,
|
OpenAIStepOutputs,
|
||||||
FeatureFlag,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { env, features } from "@budibase/backend-core"
|
import { env } from "@budibase/backend-core"
|
||||||
import * as automationUtils from "../automationUtils"
|
import * as automationUtils from "../automationUtils"
|
||||||
import * as pro from "@budibase/pro"
|
import * as pro from "@budibase/pro"
|
||||||
|
|
||||||
|
@ -99,12 +98,8 @@ export async function run({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response
|
let response
|
||||||
const customConfigsEnabled =
|
const customConfigsEnabled = await pro.features.isAICustomConfigsEnabled()
|
||||||
(await features.isEnabled(FeatureFlag.AI_CUSTOM_CONFIGS)) &&
|
const budibaseAIEnabled = await pro.features.isBudibaseAIEnabled()
|
||||||
(await pro.features.isAICustomConfigsEnabled())
|
|
||||||
const budibaseAIEnabled =
|
|
||||||
(await features.isEnabled(FeatureFlag.BUDIBASE_AI)) &&
|
|
||||||
(await pro.features.isBudibaseAIEnabled())
|
|
||||||
|
|
||||||
let llmWrapper
|
let llmWrapper
|
||||||
if (budibaseAIEnabled || customConfigsEnabled) {
|
if (budibaseAIEnabled || customConfigsEnabled) {
|
||||||
|
|
|
@ -122,9 +122,10 @@ export async function run({
|
||||||
sortType =
|
sortType =
|
||||||
fieldType === FieldType.NUMBER ? FieldType.NUMBER : FieldType.STRING
|
fieldType === FieldType.NUMBER ? FieldType.NUMBER : FieldType.STRING
|
||||||
}
|
}
|
||||||
|
// when passing the tableId in the Ctx it needs to be decoded
|
||||||
const ctx = buildCtx(appId, null, {
|
const ctx = buildCtx(appId, null, {
|
||||||
params: {
|
params: {
|
||||||
tableId,
|
tableId: decodeURIComponent(tableId),
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
sortType,
|
sortType,
|
||||||
|
|
|
@ -90,6 +90,8 @@ export async function run({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tableId = inputs.row.tableId
|
const tableId = inputs.row.tableId
|
||||||
|
? decodeURIComponent(inputs.row.tableId)
|
||||||
|
: inputs.row.tableId
|
||||||
|
|
||||||
// Base update
|
// Base update
|
||||||
let rowUpdate: Record<string, any>
|
let rowUpdate: Record<string, any>
|
||||||
|
@ -157,7 +159,7 @@ export async function run({
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
rowId: inputs.rowId,
|
rowId: inputs.rowId,
|
||||||
tableId,
|
tableId: tableId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await rowController.patch(ctx)
|
await rowController.patch(ctx)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { EmptyFilterOption, SortOrder, Table } from "@budibase/types"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||||
import * as automation from "../index"
|
import * as automation from "../index"
|
||||||
|
import { basicTable } from "../../tests/utilities/structures"
|
||||||
|
|
||||||
const NAME = "Test"
|
const NAME = "Test"
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ describe("Test a query step automation", () => {
|
||||||
await automation.init()
|
await automation.init()
|
||||||
await config.init()
|
await config.init()
|
||||||
table = await config.createTable()
|
table = await config.createTable()
|
||||||
|
|
||||||
const row = {
|
const row = {
|
||||||
name: NAME,
|
name: NAME,
|
||||||
description: "original description",
|
description: "original description",
|
||||||
|
@ -153,4 +155,32 @@ describe("Test a query step automation", () => {
|
||||||
expect(result.steps[0].outputs.rows).toBeDefined()
|
expect(result.steps[0].outputs.rows).toBeDefined()
|
||||||
expect(result.steps[0].outputs.rows.length).toBe(2)
|
expect(result.steps[0].outputs.rows.length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("return rows when querying a table with a space in the name", async () => {
|
||||||
|
const tableWithSpaces = await config.createTable({
|
||||||
|
...basicTable(),
|
||||||
|
name: "table with spaces",
|
||||||
|
})
|
||||||
|
await config.createRow({
|
||||||
|
name: NAME,
|
||||||
|
tableId: tableWithSpaces._id,
|
||||||
|
})
|
||||||
|
const result = await createAutomationBuilder({
|
||||||
|
name: "Return All Test",
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
.appAction({ fields: {} })
|
||||||
|
.queryRows(
|
||||||
|
{
|
||||||
|
tableId: tableWithSpaces._id!,
|
||||||
|
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
|
||||||
|
filters: {},
|
||||||
|
},
|
||||||
|
{ stepName: "Query table with spaces" }
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
expect(result.steps[0].outputs.success).toBe(true)
|
||||||
|
expect(result.steps[0].outputs.rows).toBeDefined()
|
||||||
|
expect(result.steps[0].outputs.rows.length).toBe(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,7 +13,7 @@ const mainDescriptions = datasourceDescribe({
|
||||||
|
|
||||||
if (mainDescriptions.length) {
|
if (mainDescriptions.length) {
|
||||||
describe.each(mainDescriptions)(
|
describe.each(mainDescriptions)(
|
||||||
"/postgres integrations",
|
"/postgres integrations ($dbName)",
|
||||||
({ config, dsProvider }) => {
|
({ config, dsProvider }) => {
|
||||||
let datasource: Datasource
|
let datasource: Datasource
|
||||||
let client: Knex
|
let client: Knex
|
||||||
|
|
|
@ -73,6 +73,27 @@ describe("Captures of real examples", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("read", () => {
|
describe("read", () => {
|
||||||
|
it("should retrieve all fields if non are specified", () => {
|
||||||
|
const queryJson = getJson("basicFetch.json")
|
||||||
|
delete queryJson.resource
|
||||||
|
|
||||||
|
let query = new Sql(SqlClient.POSTGRES)._query(queryJson)
|
||||||
|
expect(query).toEqual({
|
||||||
|
bindings: [primaryLimit],
|
||||||
|
sql: `select * from "persons" as "a" order by "a"."firstname" asc nulls first, "a"."personid" asc limit $1`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should retrieve only requested fields", () => {
|
||||||
|
const queryJson = getJson("basicFetch.json")
|
||||||
|
|
||||||
|
let query = new Sql(SqlClient.POSTGRES)._query(queryJson)
|
||||||
|
expect(query).toEqual({
|
||||||
|
bindings: [primaryLimit],
|
||||||
|
sql: `select "a"."year", "a"."firstname", "a"."personid", "a"."age", "a"."type", "a"."lastname" from "persons" as "a" order by "a"."firstname" asc nulls first, "a"."personid" asc limit $1`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should handle basic retrieval with relationships", () => {
|
it("should handle basic retrieval with relationships", () => {
|
||||||
const queryJson = getJson("basicFetchWithRelationships.json")
|
const queryJson = getJson("basicFetchWithRelationships.json")
|
||||||
let query = new Sql(SqlClient.POSTGRES, relationshipLimit)._query(
|
let query = new Sql(SqlClient.POSTGRES, relationshipLimit)._query(
|
||||||
|
@ -112,9 +133,9 @@ describe("Captures of real examples", () => {
|
||||||
bindings: [primaryLimit, relationshipLimit],
|
bindings: [primaryLimit, relationshipLimit],
|
||||||
sql: expect.stringContaining(
|
sql: expect.stringContaining(
|
||||||
multiline(
|
multiline(
|
||||||
`with "paginated" as (select "a".* from "products" as "a" order by "a"."productname" asc nulls first, "a"."productid" asc limit $1)
|
`with "paginated" as (select * from "products" as "a" order by "a"."productname" asc nulls first, "a"."productid" asc limit $1)
|
||||||
select "a".*, (select json_agg(json_build_object('executorid',"b"."executorid",'qaid',"b"."qaid",'taskid',"b"."taskid",'completed',"b"."completed",'taskname',"b"."taskname"))
|
select "a"."productname", "a"."productid", (select json_agg(json_build_object('executorid',"b"."executorid",'qaid',"b"."qaid",'taskid',"b"."taskid",'completed',"b"."completed",'taskname',"b"."taskname"))
|
||||||
from (select "b".* from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid" where "c"."productid" = "a"."productid" order by "b"."taskid" asc limit $2) as "b") as "tasks"
|
from (select "b"."executorid", "b"."qaid", "b"."taskid", "b"."completed", "b"."taskname" from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid" where "c"."productid" = "a"."productid" order by "b"."taskid" asc limit $2) as "b") as "tasks"
|
||||||
from "paginated" as "a" order by "a"."productname" asc nulls first, "a"."productid" asc`
|
from "paginated" as "a" order by "a"."productname" asc nulls first, "a"."productid" asc`
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -130,9 +151,9 @@ describe("Captures of real examples", () => {
|
||||||
expect(query).toEqual({
|
expect(query).toEqual({
|
||||||
bindings: [...filters, relationshipLimit, relationshipLimit],
|
bindings: [...filters, relationshipLimit, relationshipLimit],
|
||||||
sql: multiline(
|
sql: multiline(
|
||||||
`with "paginated" as (select "a".* from "tasks" as "a" where "a"."taskid" in ($1, $2) order by "a"."taskid" asc limit $3)
|
`with "paginated" as (select * from "tasks" as "a" where "a"."taskid" in ($1, $2) order by "a"."taskid" asc limit $3)
|
||||||
select "a".*, (select json_agg(json_build_object('productid',"b"."productid",'productname',"b"."productname"))
|
select "a"."executorid", "a"."taskname", "a"."taskid", "a"."completed", "a"."qaid", (select json_agg(json_build_object('productid',"b"."productid",'productname',"b"."productname"))
|
||||||
from (select "b".* from "products" as "b" inner join "products_tasks" as "c" on "b"."productid" = "c"."productid"
|
from (select "b"."productid", "b"."productname" from "products" as "b" inner join "products_tasks" as "c" on "b"."productid" = "c"."productid"
|
||||||
where "c"."taskid" = "a"."taskid" order by "b"."productid" asc limit $4) as "b") as "products" from "paginated" as "a" order by "a"."taskid" asc`
|
where "c"."taskid" = "a"."taskid" order by "b"."productid" asc limit $4) as "b") as "products" from "paginated" as "a" order by "a"."taskid" asc`
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
@ -209,7 +230,7 @@ describe("Captures of real examples", () => {
|
||||||
bindings: ["ddd", ""],
|
bindings: ["ddd", ""],
|
||||||
sql: multiline(`delete from "compositetable" as "a"
|
sql: multiline(`delete from "compositetable" as "a"
|
||||||
where COALESCE("a"."keypartone" = $1, FALSE) and COALESCE("a"."keyparttwo" = $2, FALSE)
|
where COALESCE("a"."keypartone" = $1, FALSE) and COALESCE("a"."keyparttwo" = $2, FALSE)
|
||||||
returning "a".*`),
|
returning "a"."keyparttwo", "a"."keypartone", "a"."name"`),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue