Merge branch 'v3-ui' of github.com:Budibase/budibase into new-rbac-ui
This commit is contained in:
commit
06413fda58
|
@ -3,26 +3,50 @@ name: deploy-featurebranch
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [
|
types: [
|
||||||
labeled,
|
labeled,
|
||||||
# default types below (https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request)
|
# default types below (https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request)
|
||||||
opened,
|
opened,
|
||||||
synchronize,
|
synchronize,
|
||||||
reopened,
|
reopened,
|
||||||
]
|
]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') &&
|
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') &&
|
||||||
contains(github.event.pull_request.labels.*.name, 'feature-branch')
|
(
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'feature-branch') ||
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'feature-branch-pro') ||
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'feature-branch-team') ||
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'feature-branch-business') ||
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'feature-branch-enterprise')
|
||||||
|
)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set PAYLOAD_LICENSE_TYPE
|
||||||
|
id: set_license_type
|
||||||
|
run: |
|
||||||
|
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch') }}" == "true" ]]; then
|
||||||
|
echo "PAYLOAD_LICENSE_TYPE=free" >> $GITHUB_ENV
|
||||||
|
elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch-pro') }}" == "true" ]]; then
|
||||||
|
echo "PAYLOAD_LICENSE_TYPE=pro" >> $GITHUB_ENV
|
||||||
|
elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch-team') }}" == "true" ]]; then
|
||||||
|
echo "PAYLOAD_LICENSE_TYPE=team" >> $GITHUB_ENV
|
||||||
|
elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch-business') }}" == "true" ]]; then
|
||||||
|
echo "PAYLOAD_LICENSE_TYPE=business" >> $GITHUB_ENV
|
||||||
|
elif [[ "${{ contains(github.event.pull_request.labels.*.name, 'feature-branch-enterprise') }}" == "true" ]]; then
|
||||||
|
echo "PAYLOAD_LICENSE_TYPE=enterprise" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "PAYLOAD_LICENSE_TYPE=free" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||||
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
|
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
PAYLOAD_LICENSE_TYPE: "free"
|
PAYLOAD_LICENSE_TYPE: ${{ env.PAYLOAD_LICENSE_TYPE }}
|
||||||
with:
|
with:
|
||||||
repository: budibase/budibase-deploys
|
repository: budibase/budibase-deploys
|
||||||
event: featurebranch-qa-deploy
|
event: featurebranch-qa-deploy
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.32.15",
|
"version": "2.32.17",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -28,6 +28,7 @@ export enum Config {
|
||||||
OIDC = "oidc",
|
OIDC = "oidc",
|
||||||
OIDC_LOGOS = "logos_oidc",
|
OIDC_LOGOS = "logos_oidc",
|
||||||
SCIM = "scim",
|
SCIM = "scim",
|
||||||
|
AI = "AI",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MIN_VALID_DATE = new Date(-2147483647000)
|
export const MIN_VALID_DATE = new Date(-2147483647000)
|
||||||
|
|
|
@ -211,6 +211,17 @@ export class DatabaseImpl implements Database {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async tryGet<T extends Document>(id?: string): Promise<T | undefined> {
|
||||||
|
try {
|
||||||
|
return await this.get<T>(id)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.statusCode === 404) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getMultiple<T extends Document>(
|
async getMultiple<T extends Document>(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
opts?: { allowMissing?: boolean; excludeDocs?: boolean }
|
opts?: { allowMissing?: boolean; excludeDocs?: boolean }
|
||||||
|
|
|
@ -42,6 +42,13 @@ export class DDInstrumentedDatabase implements Database {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tryGet<T extends Document>(id?: string | undefined): Promise<T | undefined> {
|
||||||
|
return tracer.trace("db.tryGet", span => {
|
||||||
|
span?.addTags({ db_name: this.name, doc_id: id })
|
||||||
|
return this.db.tryGet(id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
getMultiple<T extends Document>(
|
getMultiple<T extends Document>(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
opts?: { allowMissing?: boolean | undefined } | undefined
|
opts?: { allowMissing?: boolean | undefined } | undefined
|
||||||
|
|
|
@ -272,6 +272,7 @@ export const flags = new FlagSet({
|
||||||
SQS: Flag.boolean(env.isDev()),
|
SQS: Flag.boolean(env.isDev()),
|
||||||
[FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(env.isDev()),
|
[FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(env.isDev()),
|
||||||
[FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(env.isDev()),
|
[FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(env.isDev()),
|
||||||
|
[FeatureFlag.TABLES_DEFAULT_ADMIN]: Flag.boolean(env.isDev()),
|
||||||
})
|
})
|
||||||
|
|
||||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
|
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
|
||||||
|
|
|
@ -457,7 +457,9 @@ export function getExternalRoleID(roleId: string, version?: string) {
|
||||||
roleId.startsWith(DocumentType.ROLE) &&
|
roleId.startsWith(DocumentType.ROLE) &&
|
||||||
(isBuiltin(roleId) || version === RoleIDVersion.NAME)
|
(isBuiltin(roleId) || version === RoleIDVersion.NAME)
|
||||||
) {
|
) {
|
||||||
return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1]
|
const parts = roleId.split(SEPARATOR)
|
||||||
|
parts.shift()
|
||||||
|
return parts.join(SEPARATOR)
|
||||||
}
|
}
|
||||||
return roleId
|
return roleId
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,6 +273,7 @@ class InternalBuilder {
|
||||||
const col = parts.pop()!
|
const col = parts.pop()!
|
||||||
const schema = this.table.schema[col]
|
const schema = this.table.schema[col]
|
||||||
let identifier = this.quotedIdentifier(field)
|
let identifier = this.quotedIdentifier(field)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
schema.type === FieldType.STRING ||
|
schema.type === FieldType.STRING ||
|
||||||
schema.type === FieldType.LONGFORM ||
|
schema.type === FieldType.LONGFORM ||
|
||||||
|
@ -957,6 +958,13 @@ class InternalBuilder {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAggregateField(field: string): boolean {
|
||||||
|
const found = this.query.resource?.aggregations?.find(
|
||||||
|
aggregation => aggregation.name === field
|
||||||
|
)
|
||||||
|
return !!found
|
||||||
|
}
|
||||||
|
|
||||||
addSorting(query: Knex.QueryBuilder): Knex.QueryBuilder {
|
addSorting(query: Knex.QueryBuilder): Knex.QueryBuilder {
|
||||||
let { sort, resource } = this.query
|
let { sort, resource } = this.query
|
||||||
const primaryKey = this.table.primary
|
const primaryKey = this.table.primary
|
||||||
|
@ -979,13 +987,17 @@ class InternalBuilder {
|
||||||
nulls = value.direction === SortOrder.ASCENDING ? "first" : "last"
|
nulls = value.direction === SortOrder.ASCENDING ? "first" : "last"
|
||||||
}
|
}
|
||||||
|
|
||||||
let composite = `${aliased}.${key}`
|
if (this.isAggregateField(key)) {
|
||||||
if (this.client === SqlClient.ORACLE) {
|
query = query.orderBy(key, direction, nulls)
|
||||||
query = query.orderByRaw(
|
|
||||||
`${this.convertClobs(composite)} ${direction} nulls ${nulls}`
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
query = query.orderBy(composite, direction, nulls)
|
let composite = `${aliased}.${key}`
|
||||||
|
if (this.client === SqlClient.ORACLE) {
|
||||||
|
query = query.orderByRaw(
|
||||||
|
`${this.convertClobs(composite)} ${direction} nulls ${nulls}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
query = query.orderBy(composite, direction, nulls)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1276,7 +1288,8 @@ class InternalBuilder {
|
||||||
schema.constraints?.presence === true ||
|
schema.constraints?.presence === true ||
|
||||||
schema.type === FieldType.FORMULA ||
|
schema.type === FieldType.FORMULA ||
|
||||||
schema.type === FieldType.AUTO ||
|
schema.type === FieldType.AUTO ||
|
||||||
schema.type === FieldType.LINK
|
schema.type === FieldType.LINK ||
|
||||||
|
schema.type === FieldType.AI
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import SchemaBuilder = Knex.SchemaBuilder
|
||||||
import CreateTableBuilder = Knex.CreateTableBuilder
|
import CreateTableBuilder = Knex.CreateTableBuilder
|
||||||
|
|
||||||
function isIgnoredType(type: FieldType) {
|
function isIgnoredType(type: FieldType) {
|
||||||
const ignored = [FieldType.LINK, FieldType.FORMULA]
|
const ignored = [FieldType.LINK, FieldType.FORMULA, FieldType.AI]
|
||||||
return ignored.indexOf(type) !== -1
|
return ignored.indexOf(type) !== -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,6 +144,9 @@ function generateSchema(
|
||||||
case FieldType.FORMULA:
|
case FieldType.FORMULA:
|
||||||
// This is allowed, but nothing to do on the external datasource
|
// This is allowed, but nothing to do on the external datasource
|
||||||
break
|
break
|
||||||
|
case FieldType.AI:
|
||||||
|
// This is allowed, but nothing to do on the external datasource
|
||||||
|
break
|
||||||
case FieldType.ATTACHMENTS:
|
case FieldType.ATTACHMENTS:
|
||||||
case FieldType.ATTACHMENT_SINGLE:
|
case FieldType.ATTACHMENT_SINGLE:
|
||||||
case FieldType.SIGNATURE_SINGLE:
|
case FieldType.SIGNATURE_SINGLE:
|
||||||
|
|
|
@ -102,6 +102,14 @@ export const useAppBuilders = () => {
|
||||||
return useFeature(Feature.APP_BUILDERS)
|
return useFeature(Feature.APP_BUILDERS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useBudibaseAI = () => {
|
||||||
|
return useFeature(Feature.BUDIBASE_AI)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAICustomConfigs = () => {
|
||||||
|
return useFeature(Feature.AI_CUSTOM_CONFIGS)
|
||||||
|
}
|
||||||
|
|
||||||
// QUOTAS
|
// QUOTAS
|
||||||
|
|
||||||
export const setAutomationLogsQuota = (value: number) => {
|
export const setAutomationLogsQuota = (value: number) => {
|
||||||
|
|
|
@ -124,7 +124,7 @@
|
||||||
subtype: column.subtype,
|
subtype: column.subtype,
|
||||||
visible: column.visible,
|
visible: column.visible,
|
||||||
readonly: column.readonly,
|
readonly: column.readonly,
|
||||||
constraints: column.constraints, // This is needed to properly display "users" column
|
icon: column.icon,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables, datasources } from "stores/builder"
|
import { tables, datasources } from "stores/builder"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
import {
|
import {
|
||||||
FIELDS,
|
FIELDS,
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils"
|
import { getAutoColumnInformation, buildAutoColumn } from "helpers/utils"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import AIFieldConfiguration from "components/common/AIFieldConfiguration.svelte"
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import { getBindings } from "components/backend/DataTable/formula"
|
import { getBindings } from "components/backend/DataTable/formula"
|
||||||
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
import JSONSchemaModal from "./JSONSchemaModal.svelte"
|
||||||
|
@ -50,18 +52,13 @@
|
||||||
import { isEnabled } from "helpers/featureFlags"
|
import { isEnabled } from "helpers/featureFlags"
|
||||||
import { getUserBindings } from "dataBinding"
|
import { getUserBindings } from "dataBinding"
|
||||||
|
|
||||||
const AUTO_TYPE = FieldType.AUTO
|
export let field
|
||||||
const FORMULA_TYPE = FieldType.FORMULA
|
|
||||||
const LINK_TYPE = FieldType.LINK
|
|
||||||
const STRING_TYPE = FieldType.STRING
|
|
||||||
const NUMBER_TYPE = FieldType.NUMBER
|
|
||||||
const JSON_TYPE = FieldType.JSON
|
|
||||||
const DATE_TYPE = FieldType.DATETIME
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const { dispatch: gridDispatch, rows } = getContext("grid")
|
const { dispatch: gridDispatch, rows } = getContext("grid")
|
||||||
|
const SafeID = `${makePropSafe("user")}.${makePropSafe("_id")}`
|
||||||
export let field
|
const SingleUserDefault = `{{ ${SafeID} }}`
|
||||||
|
const MultiUserDefault = `{{ js "${btoa(`return [$("${SafeID}")]`)}" }}`
|
||||||
|
|
||||||
let mounted = false
|
let mounted = false
|
||||||
let originalName
|
let originalName
|
||||||
|
@ -104,13 +101,15 @@
|
||||||
let optionsValid = true
|
let optionsValid = true
|
||||||
|
|
||||||
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
||||||
|
$: aiEnabled =
|
||||||
|
$licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled
|
||||||
$: if (primaryDisplay) {
|
$: if (primaryDisplay) {
|
||||||
editableColumn.constraints.presence = { allowEmpty: false }
|
editableColumn.constraints.presence = { allowEmpty: false }
|
||||||
}
|
}
|
||||||
$: {
|
$: {
|
||||||
// this parses any changes the user has made when creating a new internal relationship
|
// this parses any changes the user has made when creating a new internal relationship
|
||||||
// into what we expect the schema to look like
|
// into what we expect the schema to look like
|
||||||
if (editableColumn.type === LINK_TYPE) {
|
if (editableColumn.type === FieldType.LINK) {
|
||||||
relationshipTableIdPrimary = table._id
|
relationshipTableIdPrimary = table._id
|
||||||
if (relationshipPart1 === PrettyRelationshipDefinitions.ONE) {
|
if (relationshipPart1 === PrettyRelationshipDefinitions.ONE) {
|
||||||
relationshipOpts2 = relationshipOpts2.filter(
|
relationshipOpts2 = relationshipOpts2.filter(
|
||||||
|
@ -147,7 +146,7 @@
|
||||||
UNEDITABLE_USER_FIELDS.includes(editableColumn.name)
|
UNEDITABLE_USER_FIELDS.includes(editableColumn.name)
|
||||||
$: invalid =
|
$: invalid =
|
||||||
!editableColumn?.name ||
|
!editableColumn?.name ||
|
||||||
(editableColumn?.type === LINK_TYPE && !editableColumn?.tableId) ||
|
(editableColumn?.type === FieldType.LINK && !editableColumn?.tableId) ||
|
||||||
Object.keys(errors).length !== 0 ||
|
Object.keys(errors).length !== 0 ||
|
||||||
!optionsValid
|
!optionsValid
|
||||||
$: errors = checkErrors(editableColumn)
|
$: errors = checkErrors(editableColumn)
|
||||||
|
@ -173,9 +172,9 @@
|
||||||
$: defaultValuesEnabled = isEnabled("DEFAULT_VALUES")
|
$: defaultValuesEnabled = isEnabled("DEFAULT_VALUES")
|
||||||
$: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type)
|
$: canHaveDefault = !required && canHaveDefaultColumn(editableColumn.type)
|
||||||
$: canBeRequired =
|
$: canBeRequired =
|
||||||
editableColumn?.type !== LINK_TYPE &&
|
editableColumn?.type !== FieldType.LINK &&
|
||||||
!uneditable &&
|
!uneditable &&
|
||||||
editableColumn?.type !== AUTO_TYPE &&
|
editableColumn?.type !== FieldType.AUTO &&
|
||||||
!editableColumn.autocolumn
|
!editableColumn.autocolumn
|
||||||
$: hasDefault =
|
$: hasDefault =
|
||||||
editableColumn?.default != null && editableColumn?.default !== ""
|
editableColumn?.default != null && editableColumn?.default !== ""
|
||||||
|
@ -224,7 +223,7 @@
|
||||||
|
|
||||||
function makeFieldId(type, subtype, autocolumn) {
|
function makeFieldId(type, subtype, autocolumn) {
|
||||||
// don't make field IDs for auto types
|
// don't make field IDs for auto types
|
||||||
if (type === AUTO_TYPE || autocolumn) {
|
if (type === FieldType.AUTO || autocolumn) {
|
||||||
return type.toUpperCase()
|
return type.toUpperCase()
|
||||||
} else if (
|
} else if (
|
||||||
type === FieldType.BB_REFERENCE ||
|
type === FieldType.BB_REFERENCE ||
|
||||||
|
@ -249,7 +248,7 @@
|
||||||
// Here we are setting the relationship values based on the editableColumn
|
// Here we are setting the relationship values based on the editableColumn
|
||||||
// This part of the code is used when viewing an existing field hence the check
|
// This part of the code is used when viewing an existing field hence the check
|
||||||
// for the tableId
|
// for the tableId
|
||||||
if (editableColumn.type === LINK_TYPE && editableColumn.tableId) {
|
if (editableColumn.type === FieldType.LINK && editableColumn.tableId) {
|
||||||
relationshipTableIdPrimary = table._id
|
relationshipTableIdPrimary = table._id
|
||||||
relationshipTableIdSecondary = editableColumn.tableId
|
relationshipTableIdSecondary = editableColumn.tableId
|
||||||
if (editableColumn.relationshipType in relationshipMap) {
|
if (editableColumn.relationshipType in relationshipMap) {
|
||||||
|
@ -290,14 +289,14 @@
|
||||||
|
|
||||||
delete saveColumn.fieldId
|
delete saveColumn.fieldId
|
||||||
|
|
||||||
if (saveColumn.type === AUTO_TYPE) {
|
if (saveColumn.type === FieldType.AUTO) {
|
||||||
saveColumn = buildAutoColumn(
|
saveColumn = buildAutoColumn(
|
||||||
$tables.selected.name,
|
$tables.selected.name,
|
||||||
saveColumn.name,
|
saveColumn.name,
|
||||||
saveColumn.subtype
|
saveColumn.subtype
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (saveColumn.type !== LINK_TYPE) {
|
if (saveColumn.type !== FieldType.LINK) {
|
||||||
delete saveColumn.fieldName
|
delete saveColumn.fieldName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,9 +383,9 @@
|
||||||
editableColumn.subtype = definition.subtype
|
editableColumn.subtype = definition.subtype
|
||||||
|
|
||||||
// Default relationships many to many
|
// Default relationships many to many
|
||||||
if (editableColumn.type === LINK_TYPE) {
|
if (editableColumn.type === FieldType.LINK) {
|
||||||
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
editableColumn.relationshipType = RelationshipType.MANY_TO_MANY
|
||||||
} else if (editableColumn.type === FORMULA_TYPE) {
|
} else if (editableColumn.type === FieldType.FORMULA) {
|
||||||
editableColumn.formulaType = "dynamic"
|
editableColumn.formulaType = "dynamic"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,6 +451,7 @@
|
||||||
FIELDS.BOOLEAN,
|
FIELDS.BOOLEAN,
|
||||||
FIELDS.DATETIME,
|
FIELDS.DATETIME,
|
||||||
FIELDS.LINK,
|
FIELDS.LINK,
|
||||||
|
...(aiEnabled ? [FIELDS.AI] : []),
|
||||||
FIELDS.LONGFORM,
|
FIELDS.LONGFORM,
|
||||||
FIELDS.USER,
|
FIELDS.USER,
|
||||||
FIELDS.USERS,
|
FIELDS.USERS,
|
||||||
|
@ -505,17 +505,23 @@
|
||||||
fieldToCheck.constraints = {}
|
fieldToCheck.constraints = {}
|
||||||
}
|
}
|
||||||
// some string types may have been built by server, may not always have constraints
|
// some string types may have been built by server, may not always have constraints
|
||||||
if (fieldToCheck.type === STRING_TYPE && !fieldToCheck.constraints.length) {
|
if (
|
||||||
|
fieldToCheck.type === FieldType.STRING &&
|
||||||
|
!fieldToCheck.constraints.length
|
||||||
|
) {
|
||||||
fieldToCheck.constraints.length = {}
|
fieldToCheck.constraints.length = {}
|
||||||
}
|
}
|
||||||
// some number types made server-side will be missing constraints
|
// some number types made server-side will be missing constraints
|
||||||
if (
|
if (
|
||||||
fieldToCheck.type === NUMBER_TYPE &&
|
fieldToCheck.type === FieldType.NUMBER &&
|
||||||
!fieldToCheck.constraints.numericality
|
!fieldToCheck.constraints.numericality
|
||||||
) {
|
) {
|
||||||
fieldToCheck.constraints.numericality = {}
|
fieldToCheck.constraints.numericality = {}
|
||||||
}
|
}
|
||||||
if (fieldToCheck.type === DATE_TYPE && !fieldToCheck.constraints.datetime) {
|
if (
|
||||||
|
fieldToCheck.type === FieldType.DATETIME &&
|
||||||
|
!fieldToCheck.constraints.datetime
|
||||||
|
) {
|
||||||
fieldToCheck.constraints.datetime = {}
|
fieldToCheck.constraints.datetime = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -590,13 +596,13 @@
|
||||||
on:input={e => {
|
on:input={e => {
|
||||||
if (
|
if (
|
||||||
!uneditable &&
|
!uneditable &&
|
||||||
!(linkEditDisabled && editableColumn.type === LINK_TYPE)
|
!(linkEditDisabled && editableColumn.type === FieldType.LINK)
|
||||||
) {
|
) {
|
||||||
editableColumn.name = e.target.value
|
editableColumn.name = e.target.value
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={uneditable ||
|
disabled={uneditable ||
|
||||||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
|
(linkEditDisabled && editableColumn.type === FieldType.LINK)}
|
||||||
error={errors?.name}
|
error={errors?.name}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -610,7 +616,7 @@
|
||||||
getOptionValue={field => field.fieldId}
|
getOptionValue={field => field.fieldId}
|
||||||
getOptionIcon={field => field.icon}
|
getOptionIcon={field => field.icon}
|
||||||
isOptionEnabled={option => {
|
isOptionEnabled={option => {
|
||||||
if (option.type === AUTO_TYPE) {
|
if (option.type === FieldType.AUTO) {
|
||||||
return availableAutoColumnKeys?.length > 0
|
return availableAutoColumnKeys?.length > 0
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -653,7 +659,7 @@
|
||||||
bind:optionColors={editableColumn.optionColors}
|
bind:optionColors={editableColumn.optionColors}
|
||||||
bind:valid={optionsValid}
|
bind:valid={optionsValid}
|
||||||
/>
|
/>
|
||||||
{:else if editableColumn.type === DATE_TYPE && !editableColumn.autocolumn}
|
{:else if editableColumn.type === FieldType.DATETIME && !editableColumn.autocolumn}
|
||||||
<div class="split-label">
|
<div class="split-label">
|
||||||
<div class="label-length">
|
<div class="label-length">
|
||||||
<Label size="M">Earliest</Label>
|
<Label size="M">Earliest</Label>
|
||||||
|
@ -740,7 +746,7 @@
|
||||||
{tableOptions}
|
{tableOptions}
|
||||||
{errors}
|
{errors}
|
||||||
/>
|
/>
|
||||||
{:else if editableColumn.type === FORMULA_TYPE}
|
{:else if editableColumn.type === FieldType.FORMULA}
|
||||||
{#if !externalTable}
|
{#if !externalTable}
|
||||||
<div class="split-label">
|
<div class="split-label">
|
||||||
<div class="label-length">
|
<div class="label-length">
|
||||||
|
@ -783,12 +789,19 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if editableColumn.type === JSON_TYPE}
|
{:else if editableColumn.type === FieldType.AI}
|
||||||
|
<AIFieldConfiguration
|
||||||
|
aiField={editableColumn}
|
||||||
|
context={rowGoldenSample}
|
||||||
|
bindings={getBindings({ table })}
|
||||||
|
schema={table.schema}
|
||||||
|
/>
|
||||||
|
{:else if editableColumn.type === FieldType.JSON}
|
||||||
<Button primary text on:click={openJsonSchemaEditor}>
|
<Button primary text on:click={openJsonSchemaEditor}>
|
||||||
Open schema editor
|
Open schema editor
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if editableColumn.type === AUTO_TYPE || editableColumn.autocolumn}
|
{#if editableColumn.type === FieldType.AUTO || editableColumn.autocolumn}
|
||||||
<Select
|
<Select
|
||||||
label="Auto column type"
|
label="Auto column type"
|
||||||
value={editableColumn.subtype}
|
value={editableColumn.subtype}
|
||||||
|
@ -835,6 +848,18 @@
|
||||||
(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}
|
||||||
|
{@const defaultValue =
|
||||||
|
editableColumn.type === FieldType.BB_REFERENCE_SINGLE
|
||||||
|
? SingleUserDefault
|
||||||
|
: MultiUserDefault}
|
||||||
|
<Toggle
|
||||||
|
disabled={!canHaveDefault}
|
||||||
|
text="Default to current user"
|
||||||
|
value={editableColumn.default === defaultValue}
|
||||||
|
on:change={e =>
|
||||||
|
(editableColumn.default = e.detail ? defaultValue : undefined)}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
disabled={!canHaveDefault}
|
disabled={!canHaveDefault}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
|
|
||||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
|
const AI_TYPE = FIELDS.AI.type
|
||||||
|
|
||||||
export let row = {}
|
export let row = {}
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each tableSchema as [key, meta]}
|
{#each tableSchema as [key, meta]}
|
||||||
{#if !meta.autocolumn && meta.type !== FORMULA_TYPE}
|
{#if !meta.autocolumn && meta.type !== FORMULA_TYPE && meta.type !== AI_TYPE}
|
||||||
<div>
|
<div>
|
||||||
<RowFieldControl error={errors[key]} {meta} bind:value={row[key]} />
|
<RowFieldControl error={errors[key]} {meta} bind:value={row[key]} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script>
|
||||||
|
import { Helpers, Multiselect, Select } from "@budibase/bbui"
|
||||||
|
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||||
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
|
import {
|
||||||
|
AIOperations,
|
||||||
|
OperationFields,
|
||||||
|
OperationFieldTypes,
|
||||||
|
} from "@budibase/shared-core"
|
||||||
|
|
||||||
|
const AIFieldConfigOptions = Object.keys(AIOperations).map(key => ({
|
||||||
|
label: AIOperations[key].label,
|
||||||
|
value: AIOperations[key].value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
export let bindings
|
||||||
|
export let context
|
||||||
|
export let schema
|
||||||
|
export let aiField = {}
|
||||||
|
|
||||||
|
$: OperationField = OperationFields[aiField.operation]
|
||||||
|
$: schemaWithoutRelations = Object.keys(schema).filter(
|
||||||
|
key => schema[key].type !== "link"
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={"Operation"}
|
||||||
|
options={AIFieldConfigOptions}
|
||||||
|
bind:value={aiField.operation}
|
||||||
|
/>
|
||||||
|
{#if aiField.operation}
|
||||||
|
{#each Object.keys(OperationField) as key}
|
||||||
|
{#if OperationField[key] === OperationFieldTypes.BINDABLE_TEXT}
|
||||||
|
<ModalBindableInput
|
||||||
|
label={Helpers.capitalise(key)}
|
||||||
|
panel={ServerBindingPanel}
|
||||||
|
title="Prompt"
|
||||||
|
on:change={e => (aiField[key] = e.detail)}
|
||||||
|
value={aiField[key]}
|
||||||
|
{bindings}
|
||||||
|
allowJS
|
||||||
|
{context}
|
||||||
|
/>
|
||||||
|
{:else if OperationField[key] === OperationFieldTypes.MULTI_COLUMN}
|
||||||
|
<Multiselect
|
||||||
|
bind:value={aiField[key]}
|
||||||
|
label={Helpers.capitalise(key)}
|
||||||
|
options={schemaWithoutRelations}
|
||||||
|
/>
|
||||||
|
{:else if OperationField[key] === OperationFieldTypes.COLUMN}
|
||||||
|
<Select
|
||||||
|
bind:value={aiField[key]}
|
||||||
|
label={Helpers.capitalise(key)}
|
||||||
|
options={schemaWithoutRelations}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
const getSchemaFields = resourceId => {
|
const getSchemaFields = resourceId => {
|
||||||
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
||||||
return Object.values(schema || {})
|
return Object.values(schema || {}).filter(field => !field.readonly)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFieldsChanged = e => {
|
const onFieldsChanged = e => {
|
||||||
|
|
|
@ -14,7 +14,13 @@
|
||||||
function daysUntilCancel() {
|
function daysUntilCancel() {
|
||||||
const cancelAt = license?.billing?.subscription?.cancelAt
|
const cancelAt = license?.billing?.subscription?.cancelAt
|
||||||
const diffTime = Math.abs(cancelAt - new Date().getTime()) / 1000
|
const diffTime = Math.abs(cancelAt - new Date().getTime()) / 1000
|
||||||
return Math.floor(diffTime / oneDayInSeconds)
|
const days = Math.floor(diffTime / oneDayInSeconds)
|
||||||
|
if (days === 1) {
|
||||||
|
return "tomorrow."
|
||||||
|
} else if (days === 0) {
|
||||||
|
return "today."
|
||||||
|
}
|
||||||
|
return `in ${days} days.`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -28,7 +34,7 @@
|
||||||
extraLinkAction={$licensing.goToUpgradePage}
|
extraLinkAction={$licensing.goToUpgradePage}
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
>
|
>
|
||||||
Your free trial will end in {daysUntilCancel()} days.
|
Your free trial will end {daysUntilCancel()}
|
||||||
</Banner>
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -159,6 +159,12 @@ export const FIELDS = {
|
||||||
icon: TypeIconMap[FieldType.FORMULA],
|
icon: TypeIconMap[FieldType.FORMULA],
|
||||||
constraints: {},
|
constraints: {},
|
||||||
},
|
},
|
||||||
|
AI: {
|
||||||
|
name: "AI",
|
||||||
|
type: FieldType.AI,
|
||||||
|
icon: TypeIconMap[FieldType.AI],
|
||||||
|
constraints: {},
|
||||||
|
},
|
||||||
JSON: {
|
JSON: {
|
||||||
name: "JSON",
|
name: "JSON",
|
||||||
type: FieldType.JSON,
|
type: FieldType.JSON,
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
sourceType: DB_TYPE_EXTERNAL,
|
sourceType: DB_TYPE_EXTERNAL,
|
||||||
schema: {
|
schema: {
|
||||||
id: {
|
id: {
|
||||||
|
name: "id",
|
||||||
autocolumn: true,
|
autocolumn: true,
|
||||||
type: "number",
|
type: "number",
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { viewsV2, rowActions } from "stores/builder"
|
import { viewsV2, rowActions } from "stores/builder"
|
||||||
import { admin, themeStore } from "stores/portal"
|
import { admin, themeStore, licensing } from "stores/portal"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
@ -49,6 +49,7 @@
|
||||||
{buttons}
|
{buttons}
|
||||||
allowAddRows
|
allowAddRows
|
||||||
allowDeleteRows
|
allowDeleteRows
|
||||||
|
aiEnabled={$licensing.budibaseAIEnabled || $licensing.customAIConfigsEnabled}
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
on:updatedatasource={handleGridViewUpdate}
|
on:updatedatasource={handleGridViewUpdate}
|
||||||
isCloud={$admin.cloud}
|
isCloud={$admin.cloud}
|
||||||
|
|
|
@ -196,7 +196,7 @@
|
||||||
on:contextmenu={openTableContextMenu}
|
on:contextmenu={openTableContextMenu}
|
||||||
>
|
>
|
||||||
<div class="nav-item__title">
|
<div class="nav-item__title">
|
||||||
{table._id === TableNames.USERS ? "App users" : table.name}
|
{table?._id === TableNames.USERS ? "App users" : table?.name || ""}
|
||||||
</div>
|
</div>
|
||||||
{#if tableSelectedBy}
|
{#if tableSelectedBy}
|
||||||
<UserAvatars size="XS" users={tableSelectedBy} />
|
<UserAvatars size="XS" users={tableSelectedBy} />
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
rowActions,
|
rowActions,
|
||||||
roles,
|
roles,
|
||||||
} from "stores/builder"
|
} from "stores/builder"
|
||||||
import { themeStore, admin } from "stores/portal"
|
import { themeStore, admin, licensing } from "stores/portal"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -130,6 +130,8 @@
|
||||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||||
showAvatars={false}
|
showAvatars={false}
|
||||||
isCloud={$admin.cloud}
|
isCloud={$admin.cloud}
|
||||||
|
aiEnabled={$licensing.budibaseAIEnabled ||
|
||||||
|
$licensing.customAIConfigsEnabled}
|
||||||
{buttons}
|
{buttons}
|
||||||
buttonsCollapsed
|
buttonsCollapsed
|
||||||
on:updatedatasource={handleGridTableUpdate}
|
on:updatedatasource={handleGridTableUpdate}
|
||||||
|
|
|
@ -6,16 +6,22 @@ import { dataFilters } from "@budibase/shared-core"
|
||||||
function convertToSearchFilters(view) {
|
function convertToSearchFilters(view) {
|
||||||
// convert from SearchFilterGroup type
|
// convert from SearchFilterGroup type
|
||||||
if (view?.query) {
|
if (view?.query) {
|
||||||
view.queryUI = view.query
|
return {
|
||||||
view.query = dataFilters.buildQuery(view.query)
|
...view,
|
||||||
|
queryUI: view.query,
|
||||||
|
query: dataFilters.buildQuery(view.query),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertToSearchFilterGroup(view) {
|
function convertToSearchFilterGroup(view) {
|
||||||
if (view?.queryUI) {
|
if (view?.queryUI) {
|
||||||
view.query = view.queryUI
|
return {
|
||||||
delete view.queryUI
|
...view,
|
||||||
|
query: view.queryUI,
|
||||||
|
queryUI: undefined,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
// because it functions similarly to one
|
// because it functions similarly to one
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { get, derived, readable } from "svelte/store"
|
import { get, derived, readable } from "svelte/store"
|
||||||
|
import { featuresStore } from "stores"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import { Grid } from "@budibase/frontend-core"
|
||||||
|
|
||||||
// table is actually any datasource, but called table for legacy compatibility
|
// table is actually any datasource, but called table for legacy compatibility
|
||||||
|
@ -186,6 +187,7 @@
|
||||||
{buttonsCollapsed}
|
{buttonsCollapsed}
|
||||||
{buttonsCollapsedText}
|
{buttonsCollapsedText}
|
||||||
isCloud={$environmentStore.cloud}
|
isCloud={$environmentStore.cloud}
|
||||||
|
aiEnabled={$featuresStore.aiEnabled}
|
||||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
import { GridRowHeight, GridColumns } from "constants"
|
import { GridRowHeight, GridColumns } from "constants"
|
||||||
import { memo } from "@budibase/frontend-core"
|
import { memo } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
export let onClick
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, builderStore } = getContext("sdk")
|
const { styleable, builderStore } = getContext("sdk")
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
@ -121,15 +123,19 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
class="grid"
|
class="grid"
|
||||||
class:mobile
|
class:mobile
|
||||||
|
class:clickable={!!onClick}
|
||||||
bind:clientWidth={width}
|
bind:clientWidth={width}
|
||||||
bind:clientHeight={height}
|
bind:clientHeight={height}
|
||||||
use:styleable={$styles}
|
use:styleable={$styles}
|
||||||
data-cols={GridColumns}
|
data-cols={GridColumns}
|
||||||
data-col-size={colSize}
|
data-col-size={colSize}
|
||||||
|
on:click={onClick}
|
||||||
>
|
>
|
||||||
{#if inBuilder}
|
{#if inBuilder}
|
||||||
<div class="underlay">
|
<div class="underlay">
|
||||||
|
@ -176,6 +182,9 @@
|
||||||
.placeholder.first-col {
|
.placeholder.first-col {
|
||||||
border-left: 1px solid var(--spectrum-global-color-gray-900);
|
border-left: 1px solid var(--spectrum-global-color-gray-900);
|
||||||
}
|
}
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Highlight grid lines when resizing children */
|
/* Highlight grid lines when resizing children */
|
||||||
:global(.grid.highlight > .underlay) {
|
:global(.grid.highlight > .underlay) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { derived } from "svelte/store"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
import { authStore } from "./auth"
|
import { authStore } from "./auth"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
import { Feature } from "@budibase/types"
|
||||||
|
|
||||||
const createFeaturesStore = () => {
|
const createFeaturesStore = () => {
|
||||||
return derived([authStore, appStore], ([$authStore, $appStore]) => {
|
return derived([authStore, appStore], ([$authStore, $appStore]) => {
|
||||||
|
@ -33,8 +34,13 @@ const createFeaturesStore = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const license = getUserLicense()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logoEnabled: isFreePlan(),
|
logoEnabled: isFreePlan(),
|
||||||
|
aiEnabled:
|
||||||
|
license?.features?.includes(Feature.AI_CUSTOM_CONFIGS) ||
|
||||||
|
license?.features?.includes(Feature.BUDIBASE_AI),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,7 +174,7 @@
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
{#if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA].includes(filter.type)}
|
{#if [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA, FieldType.AI].includes(filter.type)}
|
||||||
<Input
|
<Input
|
||||||
disabled={filter.noValue}
|
disabled={filter.noValue}
|
||||||
value={readableValue}
|
value={readableValue}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { clickOutside } from "@budibase/bbui"
|
||||||
|
import GridPopover from "../overlays/GridPopover.svelte"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let focused = false
|
||||||
|
export let api
|
||||||
|
|
||||||
|
let textarea
|
||||||
|
let isOpen = false
|
||||||
|
let anchor
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (!focused) {
|
||||||
|
isOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = () => {
|
||||||
|
return isOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = async () => {
|
||||||
|
isOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
textarea?.blur()
|
||||||
|
isOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
api = {
|
||||||
|
focus: () => open(),
|
||||||
|
blur: () => close(),
|
||||||
|
isActive: () => isOpen,
|
||||||
|
onKeyDown,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div class="long-form-cell" on:click={open} bind:this={anchor}>
|
||||||
|
<div class="value">
|
||||||
|
{value || ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<GridPopover {anchor} on:close={close}>
|
||||||
|
<textarea
|
||||||
|
disabled
|
||||||
|
bind:this={textarea}
|
||||||
|
value={value || ""}
|
||||||
|
on:wheel|stopPropagation
|
||||||
|
spellcheck="false"
|
||||||
|
use:clickOutside={close}
|
||||||
|
/>
|
||||||
|
</GridPopover>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.long-form-cell {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
padding: var(--cell-padding);
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: var(--content-lines);
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
border: none;
|
||||||
|
width: 320px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: var(--max-cell-render-overflow);
|
||||||
|
padding: var(--cell-padding);
|
||||||
|
margin: 0;
|
||||||
|
background: var(--cell-background);
|
||||||
|
font-size: var(--cell-font-size);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
color: inherit;
|
||||||
|
z-index: 1;
|
||||||
|
resize: none;
|
||||||
|
line-height: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -95,7 +95,8 @@
|
||||||
const { type, formulaType } = col.schema
|
const { type, formulaType } = col.schema
|
||||||
return (
|
return (
|
||||||
searchableTypes.includes(type) ||
|
searchableTypes.includes(type) ||
|
||||||
(type === FieldType.FORMULA && formulaType === FormulaType.STATIC)
|
(type === FieldType.FORMULA && formulaType === FormulaType.STATIC) ||
|
||||||
|
type === FieldType.AI
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,7 @@
|
||||||
let candidateIndex
|
let candidateIndex
|
||||||
let lastSearchId
|
let lastSearchId
|
||||||
let searching = false
|
let searching = false
|
||||||
let container
|
|
||||||
let anchor
|
let anchor
|
||||||
let relationshipAnchor
|
|
||||||
let relationshipFields
|
|
||||||
|
|
||||||
$: fieldValue = parseValue(value)
|
$: fieldValue = parseValue(value)
|
||||||
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
||||||
|
@ -57,13 +54,6 @@
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
$: showRelationshipFields =
|
|
||||||
relationshipAnchor &&
|
|
||||||
relationshipFields &&
|
|
||||||
Object.keys(relationshipFields).length &&
|
|
||||||
focused &&
|
|
||||||
!isOpen
|
|
||||||
|
|
||||||
const parseValue = value => {
|
const parseValue = value => {
|
||||||
if (Array.isArray(value) && value.every(x => x?._id)) {
|
if (Array.isArray(value) && value.every(x => x?._id)) {
|
||||||
return value
|
return value
|
||||||
|
@ -208,7 +198,6 @@
|
||||||
|
|
||||||
// Toggles whether a row is included in the relationship or not
|
// Toggles whether a row is included in the relationship or not
|
||||||
const toggleRow = async row => {
|
const toggleRow = async row => {
|
||||||
hideRelationshipFields()
|
|
||||||
if (fieldValue?.some(x => x._id === row._id)) {
|
if (fieldValue?.some(x => x._id === row._id)) {
|
||||||
// If the row is already included, remove it and update the candidate
|
// If the row is already included, remove it and update the candidate
|
||||||
// row to be the same position if possible
|
// row to be the same position if possible
|
||||||
|
@ -245,16 +234,6 @@
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayRelationshipFields = (e, relationship) => {
|
|
||||||
relationshipAnchor = e.target
|
|
||||||
relationshipFields = relationFields[relationship._id]
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideRelationshipFields = () => {
|
|
||||||
relationshipAnchor = null
|
|
||||||
relationshipFields = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
api = {
|
api = {
|
||||||
focus: open,
|
focus: open,
|
||||||
|
@ -274,7 +253,7 @@
|
||||||
style="--color:{color};"
|
style="--color:{color};"
|
||||||
bind:this={anchor}
|
bind:this={anchor}
|
||||||
>
|
>
|
||||||
<div class="container" bind:this={container}>
|
<div class="container">
|
||||||
<div
|
<div
|
||||||
class="values"
|
class="values"
|
||||||
class:wrap={editable || contentLines > 1}
|
class:wrap={editable || contentLines > 1}
|
||||||
|
@ -286,9 +265,7 @@
|
||||||
<div
|
<div
|
||||||
class="badge"
|
class="badge"
|
||||||
class:extra-info={!!relationFields[relationship._id]}
|
class:extra-info={!!relationFields[relationship._id]}
|
||||||
on:mouseenter={e => displayRelationshipFields(e, relationship)}
|
|
||||||
on:focus={() => {}}
|
on:focus={() => {}}
|
||||||
on:mouseleave={() => hideRelationshipFields()}
|
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{readable(
|
{readable(
|
||||||
|
@ -363,26 +340,6 @@
|
||||||
</GridPopover>
|
</GridPopover>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showRelationshipFields}
|
|
||||||
<GridPopover
|
|
||||||
anchor={relationshipAnchor}
|
|
||||||
maxWidth={400}
|
|
||||||
offset={4}
|
|
||||||
clickOutsideOverride
|
|
||||||
>
|
|
||||||
<div class="relationship-fields">
|
|
||||||
{#each Object.entries(relationshipFields) as [fieldName, fieldValue]}
|
|
||||||
<div class="relationship-field-name">
|
|
||||||
{fieldName}
|
|
||||||
</div>
|
|
||||||
<div class="relationship-field-value">
|
|
||||||
{fieldValue}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</GridPopover>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
@ -547,29 +504,4 @@
|
||||||
.search :global(.spectrum-Form-item) {
|
.search :global(.spectrum-Form-item) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.relationship-fields {
|
|
||||||
margin: var(--spacing-m) var(--spacing-l);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
grid-row-gap: var(--spacing-m);
|
|
||||||
grid-column-gap: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.relationship-field-name {
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
font-size: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
.relationship-field-value {
|
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
line-clamp: 3;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
export let darkMode = false
|
export let darkMode = false
|
||||||
export let isCloud = null
|
export let isCloud = null
|
||||||
export let rowConditions = null
|
export let rowConditions = null
|
||||||
|
export let aiEnabled = false
|
||||||
|
|
||||||
// Unique identifier for DOM nodes inside this instance
|
// Unique identifier for DOM nodes inside this instance
|
||||||
const gridID = `grid-${Math.random().toString().slice(2)}`
|
const gridID = `grid-${Math.random().toString().slice(2)}`
|
||||||
|
@ -104,6 +105,7 @@
|
||||||
buttonsCollapsedText,
|
buttonsCollapsedText,
|
||||||
darkMode,
|
darkMode,
|
||||||
isCloud,
|
isCloud,
|
||||||
|
aiEnabled,
|
||||||
rowConditions,
|
rowConditions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import TextCell from "../cells/TextCell.svelte"
|
||||||
import LongFormCell from "../cells/LongFormCell.svelte"
|
import LongFormCell from "../cells/LongFormCell.svelte"
|
||||||
import BooleanCell from "../cells/BooleanCell.svelte"
|
import BooleanCell from "../cells/BooleanCell.svelte"
|
||||||
import FormulaCell from "../cells/FormulaCell.svelte"
|
import FormulaCell from "../cells/FormulaCell.svelte"
|
||||||
|
import AICell from "../cells/AICell.svelte"
|
||||||
import JSONCell from "../cells/JSONCell.svelte"
|
import JSONCell from "../cells/JSONCell.svelte"
|
||||||
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
||||||
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
||||||
|
@ -31,6 +32,7 @@ const TypeComponentMap = {
|
||||||
[FieldType.ATTACHMENT_SINGLE]: AttachmentSingleCell,
|
[FieldType.ATTACHMENT_SINGLE]: AttachmentSingleCell,
|
||||||
[FieldType.LINK]: RelationshipCell,
|
[FieldType.LINK]: RelationshipCell,
|
||||||
[FieldType.FORMULA]: FormulaCell,
|
[FieldType.FORMULA]: FormulaCell,
|
||||||
|
[FieldType.AI]: AICell,
|
||||||
[FieldType.JSON]: JSONCell,
|
[FieldType.JSON]: JSONCell,
|
||||||
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
||||||
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
|
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Menu, MenuItem, Helpers } from "@budibase/bbui"
|
import { Menu, MenuItem, Helpers } from "@budibase/bbui"
|
||||||
|
import { FieldType } from "@budibase/types"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { NewRowID } from "../lib/constants"
|
import { NewRowID } from "../lib/constants"
|
||||||
import GridPopover from "./GridPopover.svelte"
|
import GridPopover from "./GridPopover.svelte"
|
||||||
|
@ -26,6 +27,9 @@
|
||||||
|
|
||||||
$: style = makeStyle($menu)
|
$: style = makeStyle($menu)
|
||||||
$: isNewRow = $focusedRowId === NewRowID
|
$: isNewRow = $focusedRowId === NewRowID
|
||||||
|
$: hasAIColumns = $visibleColumns.some(
|
||||||
|
col => col.schema.type === FieldType.AI
|
||||||
|
)
|
||||||
|
|
||||||
const makeStyle = menu => {
|
const makeStyle = menu => {
|
||||||
return `left:${menu.left}px; top:${menu.top}px;`
|
return `left:${menu.left}px; top:${menu.top}px;`
|
||||||
|
@ -53,6 +57,12 @@
|
||||||
await Helpers.copyToClipboard(value)
|
await Helpers.copyToClipboard(value)
|
||||||
$notifications.success("Copied to clipboard")
|
$notifications.success("Copied to clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generateAIColumns = async () => {
|
||||||
|
menu.actions.close()
|
||||||
|
await rows.actions.applyRowChanges({ rowId: $focusedRowId })
|
||||||
|
$notifications.success("Generated AI columns")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor} {style} class="menu-anchor" />
|
<div bind:this={anchor} {style} class="menu-anchor" />
|
||||||
|
@ -161,6 +171,15 @@
|
||||||
>
|
>
|
||||||
Delete row
|
Delete row
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{#if $config.aiEnabled}
|
||||||
|
<MenuItem
|
||||||
|
icon="MagicWand"
|
||||||
|
disabled={isNewRow || !hasAIColumns}
|
||||||
|
on:click={generateAIColumns}
|
||||||
|
>
|
||||||
|
Generate AI Columns
|
||||||
|
</MenuItem>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Menu>
|
</Menu>
|
||||||
</GridPopover>
|
</GridPopover>
|
||||||
|
|
|
@ -109,6 +109,7 @@ export const createActions = context => {
|
||||||
column.schema.autocolumn ||
|
column.schema.autocolumn ||
|
||||||
column.schema.disabled ||
|
column.schema.disabled ||
|
||||||
column.schema.type === "formula" ||
|
column.schema.type === "formula" ||
|
||||||
|
column.schema.type === "ai" ||
|
||||||
column.schema.readonly
|
column.schema.readonly
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,12 @@ import { dataFilters } from "@budibase/shared-core"
|
||||||
|
|
||||||
function convertToSearchFilters(view) {
|
function convertToSearchFilters(view) {
|
||||||
// convert from SearchFilterGroup type
|
// convert from SearchFilterGroup type
|
||||||
if (view.query) {
|
if (view?.query) {
|
||||||
view.queryUI = view.query
|
return {
|
||||||
view.query = dataFilters.buildQuery(view.query)
|
...view,
|
||||||
|
queryUI: view.query,
|
||||||
|
query: dataFilters.buildQuery(view.query),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,9 +160,10 @@ export const TypeIconMap = {
|
||||||
[FieldType.ATTACHMENT_SINGLE]: "DocumentFragment",
|
[FieldType.ATTACHMENT_SINGLE]: "DocumentFragment",
|
||||||
[FieldType.LINK]: "DataCorrelated",
|
[FieldType.LINK]: "DataCorrelated",
|
||||||
[FieldType.FORMULA]: "Calculator",
|
[FieldType.FORMULA]: "Calculator",
|
||||||
|
[FieldType.AI]: "MagicWand",
|
||||||
[FieldType.JSON]: "Brackets",
|
[FieldType.JSON]: "Brackets",
|
||||||
[FieldType.BIGINT]: "TagBold",
|
[FieldType.BIGINT]: "TagBold",
|
||||||
[FieldType.AUTO]: "MagicWand",
|
[FieldType.AUTO]: "Shapes",
|
||||||
[FieldType.BB_REFERENCE]: {
|
[FieldType.BB_REFERENCE]: {
|
||||||
[BBReferenceFieldSubType.USER]: "UserGroup",
|
[BBReferenceFieldSubType.USER]: "UserGroup",
|
||||||
[BBReferenceFieldSubType.USERS]: "UserGroup",
|
[BBReferenceFieldSubType.USERS]: "UserGroup",
|
||||||
|
|
|
@ -24,9 +24,9 @@ const columnTypeManyParser = {
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
return value?.map(v => parseDate(v))
|
return value.map(v => parseDate(v))
|
||||||
},
|
},
|
||||||
[FieldType.BOOLEAN]: value => value?.map(v => !!v),
|
[FieldType.BOOLEAN]: value => value.map(v => !!v),
|
||||||
[FieldType.BB_REFERENCE_SINGLE]: value => [
|
[FieldType.BB_REFERENCE_SINGLE]: value => [
|
||||||
...new Map(value.map(i => [i._id, i])).values(),
|
...new Map(value.map(i => [i._id, i])).values(),
|
||||||
],
|
],
|
||||||
|
@ -80,14 +80,10 @@ export function getRelatedTableValues(row, field, fromField) {
|
||||||
result = row[field.related.field]?.[0]?.[field.related.subField]
|
result = row[field.related.field]?.[0]?.[field.related.subField]
|
||||||
} else {
|
} else {
|
||||||
const parser = columnTypeManyParser[field.type] || (value => value)
|
const parser = columnTypeManyParser[field.type] || (value => value)
|
||||||
|
const value = row[field.related.field]
|
||||||
result = parser(
|
?.flatMap(r => r[field.related.subField])
|
||||||
row[field.related.field]
|
?.filter(i => i !== undefined && i !== null)
|
||||||
?.flatMap(r => r[field.related.subField])
|
result = parser(value || [], field)
|
||||||
?.filter(i => i !== undefined && i !== null),
|
|
||||||
field
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
FieldType.STRING,
|
FieldType.STRING,
|
||||||
|
|
|
@ -2,6 +2,10 @@ import { helpers } from "@budibase/shared-core"
|
||||||
import { TypeIconMap } from "../constants"
|
import { TypeIconMap } from "../constants"
|
||||||
|
|
||||||
export const getColumnIcon = column => {
|
export const getColumnIcon = column => {
|
||||||
|
if (column.schema.icon) {
|
||||||
|
return column.schema.icon
|
||||||
|
}
|
||||||
|
|
||||||
if (column.schema.autocolumn) {
|
if (column.schema.autocolumn) {
|
||||||
return "MagicWand"
|
return "MagicWand"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit aca9828117bb97f54f40ee359f1a3f6e259174e7
|
Subproject commit 61391549614b5ac153f267633d0aaea9b07f05c5
|
|
@ -833,7 +833,8 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"static",
|
"static",
|
||||||
"dynamic"
|
"dynamic",
|
||||||
|
"ai"
|
||||||
],
|
],
|
||||||
"description": "Defines whether this is a static or dynamic formula."
|
"description": "Defines whether this is a static or dynamic formula."
|
||||||
}
|
}
|
||||||
|
@ -857,6 +858,7 @@
|
||||||
"link",
|
"link",
|
||||||
"formula",
|
"formula",
|
||||||
"auto",
|
"auto",
|
||||||
|
"ai",
|
||||||
"json",
|
"json",
|
||||||
"internal",
|
"internal",
|
||||||
"barcodeqr",
|
"barcodeqr",
|
||||||
|
@ -1042,7 +1044,8 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"static",
|
"static",
|
||||||
"dynamic"
|
"dynamic",
|
||||||
|
"ai"
|
||||||
],
|
],
|
||||||
"description": "Defines whether this is a static or dynamic formula."
|
"description": "Defines whether this is a static or dynamic formula."
|
||||||
}
|
}
|
||||||
|
@ -1066,6 +1069,7 @@
|
||||||
"link",
|
"link",
|
||||||
"formula",
|
"formula",
|
||||||
"auto",
|
"auto",
|
||||||
|
"ai",
|
||||||
"json",
|
"json",
|
||||||
"internal",
|
"internal",
|
||||||
"barcodeqr",
|
"barcodeqr",
|
||||||
|
@ -1262,7 +1266,8 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"static",
|
"static",
|
||||||
"dynamic"
|
"dynamic",
|
||||||
|
"ai"
|
||||||
],
|
],
|
||||||
"description": "Defines whether this is a static or dynamic formula."
|
"description": "Defines whether this is a static or dynamic formula."
|
||||||
}
|
}
|
||||||
|
@ -1286,6 +1291,7 @@
|
||||||
"link",
|
"link",
|
||||||
"formula",
|
"formula",
|
||||||
"auto",
|
"auto",
|
||||||
|
"ai",
|
||||||
"json",
|
"json",
|
||||||
"internal",
|
"internal",
|
||||||
"barcodeqr",
|
"barcodeqr",
|
||||||
|
|
|
@ -761,6 +761,7 @@ components:
|
||||||
enum:
|
enum:
|
||||||
- static
|
- static
|
||||||
- dynamic
|
- dynamic
|
||||||
|
- ai
|
||||||
description: Defines whether this is a static or dynamic formula.
|
description: Defines whether this is a static or dynamic formula.
|
||||||
- type: object
|
- type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -779,6 +780,7 @@ components:
|
||||||
- link
|
- link
|
||||||
- formula
|
- formula
|
||||||
- auto
|
- auto
|
||||||
|
- ai
|
||||||
- json
|
- json
|
||||||
- internal
|
- internal
|
||||||
- barcodeqr
|
- barcodeqr
|
||||||
|
@ -929,6 +931,7 @@ components:
|
||||||
enum:
|
enum:
|
||||||
- static
|
- static
|
||||||
- dynamic
|
- dynamic
|
||||||
|
- ai
|
||||||
description: Defines whether this is a static or dynamic formula.
|
description: Defines whether this is a static or dynamic formula.
|
||||||
- type: object
|
- type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -947,6 +950,7 @@ components:
|
||||||
- link
|
- link
|
||||||
- formula
|
- formula
|
||||||
- auto
|
- auto
|
||||||
|
- ai
|
||||||
- json
|
- json
|
||||||
- internal
|
- internal
|
||||||
- barcodeqr
|
- barcodeqr
|
||||||
|
@ -1104,6 +1108,7 @@ components:
|
||||||
enum:
|
enum:
|
||||||
- static
|
- static
|
||||||
- dynamic
|
- dynamic
|
||||||
|
- ai
|
||||||
description: Defines whether this is a static or dynamic formula.
|
description: Defines whether this is a static or dynamic formula.
|
||||||
- type: object
|
- type: object
|
||||||
properties:
|
properties:
|
||||||
|
@ -1122,6 +1127,7 @@ components:
|
||||||
- link
|
- link
|
||||||
- formula
|
- formula
|
||||||
- auto
|
- auto
|
||||||
|
- ai
|
||||||
- json
|
- json
|
||||||
- internal
|
- internal
|
||||||
- barcodeqr
|
- barcodeqr
|
||||||
|
|
|
@ -157,7 +157,8 @@ function isEditableColumn(column: FieldSchema) {
|
||||||
column.autoReason !== AutoReason.FOREIGN_KEY &&
|
column.autoReason !== AutoReason.FOREIGN_KEY &&
|
||||||
column.subtype !== AutoFieldSubType.AUTO_ID
|
column.subtype !== AutoFieldSubType.AUTO_ID
|
||||||
const isFormula = column.type === FieldType.FORMULA
|
const isFormula = column.type === FieldType.FORMULA
|
||||||
return !(isExternalAutoColumn || isFormula)
|
const isAIColumn = column.type === FieldType.AI
|
||||||
|
return !(isExternalAutoColumn || isFormula || isAIColumn)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExternalRequest<T extends Operation> {
|
export class ExternalRequest<T extends Operation> {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
import { cloneDeep } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
import { generateIdForRow } from "./utils"
|
import { generateIdForRow } from "./utils"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers } from "@budibase/shared-core"
|
||||||
|
import { HTTPError } from "@budibase/backend-core"
|
||||||
|
|
||||||
export async function handleRequest<T extends Operation>(
|
export async function handleRequest<T extends Operation>(
|
||||||
operation: T,
|
operation: T,
|
||||||
|
@ -102,6 +103,11 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
|
|
||||||
export async function destroy(ctx: UserCtx) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
const source = await utils.getSource(ctx)
|
const source = await utils.getSource(ctx)
|
||||||
|
|
||||||
|
if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {
|
||||||
|
throw new HTTPError("Cannot delete rows through a calculation view", 400)
|
||||||
|
}
|
||||||
|
|
||||||
const _id = ctx.request.body._id
|
const _id = ctx.request.body._id
|
||||||
const { row } = await handleRequest(Operation.DELETE, source, {
|
const { row } = await handleRequest(Operation.DELETE, source, {
|
||||||
id: breakRowIdField(_id),
|
id: breakRowIdField(_id),
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context, HTTPError } from "@budibase/backend-core"
|
||||||
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
|
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
|
||||||
import {
|
import {
|
||||||
FieldType,
|
FieldType,
|
||||||
|
@ -16,6 +16,7 @@ import {
|
||||||
PatchRowRequest,
|
PatchRowRequest,
|
||||||
PatchRowResponse,
|
PatchRowResponse,
|
||||||
Row,
|
Row,
|
||||||
|
Table,
|
||||||
UserCtx,
|
UserCtx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
@ -97,15 +98,26 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
|
|
||||||
export async function destroy(ctx: UserCtx) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const { tableId } = utils.getSourceId(ctx)
|
const source = await utils.getSource(ctx)
|
||||||
|
|
||||||
|
if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {
|
||||||
|
throw new HTTPError("Cannot delete rows through a calculation view", 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
let table: Table
|
||||||
|
if (sdk.views.isView(source)) {
|
||||||
|
table = await sdk.views.getTable(source.id)
|
||||||
|
} else {
|
||||||
|
table = source
|
||||||
|
}
|
||||||
|
|
||||||
const { _id } = ctx.request.body
|
const { _id } = ctx.request.body
|
||||||
let row = await db.get<Row>(_id)
|
let row = await db.get<Row>(_id)
|
||||||
let _rev = ctx.request.body._rev || row._rev
|
let _rev = ctx.request.body._rev || row._rev
|
||||||
|
|
||||||
if (row.tableId !== tableId) {
|
if (row.tableId !== table._id) {
|
||||||
throw "Supplied tableId doesn't match the row's tableId"
|
throw "Supplied tableId doesn't match the row's tableId"
|
||||||
}
|
}
|
||||||
const table = await sdk.tables.getTable(tableId)
|
|
||||||
// update the row to include full relationships before deleting them
|
// update the row to include full relationships before deleting them
|
||||||
row = await outputProcessing(table, row, {
|
row = await outputProcessing(table, row, {
|
||||||
squash: false,
|
squash: false,
|
||||||
|
@ -115,7 +127,7 @@ export async function destroy(ctx: UserCtx) {
|
||||||
await linkRows.updateLinks({
|
await linkRows.updateLinks({
|
||||||
eventType: linkRows.EventType.ROW_DELETE,
|
eventType: linkRows.EventType.ROW_DELETE,
|
||||||
row,
|
row,
|
||||||
tableId,
|
tableId: table._id!,
|
||||||
})
|
})
|
||||||
// remove any attachments that were on the row from object storage
|
// remove any attachments that were on the row from object storage
|
||||||
await AttachmentCleanup.rowDelete(table, [row])
|
await AttachmentCleanup.rowDelete(table, [row])
|
||||||
|
@ -123,7 +135,7 @@ export async function destroy(ctx: UserCtx) {
|
||||||
await updateRelatedFormula(table, row)
|
await updateRelatedFormula(table, row)
|
||||||
|
|
||||||
let response
|
let response
|
||||||
if (tableId === InternalTables.USER_METADATA) {
|
if (table._id === InternalTables.USER_METADATA) {
|
||||||
ctx.params = {
|
ctx.params = {
|
||||||
id: _id,
|
id: _id,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { getRowParams } from "../../../db/utils"
|
import { getRowParams } from "../../../db/utils"
|
||||||
import {
|
import {
|
||||||
outputProcessing,
|
outputProcessing,
|
||||||
|
processAIColumns,
|
||||||
processFormulas,
|
processFormulas,
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
|
@ -9,6 +10,7 @@ import * as linkRows from "../../../db/linkedRows"
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
import * as pro from "@budibase/pro"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function runs through a list of enriched rows, looks at the rows which
|
* This function runs through a list of enriched rows, looks at the rows which
|
||||||
|
@ -102,7 +104,7 @@ export async function updateAllFormulasInTable(table: Table) {
|
||||||
(enriched: Row) => enriched._id === row._id
|
(enriched: Row) => enriched._id === row._id
|
||||||
)
|
)
|
||||||
if (enrichedRow) {
|
if (enrichedRow) {
|
||||||
const processed = await processFormulas(table, cloneDeep(row), {
|
let processed = await processFormulas(table, cloneDeep(row), {
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
contextRows: [enrichedRow],
|
contextRows: [enrichedRow],
|
||||||
})
|
})
|
||||||
|
@ -142,12 +144,27 @@ export async function finaliseRow(
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
contextRows: [enrichedRow],
|
contextRows: [enrichedRow],
|
||||||
})
|
})
|
||||||
|
const aiEnabled =
|
||||||
|
(await pro.features.isBudibaseAIEnabled()) ||
|
||||||
|
(await pro.features.isAICustomConfigsEnabled())
|
||||||
|
if (aiEnabled) {
|
||||||
|
row = await processAIColumns(table, row, {
|
||||||
|
contextRows: [enrichedRow],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const response = await db.put(row)
|
const response = await db.put(row)
|
||||||
// for response, calculate the formulas for the enriched row
|
// for response, calculate the formulas for the enriched row
|
||||||
enrichedRow._rev = response.rev
|
enrichedRow._rev = response.rev
|
||||||
enrichedRow = await processFormulas(table, enrichedRow, {
|
enrichedRow = await processFormulas(table, enrichedRow, {
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
})
|
})
|
||||||
|
if (aiEnabled) {
|
||||||
|
enrichedRow = await processAIColumns(table, row, {
|
||||||
|
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) {
|
||||||
await updateRelatedFormula(table, enrichedRow)
|
await updateRelatedFormula(table, enrichedRow)
|
||||||
|
|
|
@ -124,6 +124,7 @@ export async function buildSqlFieldList(
|
||||||
([columnName, column]) =>
|
([columnName, column]) =>
|
||||||
column.type !== FieldType.LINK &&
|
column.type !== FieldType.LINK &&
|
||||||
column.type !== FieldType.FORMULA &&
|
column.type !== FieldType.FORMULA &&
|
||||||
|
column.type !== FieldType.AI &&
|
||||||
!existing.find(
|
!existing.find(
|
||||||
(field: string) => field === `${table.name}.${columnName}`
|
(field: string) => field === `${table.name}.${columnName}`
|
||||||
)
|
)
|
||||||
|
@ -133,9 +134,7 @@ export async function buildSqlFieldList(
|
||||||
|
|
||||||
let fields: string[] = []
|
let fields: string[] = []
|
||||||
if (sdk.views.isView(source)) {
|
if (sdk.views.isView(source)) {
|
||||||
fields = Object.keys(helpers.views.basicFields(source)).filter(
|
fields = Object.keys(helpers.views.basicFields(source))
|
||||||
key => source.schema?.[key]?.visible !== false
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
fields = extractRealFields(source)
|
fields = extractRealFields(source)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,15 @@ export async function find(ctx: Ctx<void, RowActionsResponse>) {
|
||||||
const table = await getTable(ctx)
|
const table = await getTable(ctx)
|
||||||
const tableId = table._id!
|
const tableId = table._id!
|
||||||
|
|
||||||
if (!(await sdk.rowActions.docExists(tableId))) {
|
const rowActions = await sdk.rowActions.getAll(tableId)
|
||||||
|
if (!rowActions) {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
actions: {},
|
actions: {},
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { actions } = await sdk.rowActions.getAll(tableId)
|
const { actions } = rowActions
|
||||||
const result: RowActionsResponse = {
|
const result: RowActionsResponse = {
|
||||||
actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>(
|
actions: Object.entries(actions).reduce<Record<string, RowActionResponse>>(
|
||||||
(acc, [key, action]) => ({
|
(acc, [key, action]) => ({
|
||||||
|
|
|
@ -6,10 +6,10 @@ import uniq from "lodash/uniq"
|
||||||
import { updateAllFormulasInTable } from "../row/staticFormula"
|
import { updateAllFormulasInTable } from "../row/staticFormula"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
FormulaType,
|
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
FieldType,
|
FieldType,
|
||||||
FormulaFieldMetadata,
|
FormulaFieldMetadata,
|
||||||
|
FormulaType,
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
|
|
@ -71,19 +71,20 @@ export async function fetch(ctx: UserCtx<void, FetchTablesResponse>) {
|
||||||
|
|
||||||
const datasources = await sdk.datasources.getExternalDatasources()
|
const datasources = await sdk.datasources.getExternalDatasources()
|
||||||
|
|
||||||
const external = datasources.flatMap(datasource => {
|
const external: Table[] = []
|
||||||
|
for (const datasource of datasources) {
|
||||||
let entities = datasource.entities
|
let entities = datasource.entities
|
||||||
if (entities) {
|
if (entities) {
|
||||||
return Object.values(entities).map<Table>((entity: Table) => ({
|
for (const entity of Object.values(entities)) {
|
||||||
...entity,
|
external.push({
|
||||||
sourceType: TableSourceType.EXTERNAL,
|
...(await processTable(entity)),
|
||||||
sourceId: datasource._id!,
|
sourceType: TableSourceType.EXTERNAL,
|
||||||
sql: isSQL(datasource),
|
sourceId: datasource._id!,
|
||||||
}))
|
sql: isSQL(datasource),
|
||||||
} else {
|
})
|
||||||
return []
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const result: FetchTablesResponse = []
|
const result: FetchTablesResponse = []
|
||||||
for (const table of [...internal, ...external]) {
|
for (const table of [...internal, ...external]) {
|
||||||
|
@ -139,6 +140,7 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
|
||||||
export async function destroy(ctx: UserCtx) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
|
await sdk.rowActions.deleteAll(tableId)
|
||||||
const deletedTable = await pickApi({ tableId }).destroy(ctx)
|
const deletedTable = await pickApi({ tableId }).destroy(ctx)
|
||||||
await events.table.deleted(deletedTable)
|
await events.table.deleted(deletedTable)
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
const { roles, events, permissions } = require("@budibase/backend-core")
|
||||||
|
const setup = require("./utilities")
|
||||||
|
const { PermissionLevel } = require("@budibase/types")
|
||||||
|
const { basicRole } = setup.structures
|
||||||
|
const { BUILTIN_ROLE_IDS } = roles
|
||||||
|
const { BuiltinPermissionID } = permissions
|
||||||
|
|
||||||
|
describe("/roles", () => {
|
||||||
|
let request = setup.getRequest()
|
||||||
|
let config = setup.getConfig()
|
||||||
|
|
||||||
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
const createRole = async role => {
|
||||||
|
if (!role) {
|
||||||
|
role = basicRole()
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post(`/api/roles`)
|
||||||
|
.send(role)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
it("returns a success message when role is successfully created", async () => {
|
||||||
|
const role = basicRole()
|
||||||
|
const res = await createRole(role)
|
||||||
|
|
||||||
|
expect(res.body._id).toBeDefined()
|
||||||
|
expect(res.body._rev).toBeDefined()
|
||||||
|
expect(events.role.updated).not.toBeCalled()
|
||||||
|
expect(events.role.created).toBeCalledTimes(1)
|
||||||
|
expect(events.role.created).toBeCalledWith(res.body)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
it("updates a role", async () => {
|
||||||
|
const role = basicRole()
|
||||||
|
let res = await createRole(role)
|
||||||
|
jest.clearAllMocks()
|
||||||
|
res = await createRole(res.body)
|
||||||
|
|
||||||
|
expect(res.body._id).toBeDefined()
|
||||||
|
expect(res.body._rev).toBeDefined()
|
||||||
|
expect(events.role.created).not.toBeCalled()
|
||||||
|
expect(events.role.updated).toBeCalledTimes(1)
|
||||||
|
expect(events.role.updated).toBeCalledWith(res.body)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("fetch", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Recreate the app
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should list custom roles, plus 2 default roles", async () => {
|
||||||
|
const customRole = await config.createRole()
|
||||||
|
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/roles`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.length).toBe(5)
|
||||||
|
|
||||||
|
const adminRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN)
|
||||||
|
expect(adminRole).toBeDefined()
|
||||||
|
expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER)
|
||||||
|
expect(adminRole.permissionId).toEqual(BuiltinPermissionID.ADMIN)
|
||||||
|
|
||||||
|
const powerUserRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.POWER)
|
||||||
|
expect(powerUserRole).toBeDefined()
|
||||||
|
expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
|
||||||
|
expect(powerUserRole.permissionId).toEqual(BuiltinPermissionID.POWER)
|
||||||
|
|
||||||
|
const customRoleFetched = res.body.find(r => r._id === customRole.name)
|
||||||
|
expect(customRoleFetched).toBeDefined()
|
||||||
|
expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
|
||||||
|
expect(customRoleFetched.permissionId).toEqual(
|
||||||
|
BuiltinPermissionID.READ_ONLY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to get the role with a permission added", async () => {
|
||||||
|
const table = await config.createTable()
|
||||||
|
await config.api.permission.add({
|
||||||
|
roleId: BUILTIN_ROLE_IDS.POWER,
|
||||||
|
resourceId: table._id,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/roles`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body.length).toBeGreaterThan(0)
|
||||||
|
const power = res.body.find(role => role._id === BUILTIN_ROLE_IDS.POWER)
|
||||||
|
expect(power.permissions[table._id]).toEqual(["read"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("destroy", () => {
|
||||||
|
it("should delete custom roles", async () => {
|
||||||
|
const customRole = await config.createRole({
|
||||||
|
name: "user",
|
||||||
|
permissionId: BuiltinPermissionID.READ_ONLY,
|
||||||
|
inherits: BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
})
|
||||||
|
delete customRole._rev_tree
|
||||||
|
await request
|
||||||
|
.delete(`/api/roles/${customRole._id}/${customRole._rev}`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect(200)
|
||||||
|
await request
|
||||||
|
.get(`/api/roles/${customRole._id}`)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect(404)
|
||||||
|
expect(events.role.deleted).toBeCalledTimes(1)
|
||||||
|
expect(events.role.deleted).toBeCalledWith(customRole)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("accessible", () => {
|
||||||
|
it("should be able to fetch accessible roles (with builder)", async () => {
|
||||||
|
const res = await request
|
||||||
|
.get("/api/roles/accessible")
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body.length).toBe(5)
|
||||||
|
expect(typeof res.body[0]).toBe("string")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to fetch accessible roles (basic user)", async () => {
|
||||||
|
const res = await request
|
||||||
|
.get("/api/roles/accessible")
|
||||||
|
.set(await config.basicRoleHeaders())
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body.length).toBe(2)
|
||||||
|
expect(res.body[0]).toBe("BASIC")
|
||||||
|
expect(res.body[1]).toBe("PUBLIC")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to fetch accessible roles (no user)", async () => {
|
||||||
|
const res = await request
|
||||||
|
.get("/api/roles/accessible")
|
||||||
|
.set(config.publicHeaders())
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body.length).toBe(1)
|
||||||
|
expect(res.body[0]).toBe("PUBLIC")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not fetch higher level accessible roles when a custom role header is provided", async () => {
|
||||||
|
await createRole({
|
||||||
|
name: `custom_role_1`,
|
||||||
|
inherits: roles.BUILTIN_ROLE_IDS.BASIC,
|
||||||
|
permissionId: permissions.BuiltinPermissionID.READ_ONLY,
|
||||||
|
version: "name",
|
||||||
|
})
|
||||||
|
const res = await request
|
||||||
|
.get("/api/roles/accessible")
|
||||||
|
.set({
|
||||||
|
...config.defaultHeaders(),
|
||||||
|
"x-budibase-role": "custom_role_1",
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
expect(res.body.length).toBe(3)
|
||||||
|
expect(res.body[0]).toBe("custom_role_1")
|
||||||
|
expect(res.body[1]).toBe("BASIC")
|
||||||
|
expect(res.body[2]).toBe("PUBLIC")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -14,9 +14,11 @@ import {
|
||||||
InternalTable,
|
InternalTable,
|
||||||
tenancy,
|
tenancy,
|
||||||
features,
|
features,
|
||||||
|
utils,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
|
AIOperationEnum,
|
||||||
AttachmentFieldMetadata,
|
AttachmentFieldMetadata,
|
||||||
AutoFieldSubType,
|
AutoFieldSubType,
|
||||||
Datasource,
|
Datasource,
|
||||||
|
@ -49,6 +51,18 @@ 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"
|
||||||
|
|
||||||
|
jest.mock("@budibase/pro", () => ({
|
||||||
|
...jest.requireActual("@budibase/pro"),
|
||||||
|
ai: {
|
||||||
|
LargeLanguageModel: {
|
||||||
|
forCurrentTenant: async () => ({
|
||||||
|
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)
|
||||||
interface WaitOptions {
|
interface WaitOptions {
|
||||||
|
@ -757,6 +771,70 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("user column", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
default: "{{ [Current User]._id }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates a new row with a default value successfully", async () => {
|
||||||
|
const row = await config.api.row.save(table._id!, {})
|
||||||
|
expect(row.user._id).toEqual(config.getUser()._id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not use default value if value specified", async () => {
|
||||||
|
const id = `us_${utils.newid()}`
|
||||||
|
await config.createUser({ _id: id })
|
||||||
|
const row = await config.api.row.save(table._id!, {
|
||||||
|
user: id,
|
||||||
|
})
|
||||||
|
expect(row.user._id).toEqual(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("multi-user column", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
users: {
|
||||||
|
name: "users",
|
||||||
|
type: FieldType.BB_REFERENCE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
default: ["{{ [Current User]._id }}"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates a new row with a default value successfully", async () => {
|
||||||
|
const row = await config.api.row.save(table._id!, {})
|
||||||
|
expect(row.users).toHaveLength(1)
|
||||||
|
expect(row.users[0]._id).toEqual(config.getUser()._id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not use default value if value specified", async () => {
|
||||||
|
const id = `us_${utils.newid()}`
|
||||||
|
await config.createUser({ _id: id })
|
||||||
|
const row = await config.api.row.save(table._id!, {
|
||||||
|
users: [id],
|
||||||
|
})
|
||||||
|
expect(row.users).toHaveLength(1)
|
||||||
|
expect(row.users[0]._id).toEqual(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("bindings", () => {
|
describe("bindings", () => {
|
||||||
describe("string column", () => {
|
describe("string column", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
@ -2021,6 +2099,7 @@ describe.each([
|
||||||
[FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(),
|
[FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(),
|
||||||
[FieldType.FORMULA]: undefined, // generated field
|
[FieldType.FORMULA]: undefined, // generated field
|
||||||
[FieldType.AUTO]: undefined, // generated field
|
[FieldType.AUTO]: undefined, // generated field
|
||||||
|
[FieldType.AI]: undefined, // generated field
|
||||||
[FieldType.JSON]: { name: generator.guid() },
|
[FieldType.JSON]: { name: generator.guid() },
|
||||||
[FieldType.INTERNAL]: generator.guid(),
|
[FieldType.INTERNAL]: generator.guid(),
|
||||||
[FieldType.BARCODEQR]: generator.guid(),
|
[FieldType.BARCODEQR]: generator.guid(),
|
||||||
|
@ -2124,6 +2203,7 @@ describe.each([
|
||||||
expectedRowData["bb_reference_single"].sample,
|
expectedRowData["bb_reference_single"].sample,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
ai: null,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
@ -2910,6 +2990,57 @@ describe.each([
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
isSqs &&
|
||||||
|
describe("AI fields", () => {
|
||||||
|
let table: Table
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
mocks.licenses.useBudibaseAI()
|
||||||
|
mocks.licenses.useAICustomConfigs()
|
||||||
|
table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
ai: {
|
||||||
|
name: "ai",
|
||||||
|
type: FieldType.AI,
|
||||||
|
operation: AIOperationEnum.PROMPT,
|
||||||
|
prompt: "Convert the following to German: '{{ product }}'",
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
name: "product",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
product: generator.word(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.unmock("@budibase/pro")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to save a row with an AI column", async () => {
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows.length).toBe(1)
|
||||||
|
expect(rows[0].ai).toEqual("Mock LLM Response")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to update a row with an AI column", async () => {
|
||||||
|
const { rows } = await config.api.row.search(table._id!)
|
||||||
|
expect(rows.length).toBe(1)
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
product: generator.word(),
|
||||||
|
...rows[0],
|
||||||
|
})
|
||||||
|
expect(rows.length).toBe(1)
|
||||||
|
expect(rows[0].ai).toEqual("Mock LLM Response")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Formula fields", () => {
|
describe("Formula fields", () => {
|
||||||
let table: Table
|
let table: Table
|
||||||
let otherTable: Table
|
let otherTable: Table
|
||||||
|
|
|
@ -1043,4 +1043,44 @@ describe("/rowsActions", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("scenarios", () => {
|
||||||
|
// https://linear.app/budibase/issue/BUDI-8717/
|
||||||
|
it("should not brick the app when deleting a table with row actions", async () => {
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId,
|
||||||
|
name: generator.guid(),
|
||||||
|
schema: {
|
||||||
|
name: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const rowAction = await config.api.rowAction.save(tableId, {
|
||||||
|
name: generator.guid(),
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.rowAction.setViewPermission(
|
||||||
|
tableId,
|
||||||
|
view.id,
|
||||||
|
rowAction.id
|
||||||
|
)
|
||||||
|
|
||||||
|
let actionsResp = await config.api.rowAction.find(tableId)
|
||||||
|
expect(actionsResp.actions[rowAction.id]).toEqual({
|
||||||
|
...rowAction,
|
||||||
|
allowedSources: [tableId, view.id],
|
||||||
|
})
|
||||||
|
|
||||||
|
const table = await config.api.table.get(tableId)
|
||||||
|
await config.api.table.destroy(table._id!, table._rev!)
|
||||||
|
|
||||||
|
// In the bug reported by Conor, when a delete got deleted its row action
|
||||||
|
// document was not being cleaned up. This meant there existed code paths
|
||||||
|
// that would find it and try to reference the tables within it, resulting
|
||||||
|
// in errors.
|
||||||
|
await config.api.automation.fetchEnriched({
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
|
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import {
|
import {
|
||||||
|
AIOperationEnum,
|
||||||
AutoFieldSubType,
|
AutoFieldSubType,
|
||||||
BBReferenceFieldSubType,
|
BBReferenceFieldSubType,
|
||||||
Datasource,
|
Datasource,
|
||||||
|
@ -41,11 +42,23 @@ import tk from "timekeeper"
|
||||||
import { encodeJSBinding } from "@budibase/string-templates"
|
import { encodeJSBinding } from "@budibase/string-templates"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
import { Knex } from "knex"
|
import { Knex } from "knex"
|
||||||
import { generator, structures } from "@budibase/backend-core/tests"
|
import { generator, structures, mocks } from "@budibase/backend-core/tests"
|
||||||
import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default"
|
import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default"
|
||||||
import { generateRowIdField } from "../../../integrations/utils"
|
import { generateRowIdField } from "../../../integrations/utils"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
jest.mock("@budibase/pro", () => ({
|
||||||
|
...jest.requireActual("@budibase/pro"),
|
||||||
|
ai: {
|
||||||
|
LargeLanguageModel: {
|
||||||
|
forCurrentTenant: async () => ({
|
||||||
|
run: jest.fn(() => `Mock LLM Response`),
|
||||||
|
buildPromptFromAIOperation: jest.fn(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
["in-memory", undefined],
|
["in-memory", undefined],
|
||||||
["lucene", undefined],
|
["lucene", undefined],
|
||||||
|
@ -1601,6 +1614,79 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
isSqs &&
|
||||||
|
describe("AI Column", () => {
|
||||||
|
const UNEXISTING_AI_COLUMN = "Real LLM Response"
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
mocks.licenses.useBudibaseAI()
|
||||||
|
mocks.licenses.useAICustomConfigs()
|
||||||
|
|
||||||
|
tableOrViewId = await createTableOrView({
|
||||||
|
product: { name: "product", type: FieldType.STRING },
|
||||||
|
ai: {
|
||||||
|
name: "AI",
|
||||||
|
type: FieldType.AI,
|
||||||
|
operation: AIOperationEnum.PROMPT,
|
||||||
|
prompt: "Translate '{{ product }}' into German",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await createRows([{ product: "Big Mac" }, { product: "McCrispy" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("equal", () => {
|
||||||
|
it("successfully finds rows based on AI column", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
equal: { ai: "Mock LLM Response" },
|
||||||
|
}).toContainExactly([
|
||||||
|
{ product: "Big Mac" },
|
||||||
|
{ product: "McCrispy" },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
equal: { ai: UNEXISTING_AI_COLUMN },
|
||||||
|
}).toFindNothing()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("notEqual", () => {
|
||||||
|
it("Returns nothing when searching notEqual on the mock AI response", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
notEqual: { ai: "Mock LLM Response" },
|
||||||
|
}).toContainExactly([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("return all when requesting non-existing response", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
notEqual: { ai: "Real LLM Response" },
|
||||||
|
}).toContainExactly([
|
||||||
|
{ product: "Big Mac" },
|
||||||
|
{ product: "McCrispy" },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("oneOf", () => {
|
||||||
|
it("successfully finds a row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
oneOf: { ai: ["Mock LLM Response", "Other LLM Response"] },
|
||||||
|
}).toContainExactly([
|
||||||
|
{ product: "Big Mac" },
|
||||||
|
{ product: "McCrispy" },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails to find nonexistent row", async () => {
|
||||||
|
await expectQuery({
|
||||||
|
oneOf: { ai: ["Whopper"] },
|
||||||
|
}).toFindNothing()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => {
|
describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
tableOrViewId = await createTableOrView({
|
tableOrViewId = await createTableOrView({
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
ViewCalculation,
|
ViewCalculation,
|
||||||
ViewV2Enriched,
|
ViewV2Enriched,
|
||||||
RowExportFormat,
|
RowExportFormat,
|
||||||
|
PermissionLevel,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
|
@ -191,6 +192,55 @@ describe.each([
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("permissions", () => {
|
||||||
|
it("get the base permissions for the table", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
tableForDatasource(datasource, {
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// get the explicit permissions
|
||||||
|
const { permissions } = await config.api.permission.get(table._id!, {
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
const explicitPermissions = {
|
||||||
|
role: "ADMIN",
|
||||||
|
permissionType: "EXPLICIT",
|
||||||
|
}
|
||||||
|
expect(permissions.write).toEqual(explicitPermissions)
|
||||||
|
expect(permissions.read).toEqual(explicitPermissions)
|
||||||
|
|
||||||
|
// revoke the explicit permissions
|
||||||
|
for (let level of [PermissionLevel.WRITE, PermissionLevel.READ]) {
|
||||||
|
await config.api.permission.revoke(
|
||||||
|
{
|
||||||
|
roleId: permissions[level].role,
|
||||||
|
resourceId: table._id!,
|
||||||
|
level,
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check base permissions
|
||||||
|
const { permissions: basePermissions } = await config.api.permission.get(
|
||||||
|
table._id!,
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const basePerms = { role: "BASIC", permissionType: "BASE" }
|
||||||
|
expect(basePermissions.write).toEqual(basePerms)
|
||||||
|
expect(basePermissions.read).toEqual(basePerms)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("updates a table", async () => {
|
it("updates a table", async () => {
|
||||||
const table = await config.api.table.save(
|
const table = await config.api.table.save(
|
||||||
|
|
|
@ -206,7 +206,7 @@ describe.each([
|
||||||
visible: false,
|
visible: false,
|
||||||
icon: "ic",
|
icon: "ic",
|
||||||
},
|
},
|
||||||
} as Record<string, FieldSchema>,
|
} as ViewV2Schema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdView = await config.api.viewV2.create(newView)
|
const createdView = await config.api.viewV2.create(newView)
|
||||||
|
@ -250,7 +250,7 @@ describe.each([
|
||||||
name: "Category",
|
name: "Category",
|
||||||
type: FieldType.STRING,
|
type: FieldType.STRING,
|
||||||
},
|
},
|
||||||
} as Record<string, FieldSchema>,
|
} as ViewV2Schema,
|
||||||
}
|
}
|
||||||
|
|
||||||
await config.api.viewV2.create(newView, {
|
await config.api.viewV2.create(newView, {
|
||||||
|
@ -800,6 +800,34 @@ describe.each([
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
isInternal &&
|
||||||
|
it("shouldn't trigger a complex type check on a group by field if field is invisible", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
field: {
|
||||||
|
name: "field",
|
||||||
|
type: FieldType.JSON,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await config.api.viewV2.create(
|
||||||
|
{
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
type: ViewV2Type.CALCULATION,
|
||||||
|
schema: {
|
||||||
|
field: { visible: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 201,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
|
@ -1016,7 +1044,7 @@ describe.each([
|
||||||
visible: false,
|
visible: false,
|
||||||
icon: "ic",
|
icon: "ic",
|
||||||
},
|
},
|
||||||
} as Record<string, FieldSchema>,
|
} as ViewV2Schema,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(updatedView).toEqual({
|
expect(updatedView).toEqual({
|
||||||
|
@ -1050,7 +1078,7 @@ describe.each([
|
||||||
name: "Category",
|
name: "Category",
|
||||||
type: FieldType.STRING,
|
type: FieldType.STRING,
|
||||||
},
|
},
|
||||||
} as Record<string, FieldSchema>,
|
} as ViewV2Schema,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
|
@ -1904,6 +1932,59 @@ describe.each([
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("calculation views", () => {
|
||||||
|
it("should not remove calculation columns when modifying table schema", async () => {
|
||||||
|
let table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
name: "age",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
let view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
type: ViewV2Type.CALCULATION,
|
||||||
|
schema: {
|
||||||
|
sum: {
|
||||||
|
visible: true,
|
||||||
|
calculationType: CalculationType.SUM,
|
||||||
|
field: "age",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
table = await config.api.table.get(table._id!)
|
||||||
|
await config.api.table.save({
|
||||||
|
...table,
|
||||||
|
schema: {
|
||||||
|
...table.schema,
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: { presence: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
view = await config.api.viewV2.get(view.id)
|
||||||
|
expect(Object.keys(view.schema!).sort()).toEqual([
|
||||||
|
"age",
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"sum",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("row operations", () => {
|
describe("row operations", () => {
|
||||||
|
@ -2148,6 +2229,32 @@ describe.each([
|
||||||
})
|
})
|
||||||
await config.api.row.get(table._id!, rows[1]._id!, { status: 200 })
|
await config.api.row.get(table._id!, rows[1]._id!, { status: 200 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should not be possible to delete a row in a calculation view", async () => {
|
||||||
|
const row = await config.api.row.save(table._id!, {})
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
type: ViewV2Type.CALCULATION,
|
||||||
|
schema: {
|
||||||
|
id: { visible: true },
|
||||||
|
one: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.row.delete(
|
||||||
|
view.id,
|
||||||
|
{ _id: row._id! },
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
body: {
|
||||||
|
message: "Cannot delete rows through a calculation view",
|
||||||
|
status: 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("read", () => {
|
describe("read", () => {
|
||||||
|
@ -3157,6 +3264,241 @@ describe.each([
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to filter rows on the view itself", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
quantity: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "quantity",
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
type: ViewV2Type.CALCULATION,
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
sum: {
|
||||||
|
visible: true,
|
||||||
|
calculationType: CalculationType.SUM,
|
||||||
|
field: "price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.row.bulkImport(table._id!, {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 2,
|
||||||
|
price: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows } = await config.api.viewV2.search(view.id, {
|
||||||
|
query: {},
|
||||||
|
})
|
||||||
|
expect(rows).toHaveLength(1)
|
||||||
|
expect(rows[0].sum).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to filter on group by fields", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
quantity: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "quantity",
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
type: ViewV2Type.CALCULATION,
|
||||||
|
schema: {
|
||||||
|
quantity: { visible: true },
|
||||||
|
sum: {
|
||||||
|
visible: true,
|
||||||
|
calculationType: CalculationType.SUM,
|
||||||
|
field: "price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.row.bulkImport(table._id!, {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 2,
|
||||||
|
price: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows } = await config.api.viewV2.search(view.id, {
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(rows).toHaveLength(1)
|
||||||
|
expect(rows[0].sum).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to sort by group by field", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
quantity: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "quantity",
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
type: ViewV2Type.CALCULATION,
|
||||||
|
schema: {
|
||||||
|
quantity: { visible: true },
|
||||||
|
sum: {
|
||||||
|
visible: true,
|
||||||
|
calculationType: CalculationType.SUM,
|
||||||
|
field: "price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.api.row.bulkImport(table._id!, {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 2,
|
||||||
|
price: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows } = await config.api.viewV2.search(view.id, {
|
||||||
|
query: {},
|
||||||
|
sort: "quantity",
|
||||||
|
sortOrder: SortOrder.DESCENDING,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(rows).toEqual([
|
||||||
|
expect.objectContaining({ quantity: 2, sum: 10 }),
|
||||||
|
expect.objectContaining({ quantity: 1, sum: 3 }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to sort by a calculation", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
quantity: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "quantity",
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await config.api.row.bulkImport(table._id!, {
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quantity: 2,
|
||||||
|
price: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
type: ViewV2Type.CALCULATION,
|
||||||
|
schema: {
|
||||||
|
quantity: { visible: true },
|
||||||
|
sum: {
|
||||||
|
visible: true,
|
||||||
|
calculationType: CalculationType.SUM,
|
||||||
|
field: "price",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows } = await config.api.viewV2.search(view.id, {
|
||||||
|
query: {},
|
||||||
|
sort: "sum",
|
||||||
|
sortOrder: SortOrder.DESCENDING,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(rows).toEqual([
|
||||||
|
expect.objectContaining({ quantity: 2, sum: 10 }),
|
||||||
|
expect.objectContaining({ quantity: 1, sum: 3 }),
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
!isLucene &&
|
!isLucene &&
|
||||||
|
@ -3204,6 +3546,50 @@ describe.each([
|
||||||
expect(response.rows).toHaveLength(1)
|
expect(response.rows).toHaveLength(1)
|
||||||
expect(response.rows[0].sum).toEqual(61)
|
expect(response.rows[0].sum).toEqual(61)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should be able to filter on a single user field in both the view query and search query", async () => {
|
||||||
|
const table = await config.api.table.save(
|
||||||
|
saveTableRequest({
|
||||||
|
schema: {
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
type: FieldType.BB_REFERENCE_SINGLE,
|
||||||
|
subtype: BBReferenceFieldSubType.USER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await config.api.row.save(table._id!, {
|
||||||
|
user: config.getUser()._id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
name: generator.guid(),
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
user: "{{ [user].[_id] }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
user: {
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { rows } = await config.api.viewV2.search(view.id, {
|
||||||
|
query: {
|
||||||
|
equal: {
|
||||||
|
user: "{{ [user].[_id] }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(rows).toHaveLength(1)
|
||||||
|
expect(rows[0].user._id).toEqual(config.getUser()._id)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("permissions", () => {
|
describe("permissions", () => {
|
||||||
|
|
|
@ -20,29 +20,28 @@ const OPTIONAL_NUMBER = Joi.number().optional().allow(null)
|
||||||
const OPTIONAL_BOOLEAN = Joi.boolean().optional().allow(null)
|
const OPTIONAL_BOOLEAN = Joi.boolean().optional().allow(null)
|
||||||
const APP_NAME_REGEX = /^[\w\s]+$/
|
const APP_NAME_REGEX = /^[\w\s]+$/
|
||||||
|
|
||||||
const validateViewSchemas: CustomValidator<Table> = (table, helpers) => {
|
const validateViewSchemas: CustomValidator<Table> = (table, joiHelpers) => {
|
||||||
if (table.views && Object.entries(table.views).length) {
|
if (!table.views || Object.keys(table.views).length === 0) {
|
||||||
const requiredFields = Object.entries(table.schema)
|
return table
|
||||||
.filter(([_, v]) => isRequired(v.constraints))
|
}
|
||||||
|
const required = Object.keys(table.schema).filter(key =>
|
||||||
|
isRequired(table.schema[key].constraints)
|
||||||
|
)
|
||||||
|
if (required.length === 0) {
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
for (const view of Object.values(table.views)) {
|
||||||
|
if (!sdk.views.isV2(view) || helpers.views.isCalculationView(view)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const editable = Object.entries(view.schema || {})
|
||||||
|
.filter(([_, f]) => f.visible && !f.readonly)
|
||||||
.map(([key]) => key)
|
.map(([key]) => key)
|
||||||
if (requiredFields.length) {
|
const missingField = required.find(f => !editable.includes(f))
|
||||||
for (const view of Object.values(table.views)) {
|
if (missingField) {
|
||||||
if (!sdk.views.isV2(view)) {
|
return joiHelpers.message({
|
||||||
continue
|
custom: `To make field "${missingField}" required, this field must be present and writable in views: ${view.name}.`,
|
||||||
}
|
})
|
||||||
|
|
||||||
const editableViewFields = Object.entries(view.schema || {})
|
|
||||||
.filter(([_, f]) => f.visible && !f.readonly)
|
|
||||||
.map(([key]) => key)
|
|
||||||
const missingField = requiredFields.find(
|
|
||||||
f => !editableViewFields.includes(f)
|
|
||||||
)
|
|
||||||
if (missingField) {
|
|
||||||
return helpers.message({
|
|
||||||
custom: `To make field "${missingField}" required, this field must be present and writable in views: ${view.name}.`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return table
|
return table
|
||||||
|
|
|
@ -307,7 +307,9 @@ export async function squashLinks<T = Row[] | Row>(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
[FieldType.LINK, FieldType.FORMULA].includes(tableColumn.type)
|
[FieldType.LINK, FieldType.FORMULA, FieldType.AI].includes(
|
||||||
|
tableColumn.type
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ interface AuthTokenResponse {
|
||||||
const isTypeAllowed: Record<FieldType, boolean> = {
|
const isTypeAllowed: Record<FieldType, boolean> = {
|
||||||
[FieldType.STRING]: true,
|
[FieldType.STRING]: true,
|
||||||
[FieldType.FORMULA]: true,
|
[FieldType.FORMULA]: true,
|
||||||
|
[FieldType.AI]: true,
|
||||||
[FieldType.NUMBER]: true,
|
[FieldType.NUMBER]: true,
|
||||||
[FieldType.LONGFORM]: true,
|
[FieldType.LONGFORM]: true,
|
||||||
[FieldType.DATETIME]: true,
|
[FieldType.DATETIME]: true,
|
||||||
|
@ -490,7 +491,8 @@ export class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!sheet.headerValues.includes(key) &&
|
!sheet.headerValues.includes(key) &&
|
||||||
column.type !== FieldType.FORMULA
|
column.type !== FieldType.FORMULA &&
|
||||||
|
column.type !== FieldType.AI
|
||||||
) {
|
) {
|
||||||
updatedHeaderValues.push(key)
|
updatedHeaderValues.push(key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,6 +242,7 @@ function copyExistingPropsOver(
|
||||||
let shouldKeepSchema = false
|
let shouldKeepSchema = false
|
||||||
switch (existingColumnType) {
|
switch (existingColumnType) {
|
||||||
case FieldType.FORMULA:
|
case FieldType.FORMULA:
|
||||||
|
case FieldType.AI:
|
||||||
case FieldType.AUTO:
|
case FieldType.AUTO:
|
||||||
case FieldType.INTERNAL:
|
case FieldType.INTERNAL:
|
||||||
shouldKeepSchema = true
|
shouldKeepSchema = true
|
||||||
|
|
|
@ -26,13 +26,13 @@ export async function getBuilderData(
|
||||||
return tableNameCache[tableId]
|
return tableNameCache[tableId]
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowActionNameCache: Record<string, TableRowActions> = {}
|
const rowActionNameCache: Record<string, TableRowActions | undefined> = {}
|
||||||
async function getRowActionName(tableId: string, rowActionId: string) {
|
async function getRowActionName(tableId: string, rowActionId: string) {
|
||||||
if (!rowActionNameCache[tableId]) {
|
if (!rowActionNameCache[tableId]) {
|
||||||
rowActionNameCache[tableId] = await sdk.rowActions.getAll(tableId)
|
rowActionNameCache[tableId] = await sdk.rowActions.getAll(tableId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rowActionNameCache[tableId].actions[rowActionId]?.name
|
return rowActionNameCache[tableId]?.actions[rowActionId]?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: Record<string, AutomationBuilderData> = {}
|
const result: Record<string, AutomationBuilderData> = {}
|
||||||
|
@ -51,6 +51,10 @@ export async function getBuilderData(
|
||||||
const tableName = await getTableName(tableId)
|
const tableName = await getTableName(tableId)
|
||||||
const rowActionName = await getRowActionName(tableId, rowActionId)
|
const rowActionName = await getRowActionName(tableId, rowActionId)
|
||||||
|
|
||||||
|
if (!rowActionName) {
|
||||||
|
throw new Error(`Row action not found: ${rowActionId}`)
|
||||||
|
}
|
||||||
|
|
||||||
result[automation._id!] = {
|
result[automation._id!] = {
|
||||||
displayName: rowActionName,
|
displayName: rowActionName,
|
||||||
triggerInfo: {
|
triggerInfo: {
|
||||||
|
|
|
@ -73,8 +73,7 @@ export async function getResourcePerms(
|
||||||
p[level] = { role, type: PermissionSource.BASE }
|
p[level] = { role, type: PermissionSource.BASE }
|
||||||
return p
|
return p
|
||||||
}, {})
|
}, {})
|
||||||
const result = Object.assign(basePermissions, permissions)
|
return Object.assign(basePermissions, permissions)
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDependantResources(
|
export async function getDependantResources(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
|
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
|
Automation,
|
||||||
AutomationTriggerStepId,
|
AutomationTriggerStepId,
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
TableRowActions,
|
TableRowActions,
|
||||||
|
@ -102,7 +103,25 @@ export async function get(tableId: string, rowActionId: string) {
|
||||||
export async function getAll(tableId: string) {
|
export async function getAll(tableId: string) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const rowActionsId = generateRowActionsID(tableId)
|
const rowActionsId = generateRowActionsID(tableId)
|
||||||
return await db.get<TableRowActions>(rowActionsId)
|
return await db.tryGet<TableRowActions>(rowActionsId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAll(tableId: string) {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
|
||||||
|
const doc = await getAll(tableId)
|
||||||
|
if (!doc) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const automationIds = Object.values(doc.actions).map(a => a.automationId)
|
||||||
|
const automations = await db.getMultiple<Automation>(automationIds)
|
||||||
|
|
||||||
|
for (const automation of automations) {
|
||||||
|
await sdk.automations.remove(automation._id!, automation._rev!)
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.remove(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function docExists(tableId: string) {
|
export async function docExists(tableId: string) {
|
||||||
|
@ -223,9 +242,8 @@ export async function run(tableId: any, rowActionId: any, rowId: string) {
|
||||||
throw new HTTPError("Table not found", 404)
|
throw new HTTPError("Table not found", 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { actions } = await getAll(tableId)
|
const rowActions = await getAll(tableId)
|
||||||
|
const rowAction = rowActions?.actions[rowActionId]
|
||||||
const rowAction = actions[rowActionId]
|
|
||||||
if (!rowAction) {
|
if (!rowAction) {
|
||||||
throw new HTTPError("Row action not found", 404)
|
throw new HTTPError("Row action not found", 404)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import * as external from "./search/external"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
import sdk from "../../index"
|
import sdk from "../../index"
|
||||||
import { searchInputMapping } from "./search/utils"
|
import { checkFilters, searchInputMapping } from "./search/utils"
|
||||||
import { db, features } from "@budibase/backend-core"
|
import { db, features } from "@budibase/backend-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
import { getQueryableFields, removeInvalidFilters } from "./queryUtils"
|
||||||
|
@ -81,6 +81,10 @@ export async function search(
|
||||||
options.query = {}
|
options.query = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
options.query = await enrichSearchContext(options.query, context)
|
||||||
|
}
|
||||||
|
|
||||||
// need to make sure filters in correct shape before checking for view
|
// need to make sure filters in correct shape before checking for view
|
||||||
options = searchInputMapping(table, options)
|
options = searchInputMapping(table, options)
|
||||||
|
|
||||||
|
@ -92,12 +96,15 @@ export async function search(
|
||||||
// Enrich saved query with ephemeral query params.
|
// Enrich saved query with ephemeral query params.
|
||||||
// We prevent searching on any fields that are saved as part of the query, as
|
// We prevent searching on any fields that are saved as part of the query, as
|
||||||
// that could let users find rows they should not be allowed to access.
|
// that could let users find rows they should not be allowed to access.
|
||||||
let viewQuery = dataFilters.buildQueryLegacy(view.query) || {}
|
let viewQuery = await enrichSearchContext(view.query || {}, context)
|
||||||
|
viewQuery = dataFilters.buildQueryLegacy(viewQuery) || {}
|
||||||
|
viewQuery = checkFilters(table, viewQuery)
|
||||||
delete viewQuery?.onEmptyFilter
|
delete viewQuery?.onEmptyFilter
|
||||||
|
|
||||||
const sqsEnabled = await features.flags.isEnabled("SQS")
|
const sqsEnabled = await features.flags.isEnabled("SQS")
|
||||||
const supportsLogicalOperators =
|
const supportsLogicalOperators =
|
||||||
isExternalTableID(view.tableId) || sqsEnabled
|
isExternalTableID(view.tableId) || sqsEnabled
|
||||||
|
|
||||||
if (!supportsLogicalOperators) {
|
if (!supportsLogicalOperators) {
|
||||||
// In the unlikely event that a Grouped Filter is in a non-SQS environment
|
// In the unlikely event that a Grouped Filter is in a non-SQS environment
|
||||||
// It needs to be ignored entirely
|
// It needs to be ignored entirely
|
||||||
|
@ -113,13 +120,12 @@ export async function search(
|
||||||
?.filter(filter => filter.field)
|
?.filter(filter => filter.field)
|
||||||
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
.map(filter => db.removeKeyNumbering(filter.field)) || []
|
||||||
|
|
||||||
viewQuery ??= {}
|
|
||||||
// Carry over filters for unused fields
|
// Carry over filters for unused fields
|
||||||
Object.keys(options.query).forEach(key => {
|
Object.keys(options.query).forEach(key => {
|
||||||
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
const operator = key as Exclude<SearchFilterKey, LogicalOperator>
|
||||||
Object.keys(options.query[operator] || {}).forEach(field => {
|
Object.keys(options.query[operator] || {}).forEach(field => {
|
||||||
if (!existingFields.includes(db.removeKeyNumbering(field))) {
|
if (!existingFields.includes(db.removeKeyNumbering(field))) {
|
||||||
viewQuery![operator]![field] = options.query[operator]![field]
|
viewQuery[operator]![field] = options.query[operator]![field]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -137,10 +143,6 @@ export async function search(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context) {
|
|
||||||
options.query = await enrichSearchContext(options.query, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
options.query = dataFilters.cleanupQuery(options.query)
|
options.query = dataFilters.cleanupQuery(options.query)
|
||||||
options.query = dataFilters.fixupFilterArrays(options.query)
|
options.query = dataFilters.fixupFilterArrays(options.query)
|
||||||
|
|
||||||
|
|
|
@ -68,9 +68,7 @@ async function buildInternalFieldList(
|
||||||
const { relationships, allowedFields } = opts || {}
|
const { relationships, allowedFields } = opts || {}
|
||||||
let schemaFields: string[] = []
|
let schemaFields: string[] = []
|
||||||
if (sdk.views.isView(source)) {
|
if (sdk.views.isView(source)) {
|
||||||
schemaFields = Object.keys(helpers.views.basicFields(source)).filter(
|
schemaFields = Object.keys(helpers.views.basicFields(source))
|
||||||
key => source.schema?.[key]?.visible !== false
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
schemaFields = Object.keys(source.schema).filter(
|
schemaFields = Object.keys(source.schema).filter(
|
||||||
key => source.schema[key].visible !== false
|
key => source.schema[key].visible !== false
|
||||||
|
@ -420,13 +418,26 @@ export async function search(
|
||||||
|
|
||||||
if (params.sort) {
|
if (params.sort) {
|
||||||
const sortField = table.schema[params.sort]
|
const sortField = table.schema[params.sort]
|
||||||
const sortType =
|
const isAggregateField = aggregations.some(agg => agg.name === params.sort)
|
||||||
sortField.type === FieldType.NUMBER ? SortType.NUMBER : SortType.STRING
|
|
||||||
request.sort = {
|
if (isAggregateField) {
|
||||||
[mapToUserColumn(sortField.name)]: {
|
request.sort = {
|
||||||
direction: params.sortOrder || SortOrder.ASCENDING,
|
[params.sort]: {
|
||||||
type: sortType as SortType,
|
direction: params.sortOrder || SortOrder.ASCENDING,
|
||||||
},
|
type: SortType.NUMBER,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (sortField) {
|
||||||
|
const sortType =
|
||||||
|
sortField.type === FieldType.NUMBER ? SortType.NUMBER : SortType.STRING
|
||||||
|
request.sort = {
|
||||||
|
[mapToUserColumn(sortField.name)]: {
|
||||||
|
direction: params.sortOrder || SortOrder.ASCENDING,
|
||||||
|
type: sortType as SortType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unable to sort by ${params.sort}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,35 +80,41 @@ function userColumnMapping(column: string, filters: SearchFilters) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkFilters(
|
||||||
|
table: Table,
|
||||||
|
filters: SearchFilters
|
||||||
|
): SearchFilters {
|
||||||
|
for (let [key, column] of Object.entries(table.schema || {})) {
|
||||||
|
switch (column.type) {
|
||||||
|
case FieldType.BB_REFERENCE_SINGLE: {
|
||||||
|
const subtype = column.subtype
|
||||||
|
switch (subtype) {
|
||||||
|
case BBReferenceFieldSubType.USER:
|
||||||
|
userColumnMapping(key, filters)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
utils.unreachable(subtype)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case FieldType.BB_REFERENCE: {
|
||||||
|
userColumnMapping(key, filters)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataFilters.recurseLogicalOperators(filters, filters =>
|
||||||
|
checkFilters(table, filters)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// maps through the search parameters to check if any of the inputs are invalid
|
// maps through the search parameters to check if any of the inputs are invalid
|
||||||
// based on the table schema, converts them to something that is valid.
|
// based on the table schema, converts them to something that is valid.
|
||||||
export function searchInputMapping(table: Table, options: RowSearchParams) {
|
export function searchInputMapping(table: Table, options: RowSearchParams) {
|
||||||
// need an internal function to loop over filters, because this takes the full options
|
// need an internal function to loop over filters, because this takes the full options
|
||||||
function checkFilters(filters: SearchFilters) {
|
|
||||||
for (let [key, column] of Object.entries(table.schema || {})) {
|
|
||||||
switch (column.type) {
|
|
||||||
case FieldType.BB_REFERENCE_SINGLE: {
|
|
||||||
const subtype = column.subtype
|
|
||||||
switch (subtype) {
|
|
||||||
case BBReferenceFieldSubType.USER:
|
|
||||||
userColumnMapping(key, filters)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
utils.unreachable(subtype)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case FieldType.BB_REFERENCE: {
|
|
||||||
userColumnMapping(key, filters)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dataFilters.recurseLogicalOperators(filters, checkFilters)
|
|
||||||
}
|
|
||||||
if (options.query) {
|
if (options.query) {
|
||||||
options.query = checkFilters(options.query)
|
options.query = checkFilters(table, options.query)
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Row, Table } from "@budibase/types"
|
import { FeatureFlag, Row, Table } from "@budibase/types"
|
||||||
|
|
||||||
import * as external from "./external"
|
import * as external from "./external"
|
||||||
import * as internal from "./internal"
|
import * as internal from "./internal"
|
||||||
import { isExternal } from "./utils"
|
import { isExternal } from "./utils"
|
||||||
import { setPermissions } from "../permissions"
|
import { setPermissions } from "../permissions"
|
||||||
import { roles } from "@budibase/backend-core"
|
import { features, roles } from "@budibase/backend-core"
|
||||||
|
|
||||||
export async function create(
|
export async function create(
|
||||||
table: Omit<Table, "_id" | "_rev">,
|
table: Omit<Table, "_id" | "_rev">,
|
||||||
|
@ -18,10 +18,16 @@ export async function create(
|
||||||
createdTable = await internal.create(table, rows, userId)
|
createdTable = await internal.create(table, rows, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
await setPermissions(createdTable._id!, {
|
const setExplicitPermission = await features.flags.isEnabled(
|
||||||
writeRole: roles.BUILTIN_ROLE_IDS.ADMIN,
|
FeatureFlag.TABLES_DEFAULT_ADMIN
|
||||||
readRole: roles.BUILTIN_ROLE_IDS.ADMIN,
|
)
|
||||||
})
|
|
||||||
|
if (setExplicitPermission) {
|
||||||
|
await setPermissions(createdTable._id!, {
|
||||||
|
writeRole: roles.BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
readRole: roles.BUILTIN_ROLE_IDS.ADMIN,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return createdTable
|
return createdTable
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,13 @@ export async function processTable(table: Table): Promise<Table> {
|
||||||
if (!table) {
|
if (!table) {
|
||||||
return table
|
return table
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table = { ...table }
|
||||||
if (table._id && isExternalTableID(table._id)) {
|
if (table._id && isExternalTableID(table._id)) {
|
||||||
|
// Old created external tables via Budibase might have a missing field name breaking some UI such as filters
|
||||||
|
if (table.schema["id"] && !table.schema["id"].name) {
|
||||||
|
table.schema["id"].name = "id"
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...table,
|
...table,
|
||||||
type: "table",
|
type: "table",
|
||||||
|
|
|
@ -19,6 +19,7 @@ const FieldTypeMap: Record<FieldType, SQLiteType> = {
|
||||||
[FieldType.BOOLEAN]: SQLiteType.NUMERIC,
|
[FieldType.BOOLEAN]: SQLiteType.NUMERIC,
|
||||||
[FieldType.DATETIME]: SQLiteType.TEXT,
|
[FieldType.DATETIME]: SQLiteType.TEXT,
|
||||||
[FieldType.FORMULA]: SQLiteType.TEXT,
|
[FieldType.FORMULA]: SQLiteType.TEXT,
|
||||||
|
[FieldType.AI]: SQLiteType.TEXT,
|
||||||
[FieldType.LONGFORM]: SQLiteType.TEXT,
|
[FieldType.LONGFORM]: SQLiteType.TEXT,
|
||||||
[FieldType.NUMBER]: SQLiteType.REAL,
|
[FieldType.NUMBER]: SQLiteType.REAL,
|
||||||
[FieldType.STRING]: SQLiteType.TEXT,
|
[FieldType.STRING]: SQLiteType.TEXT,
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import {
|
import {
|
||||||
|
BBReferenceFieldSubType,
|
||||||
CalculationType,
|
CalculationType,
|
||||||
canGroupBy,
|
canGroupBy,
|
||||||
|
FeatureFlag,
|
||||||
FieldType,
|
FieldType,
|
||||||
isNumeric,
|
isNumeric,
|
||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
RelationSchemaField,
|
RelationSchemaField,
|
||||||
RenameColumn,
|
RenameColumn,
|
||||||
|
RequiredKeys,
|
||||||
Table,
|
Table,
|
||||||
TableSchema,
|
TableSchema,
|
||||||
View,
|
View,
|
||||||
|
@ -13,7 +16,7 @@ import {
|
||||||
ViewV2ColumnEnriched,
|
ViewV2ColumnEnriched,
|
||||||
ViewV2Enriched,
|
ViewV2Enriched,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { context, docIds, HTTPError } from "@budibase/backend-core"
|
import { context, docIds, features, HTTPError } from "@budibase/backend-core"
|
||||||
import {
|
import {
|
||||||
helpers,
|
helpers,
|
||||||
PROTECTED_EXTERNAL_COLUMNS,
|
PROTECTED_EXTERNAL_COLUMNS,
|
||||||
|
@ -94,6 +97,13 @@ async function guardCalculationViewSchema(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!schema.field) {
|
||||||
|
throw new HTTPError(
|
||||||
|
`Calculation field "${name}" is missing a "field" property`,
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const targetSchema = table.schema[schema.field]
|
const targetSchema = table.schema[schema.field]
|
||||||
if (!targetSchema) {
|
if (!targetSchema) {
|
||||||
throw new HTTPError(
|
throw new HTTPError(
|
||||||
|
@ -241,12 +251,17 @@ export async function create(
|
||||||
await guardViewSchema(tableId, viewRequest)
|
await guardViewSchema(tableId, viewRequest)
|
||||||
const view = await pickApi(tableId).create(tableId, viewRequest)
|
const view = await pickApi(tableId).create(tableId, viewRequest)
|
||||||
|
|
||||||
// Set permissions to be the same as the table
|
const setExplicitPermission = await features.flags.isEnabled(
|
||||||
const tablePerms = await sdk.permissions.getResourcePerms(tableId)
|
FeatureFlag.TABLES_DEFAULT_ADMIN
|
||||||
await sdk.permissions.setPermissions(view.id, {
|
)
|
||||||
writeRole: tablePerms[PermissionLevel.WRITE].role,
|
if (setExplicitPermission) {
|
||||||
readRole: tablePerms[PermissionLevel.READ].role,
|
// Set permissions to be the same as the table
|
||||||
})
|
const tablePerms = await sdk.permissions.getResourcePerms(tableId)
|
||||||
|
await sdk.permissions.setPermissions(view.id, {
|
||||||
|
writeRole: tablePerms[PermissionLevel.WRITE].role,
|
||||||
|
readRole: tablePerms[PermissionLevel.READ].role,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@ -298,7 +313,11 @@ export async function enrichSchema(
|
||||||
const result: Record<string, ViewV2ColumnEnriched> = {}
|
const result: Record<string, ViewV2ColumnEnriched> = {}
|
||||||
for (const relTableFieldName of Object.keys(relTable.schema)) {
|
for (const relTableFieldName of Object.keys(relTable.schema)) {
|
||||||
const relTableField = relTable.schema[relTableFieldName]
|
const relTableField = relTable.schema[relTableFieldName]
|
||||||
if ([FieldType.LINK, FieldType.FORMULA].includes(relTableField.type)) {
|
if (
|
||||||
|
[FieldType.LINK, FieldType.FORMULA, FieldType.AI].includes(
|
||||||
|
relTableField.type
|
||||||
|
)
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,13 +328,26 @@ export async function enrichSchema(
|
||||||
const viewFieldSchema = viewFields[relTableFieldName]
|
const viewFieldSchema = viewFields[relTableFieldName]
|
||||||
const isVisible = !!viewFieldSchema?.visible
|
const isVisible = !!viewFieldSchema?.visible
|
||||||
const isReadonly = !!viewFieldSchema?.readonly
|
const isReadonly = !!viewFieldSchema?.readonly
|
||||||
result[relTableFieldName] = {
|
const enrichedFieldSchema: RequiredKeys<ViewV2ColumnEnriched> = {
|
||||||
...relTableField,
|
|
||||||
...viewFieldSchema,
|
|
||||||
name: relTableField.name,
|
|
||||||
visible: isVisible,
|
visible: isVisible,
|
||||||
readonly: isReadonly,
|
readonly: isReadonly,
|
||||||
|
order: viewFieldSchema?.order,
|
||||||
|
width: viewFieldSchema?.width,
|
||||||
|
|
||||||
|
icon: relTableField.icon,
|
||||||
|
type: relTableField.type,
|
||||||
|
subtype: relTableField.subtype,
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!enrichedFieldSchema.icon &&
|
||||||
|
relTableField.type === FieldType.BB_REFERENCE &&
|
||||||
|
relTableField.subtype === BBReferenceFieldSubType.USER &&
|
||||||
|
!helpers.schema.isDeprecatedSingleUserColumn(relTableField)
|
||||||
|
) {
|
||||||
|
// Forcing the icon, otherwise we would need to pass the constraints to show the proper icon
|
||||||
|
enrichedFieldSchema.icon = "UserGroup"
|
||||||
|
}
|
||||||
|
result[relTableFieldName] = enrichedFieldSchema
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -368,7 +400,8 @@ export function syncSchema(
|
||||||
|
|
||||||
if (view.schema) {
|
if (view.schema) {
|
||||||
for (const fieldName of Object.keys(view.schema)) {
|
for (const fieldName of Object.keys(view.schema)) {
|
||||||
if (!schema[fieldName]) {
|
const viewSchema = view.schema[fieldName]
|
||||||
|
if (!helpers.views.isCalculationField(viewSchema) && !schema[fieldName]) {
|
||||||
delete view.schema[fieldName]
|
delete view.schema[fieldName]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -355,13 +355,11 @@ describe("table sdk", () => {
|
||||||
visible: true,
|
visible: true,
|
||||||
columns: {
|
columns: {
|
||||||
title: {
|
title: {
|
||||||
name: "title",
|
|
||||||
type: "string",
|
type: "string",
|
||||||
visible: true,
|
visible: true,
|
||||||
readonly: true,
|
readonly: true,
|
||||||
},
|
},
|
||||||
age: {
|
age: {
|
||||||
name: "age",
|
|
||||||
type: "number",
|
type: "number",
|
||||||
visible: false,
|
visible: false,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Automation } from "@budibase/types"
|
import { Automation, FetchAutomationResponse } from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
|
||||||
export class AutomationAPI extends TestAPI {
|
export class AutomationAPI extends TestAPI {
|
||||||
|
@ -14,6 +14,26 @@ export class AutomationAPI extends TestAPI {
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch = async (
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<FetchAutomationResponse> => {
|
||||||
|
return await this._get<FetchAutomationResponse>(`/api/automations`, {
|
||||||
|
expectations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchEnriched = async (
|
||||||
|
expectations?: Expectations
|
||||||
|
): Promise<FetchAutomationResponse> => {
|
||||||
|
return await this._get<FetchAutomationResponse>(
|
||||||
|
`/api/automations?enrich=true`,
|
||||||
|
{
|
||||||
|
expectations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
post = async (
|
post = async (
|
||||||
body: Automation,
|
body: Automation,
|
||||||
expectations?: Expectations
|
expectations?: Expectations
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
TRIGGER_DEFINITIONS,
|
TRIGGER_DEFINITIONS,
|
||||||
} from "../../automations"
|
} from "../../automations"
|
||||||
import {
|
import {
|
||||||
|
AIOperationEnum,
|
||||||
Automation,
|
Automation,
|
||||||
AutomationActionStepId,
|
AutomationActionStepId,
|
||||||
AutomationResults,
|
AutomationResults,
|
||||||
|
@ -668,6 +669,12 @@ export function fullSchemaWithoutLinks({
|
||||||
presence: allRequired,
|
presence: allRequired,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[FieldType.AI]: {
|
||||||
|
name: "ai",
|
||||||
|
type: FieldType.AI,
|
||||||
|
operation: AIOperationEnum.PROMPT,
|
||||||
|
prompt: "Translate this into German :'{{ product }}'",
|
||||||
|
},
|
||||||
[FieldType.BARCODEQR]: {
|
[FieldType.BARCODEQR]: {
|
||||||
name: "barcodeqr",
|
name: "barcodeqr",
|
||||||
type: FieldType.BARCODEQR,
|
type: FieldType.BARCODEQR,
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
PROTECTED_EXTERNAL_COLUMNS,
|
PROTECTED_EXTERNAL_COLUMNS,
|
||||||
PROTECTED_INTERNAL_COLUMNS,
|
PROTECTED_INTERNAL_COLUMNS,
|
||||||
} from "@budibase/shared-core"
|
} from "@budibase/shared-core"
|
||||||
import { processString } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import {
|
import {
|
||||||
getTableFromSource,
|
getTableFromSource,
|
||||||
isUserMetadataTable,
|
isUserMetadataTable,
|
||||||
|
@ -134,10 +134,15 @@ async function processDefaultValues(table: Table, row: Row) {
|
||||||
|
|
||||||
for (const [key, schema] of Object.entries(table.schema)) {
|
for (const [key, schema] of Object.entries(table.schema)) {
|
||||||
if ("default" in schema && schema.default != null && row[key] == null) {
|
if ("default" in schema && schema.default != null && row[key] == null) {
|
||||||
const processed =
|
let processed: string | string[]
|
||||||
typeof schema.default === "string"
|
if (Array.isArray(schema.default)) {
|
||||||
? await processString(schema.default, ctx)
|
processed = schema.default.map(val => processStringSync(val, ctx))
|
||||||
: schema.default
|
} else if (typeof schema.default === "string") {
|
||||||
|
processed = processStringSync(schema.default, ctx)
|
||||||
|
} else {
|
||||||
|
processed = schema.default
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
row[key] = coerce(processed, schema.type)
|
row[key] = coerce(processed, schema.type)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -205,6 +210,10 @@ export async function inputProcessing(
|
||||||
if (field.type === FieldType.FORMULA) {
|
if (field.type === FieldType.FORMULA) {
|
||||||
delete clonedRow[key]
|
delete clonedRow[key]
|
||||||
}
|
}
|
||||||
|
// remove any AI values, they are to be generated
|
||||||
|
if (field.type === FieldType.AI) {
|
||||||
|
delete clonedRow[key]
|
||||||
|
}
|
||||||
// otherwise coerce what is there to correct types
|
// otherwise coerce what is there to correct types
|
||||||
else {
|
else {
|
||||||
clonedRow[key] = coerce(value, field.type)
|
clonedRow[key] = coerce(value, field.type)
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
import { fixAutoColumnSubType } from "../utils"
|
import { fixAutoColumnSubType, processAIColumns } from "../utils"
|
||||||
import { AutoFieldDefaultNames } from "../../../constants"
|
import { AutoFieldDefaultNames } from "../../../constants"
|
||||||
import {
|
import {
|
||||||
|
AIOperationEnum,
|
||||||
AutoFieldSubType,
|
AutoFieldSubType,
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
|
Table,
|
||||||
|
TableSourceType,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
|
const buildPromptMock = jest.fn()
|
||||||
|
|
||||||
|
jest.mock("@budibase/pro", () => ({
|
||||||
|
ai: {
|
||||||
|
LargeLanguageModel: {
|
||||||
|
forCurrentTenant: async () => ({
|
||||||
|
run: jest.fn(() => "response from LLM"),
|
||||||
|
buildPromptFromAIOperation: buildPromptMock,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
describe("rowProcessor utility", () => {
|
describe("rowProcessor utility", () => {
|
||||||
describe("fixAutoColumnSubType", () => {
|
describe("fixAutoColumnSubType", () => {
|
||||||
|
@ -60,4 +78,59 @@ describe("rowProcessor utility", () => {
|
||||||
expect(fixAutoColumnSubType(schema)).toEqual(schema)
|
expect(fixAutoColumnSubType(schema)).toEqual(schema)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("processAIColumns", () => {
|
||||||
|
it("ensures that bindable inputs are mapped and passed to to LLM prompt generation", async () => {
|
||||||
|
const table: Table = {
|
||||||
|
_id: generator.guid(),
|
||||||
|
name: "AITestTable",
|
||||||
|
type: "table",
|
||||||
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
sourceType: TableSourceType.INTERNAL,
|
||||||
|
schema: {
|
||||||
|
product: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "product",
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aicol: {
|
||||||
|
type: FieldType.AI,
|
||||||
|
name: "aicol",
|
||||||
|
operation: AIOperationEnum.PROMPT,
|
||||||
|
prompt: "Translate '{{ product }}' into German",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputRows = [
|
||||||
|
{
|
||||||
|
product: "Car Battery",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = await processAIColumns(table, inputRows, {
|
||||||
|
contextRows: inputRows,
|
||||||
|
})
|
||||||
|
expect(buildPromptMock).toHaveBeenCalledWith({
|
||||||
|
row: {
|
||||||
|
product: "Car Battery",
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
name: "aicol",
|
||||||
|
operation: "PROMPT",
|
||||||
|
prompt: "Translate 'Car Battery' into German",
|
||||||
|
type: "ai",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
aicol: "response from LLM",
|
||||||
|
product: "Car Battery",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,9 +8,13 @@ import {
|
||||||
FormulaType,
|
FormulaType,
|
||||||
AutoFieldSubType,
|
AutoFieldSubType,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
OperationFieldTypeEnum,
|
||||||
|
AIOperationEnum,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { OperationFields } from "@budibase/shared-core"
|
||||||
import tracer from "dd-trace"
|
import tracer from "dd-trace"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
|
import * as pro from "@budibase/pro"
|
||||||
|
|
||||||
interface FormulaOpts {
|
interface FormulaOpts {
|
||||||
dynamic?: boolean
|
dynamic?: boolean
|
||||||
|
@ -91,6 +95,66 @@ export async function processFormulas<T extends Row | Row[]>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks through the rows provided and finds AI columns - which it then processes.
|
||||||
|
*/
|
||||||
|
export async function processAIColumns<T extends Row | Row[]>(
|
||||||
|
table: Table,
|
||||||
|
inputRows: T,
|
||||||
|
{ contextRows }: FormulaOpts
|
||||||
|
): Promise<T> {
|
||||||
|
return tracer.trace("processAIColumns", {}, async span => {
|
||||||
|
const numRows = Array.isArray(inputRows) ? inputRows.length : 1
|
||||||
|
span?.addTags({ table_id: table._id, numRows })
|
||||||
|
const rows = Array.isArray(inputRows) ? inputRows : [inputRows]
|
||||||
|
const llm = await pro.ai.LargeLanguageModel.forCurrentTenant("gpt-4o-mini")
|
||||||
|
if (rows) {
|
||||||
|
// Ensure we have snippet context
|
||||||
|
await context.ensureSnippetContext()
|
||||||
|
|
||||||
|
for (let [column, schema] of Object.entries(table.schema)) {
|
||||||
|
if (schema.type !== FieldType.AI) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowUpdates = rows.map((row, i) => {
|
||||||
|
const contextRow = contextRows ? contextRows[i] : row
|
||||||
|
|
||||||
|
// Check if the type is bindable and pass through HBS if so
|
||||||
|
const operationField =
|
||||||
|
OperationFields[schema.operation as AIOperationEnum]
|
||||||
|
for (const key in schema) {
|
||||||
|
const fieldType = operationField[key as keyof typeof operationField]
|
||||||
|
if (fieldType === OperationFieldTypeEnum.BINDABLE_TEXT) {
|
||||||
|
// @ts-ignore
|
||||||
|
schema[key] = processStringSync(schema[key], contextRow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = llm.buildPromptFromAIOperation({ schema, row })
|
||||||
|
|
||||||
|
return tracer.trace("processAIColumn", {}, async span => {
|
||||||
|
span?.addTags({ table_id: table._id, column })
|
||||||
|
const llmResponse = await llm.run(prompt!)
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
[column]: llmResponse,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const processedRows = await Promise.all(rowUpdates)
|
||||||
|
|
||||||
|
// Promise.all is deterministic so can rely on the indexing here
|
||||||
|
processedRows.forEach(
|
||||||
|
(processedRow, index) => (rows[index] = processedRow)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.isArray(inputRows) ? rows : rows[0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes any date columns and ensures that those without the ignoreTimezones
|
* Processes any date columns and ensures that those without the ignoreTimezones
|
||||||
* flag set are parsed as UTC rather than local time.
|
* flag set are parsed as UTC rather than local time.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { permissions, roles } from "@budibase/backend-core"
|
import { DocumentType, permissions, roles } from "@budibase/backend-core"
|
||||||
import { DocumentType, VirtualDocumentType } from "../db/utils"
|
import { VirtualDocumentType } from "../db/utils"
|
||||||
import { getDocumentType, getVirtualDocumentType } from "@budibase/types"
|
import { getDocumentType, getVirtualDocumentType } from "@budibase/types"
|
||||||
|
|
||||||
export const CURRENTLY_SUPPORTED_LEVELS: string[] = [
|
export const CURRENTLY_SUPPORTED_LEVELS: string[] = [
|
||||||
|
@ -19,6 +19,7 @@ export function getPermissionType(resourceId: string) {
|
||||||
switch (docType) {
|
switch (docType) {
|
||||||
case DocumentType.TABLE:
|
case DocumentType.TABLE:
|
||||||
case DocumentType.ROW:
|
case DocumentType.ROW:
|
||||||
|
case DocumentType.DATASOURCE_PLUS:
|
||||||
return permissions.PermissionType.TABLE
|
return permissions.PermissionType.TABLE
|
||||||
case DocumentType.AUTOMATION:
|
case DocumentType.AUTOMATION:
|
||||||
return permissions.PermissionType.AUTOMATION
|
return permissions.PermissionType.AUTOMATION
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
import {
|
||||||
|
AIOperationEnum,
|
||||||
|
OperationFieldsType,
|
||||||
|
OperationFieldTypeEnum,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
export const AIOperations = {
|
||||||
|
SUMMARISE_TEXT: {
|
||||||
|
label: "Summarise Text",
|
||||||
|
value: "SUMMARISE_TEXT",
|
||||||
|
},
|
||||||
|
CLEAN_DATA: {
|
||||||
|
label: "Clean Data",
|
||||||
|
value: "CLEAN_DATA",
|
||||||
|
},
|
||||||
|
TRANSLATE: {
|
||||||
|
label: "Translate",
|
||||||
|
value: "TRANSLATE",
|
||||||
|
},
|
||||||
|
CATEGORISE_TEXT: {
|
||||||
|
label: "Categorise Text",
|
||||||
|
value: "CATEGORISE_TEXT",
|
||||||
|
},
|
||||||
|
SENTIMENT_ANALYSIS: {
|
||||||
|
label: "Sentiment Analysis",
|
||||||
|
value: "SENTIMENT_ANALYSIS",
|
||||||
|
},
|
||||||
|
PROMPT: {
|
||||||
|
label: "Prompt",
|
||||||
|
value: "PROMPT",
|
||||||
|
},
|
||||||
|
SEARCH_WEB: {
|
||||||
|
label: "Search Web",
|
||||||
|
value: "SEARCH_WEB",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OperationFieldTypes = {
|
||||||
|
MULTI_COLUMN: "columns",
|
||||||
|
COLUMN: "column",
|
||||||
|
BINDABLE_TEXT: "prompt",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OperationFields: OperationFieldsType = {
|
||||||
|
[AIOperationEnum.SUMMARISE_TEXT]: {
|
||||||
|
columns: OperationFieldTypeEnum.MULTI_COLUMN,
|
||||||
|
},
|
||||||
|
[AIOperationEnum.CLEAN_DATA]: {
|
||||||
|
column: OperationFieldTypeEnum.COLUMN,
|
||||||
|
},
|
||||||
|
[AIOperationEnum.TRANSLATE]: {
|
||||||
|
column: OperationFieldTypeEnum.COLUMN,
|
||||||
|
language: OperationFieldTypeEnum.BINDABLE_TEXT,
|
||||||
|
},
|
||||||
|
[AIOperationEnum.CATEGORISE_TEXT]: {
|
||||||
|
columns: OperationFieldTypeEnum.MULTI_COLUMN,
|
||||||
|
categories: OperationFieldTypeEnum.BINDABLE_TEXT,
|
||||||
|
},
|
||||||
|
[AIOperationEnum.SENTIMENT_ANALYSIS]: {
|
||||||
|
column: OperationFieldTypeEnum.COLUMN,
|
||||||
|
},
|
||||||
|
[AIOperationEnum.PROMPT]: {
|
||||||
|
prompt: OperationFieldTypeEnum.BINDABLE_TEXT,
|
||||||
|
},
|
||||||
|
[AIOperationEnum.SEARCH_WEB]: {
|
||||||
|
columns: OperationFieldTypeEnum.MULTI_COLUMN,
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from "./ai"
|
||||||
export * from "./api"
|
export * from "./api"
|
||||||
export * from "./fields"
|
export * from "./fields"
|
||||||
export * from "./rows"
|
export * from "./rows"
|
||||||
|
|
|
@ -90,6 +90,8 @@ export const getValidOperatorsForType = (
|
||||||
ops = numOps
|
ops = numOps
|
||||||
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
} else if (type === FieldType.FORMULA && formulaType === FormulaType.STATIC) {
|
||||||
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||||
|
} else if (type === FieldType.AI) {
|
||||||
|
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||||
} else if (
|
} else if (
|
||||||
type === FieldType.BB_REFERENCE_SINGLE ||
|
type === FieldType.BB_REFERENCE_SINGLE ||
|
||||||
schema.isDeprecatedSingleUserColumn(fieldType)
|
schema.isDeprecatedSingleUserColumn(fieldType)
|
||||||
|
|
|
@ -33,6 +33,13 @@ export function calculationFields(view: UnsavedViewV2) {
|
||||||
return pickBy(view.schema || {}, isCalculationField)
|
return pickBy(view.schema || {}, isCalculationField)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function basicFields(view: UnsavedViewV2) {
|
export function isVisible(field: ViewFieldMetadata) {
|
||||||
return pickBy(view.schema || {}, field => !isCalculationField(field))
|
return field.visible !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function basicFields(view: UnsavedViewV2, opts?: { visible?: boolean }) {
|
||||||
|
const { visible = true } = opts || {}
|
||||||
|
return pickBy(view.schema || {}, field => {
|
||||||
|
return !isCalculationField(field) && (!visible || isVisible(field))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.NUMBER]: true,
|
[FieldType.NUMBER]: true,
|
||||||
[FieldType.DATETIME]: true,
|
[FieldType.DATETIME]: true,
|
||||||
[FieldType.FORMULA]: true,
|
[FieldType.FORMULA]: true,
|
||||||
|
[FieldType.AI]: true,
|
||||||
[FieldType.AUTO]: true,
|
[FieldType.AUTO]: true,
|
||||||
[FieldType.INTERNAL]: true,
|
[FieldType.INTERNAL]: true,
|
||||||
[FieldType.BARCODEQR]: true,
|
[FieldType.BARCODEQR]: true,
|
||||||
|
@ -38,6 +39,7 @@ const allowSortColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.JSON]: true,
|
[FieldType.JSON]: true,
|
||||||
|
|
||||||
[FieldType.FORMULA]: false,
|
[FieldType.FORMULA]: false,
|
||||||
|
[FieldType.AI]: false,
|
||||||
[FieldType.ATTACHMENTS]: false,
|
[FieldType.ATTACHMENTS]: false,
|
||||||
[FieldType.ATTACHMENT_SINGLE]: false,
|
[FieldType.ATTACHMENT_SINGLE]: false,
|
||||||
[FieldType.SIGNATURE_SINGLE]: false,
|
[FieldType.SIGNATURE_SINGLE]: false,
|
||||||
|
@ -62,12 +64,13 @@ const allowDefaultColumnByType: Record<FieldType, boolean> = {
|
||||||
[FieldType.BIGINT]: false,
|
[FieldType.BIGINT]: false,
|
||||||
[FieldType.BOOLEAN]: false,
|
[FieldType.BOOLEAN]: false,
|
||||||
[FieldType.FORMULA]: false,
|
[FieldType.FORMULA]: false,
|
||||||
|
[FieldType.AI]: false,
|
||||||
[FieldType.ATTACHMENTS]: false,
|
[FieldType.ATTACHMENTS]: false,
|
||||||
[FieldType.ATTACHMENT_SINGLE]: false,
|
[FieldType.ATTACHMENT_SINGLE]: false,
|
||||||
[FieldType.SIGNATURE_SINGLE]: false,
|
[FieldType.SIGNATURE_SINGLE]: false,
|
||||||
[FieldType.LINK]: false,
|
[FieldType.LINK]: false,
|
||||||
[FieldType.BB_REFERENCE]: false,
|
[FieldType.BB_REFERENCE]: true,
|
||||||
[FieldType.BB_REFERENCE_SINGLE]: false,
|
[FieldType.BB_REFERENCE_SINGLE]: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canBeDisplayColumn(type: FieldType): boolean {
|
export function canBeDisplayColumn(type: FieldType): boolean {
|
||||||
|
|
|
@ -172,7 +172,7 @@ export const processSearchFilters = (
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.localeCompare(b)
|
return a.localeCompare(b)
|
||||||
})
|
})
|
||||||
.filter(key => key in filter)
|
.filter(key => filter[key])
|
||||||
|
|
||||||
if (filterPropertyKeys.length == 1) {
|
if (filterPropertyKeys.length == 1) {
|
||||||
const key = filterPropertyKeys[0],
|
const key = filterPropertyKeys[0],
|
||||||
|
|
|
@ -76,6 +76,13 @@ export enum FieldType {
|
||||||
* that is part of the initial formula definition, the formula will be live evaluated in the browser.
|
* that is part of the initial formula definition, the formula will be live evaluated in the browser.
|
||||||
*/
|
*/
|
||||||
AUTO = "auto",
|
AUTO = "auto",
|
||||||
|
/**
|
||||||
|
* A complex type, called an AI column within Budibase. This type is only supported against internal tables
|
||||||
|
* and calculates the output based on a chosen operation (summarise text, translation etc) which passes to
|
||||||
|
* the configured Budibase Large Language Model to retrieve the output and write it back into the row.
|
||||||
|
* AI fields function in a similar fashion to static formulas, and possess many of the same characteristics.
|
||||||
|
*/
|
||||||
|
AI = "ai",
|
||||||
/**
|
/**
|
||||||
* a JSON type, called JSON within Budibase. This type allows any arbitrary JSON to be input to this column
|
* a JSON type, called JSON within Budibase. This type allows any arbitrary JSON to be input to this column
|
||||||
* type, which will be represented as a JSON object in the row. This type depends on a schema being
|
* type, which will be represented as a JSON object in the row. This type depends on a schema being
|
||||||
|
|
|
@ -30,6 +30,7 @@ export enum JsonFieldSubType {
|
||||||
export enum FormulaType {
|
export enum FormulaType {
|
||||||
STATIC = "static",
|
STATIC = "static",
|
||||||
DYNAMIC = "dynamic",
|
DYNAMIC = "dynamic",
|
||||||
|
AI = "ai",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BBReferenceFieldSubType {
|
export enum BBReferenceFieldSubType {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
JsonFieldSubType,
|
JsonFieldSubType,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
} from "./constants"
|
} from "./constants"
|
||||||
|
import { AIOperationEnum } from "../../../sdk/ai"
|
||||||
|
|
||||||
export interface UIFieldMetadata {
|
export interface UIFieldMetadata {
|
||||||
order?: number
|
order?: number
|
||||||
|
@ -116,16 +117,28 @@ export interface FormulaFieldMetadata extends BaseFieldSchema {
|
||||||
formulaType?: FormulaType
|
formulaType?: FormulaType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AIFieldMetadata extends BaseFieldSchema {
|
||||||
|
type: FieldType.AI
|
||||||
|
operation: AIOperationEnum
|
||||||
|
columns?: string[]
|
||||||
|
column?: string
|
||||||
|
categories?: string[]
|
||||||
|
prompt?: string
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface BBReferenceFieldMetadata
|
export interface BBReferenceFieldMetadata
|
||||||
extends Omit<BaseFieldSchema, "subtype"> {
|
extends Omit<BaseFieldSchema, "subtype"> {
|
||||||
type: FieldType.BB_REFERENCE
|
type: FieldType.BB_REFERENCE
|
||||||
subtype: BBReferenceFieldSubType
|
subtype: BBReferenceFieldSubType
|
||||||
relationshipType?: RelationshipType
|
relationshipType?: RelationshipType
|
||||||
|
default?: string[]
|
||||||
}
|
}
|
||||||
export interface BBReferenceSingleFieldMetadata
|
export interface BBReferenceSingleFieldMetadata
|
||||||
extends Omit<BaseFieldSchema, "subtype"> {
|
extends Omit<BaseFieldSchema, "subtype"> {
|
||||||
type: FieldType.BB_REFERENCE_SINGLE
|
type: FieldType.BB_REFERENCE_SINGLE
|
||||||
subtype: Exclude<BBReferenceFieldSubType, BBReferenceFieldSubType.USERS>
|
subtype: Exclude<BBReferenceFieldSubType, BBReferenceFieldSubType.USERS>
|
||||||
|
default?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttachmentFieldMetadata extends BaseFieldSchema {
|
export interface AttachmentFieldMetadata extends BaseFieldSchema {
|
||||||
|
@ -192,6 +205,7 @@ interface OtherFieldMetadata extends BaseFieldSchema {
|
||||||
| FieldType.LINK
|
| FieldType.LINK
|
||||||
| FieldType.AUTO
|
| FieldType.AUTO
|
||||||
| FieldType.FORMULA
|
| FieldType.FORMULA
|
||||||
|
| FieldType.AI
|
||||||
| FieldType.NUMBER
|
| FieldType.NUMBER
|
||||||
| FieldType.LONGFORM
|
| FieldType.LONGFORM
|
||||||
| FieldType.BB_REFERENCE
|
| FieldType.BB_REFERENCE
|
||||||
|
@ -209,6 +223,7 @@ export type FieldSchema =
|
||||||
| RelationshipFieldMetadata
|
| RelationshipFieldMetadata
|
||||||
| AutoColumnFieldMetadata
|
| AutoColumnFieldMetadata
|
||||||
| FormulaFieldMetadata
|
| FormulaFieldMetadata
|
||||||
|
| AIFieldMetadata
|
||||||
| NumberFieldMetadata
|
| NumberFieldMetadata
|
||||||
| LongFormFieldMetadata
|
| LongFormFieldMetadata
|
||||||
| StringFieldMetadata
|
| StringFieldMetadata
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
export enum AIOperationEnum {
|
||||||
|
SUMMARISE_TEXT = "SUMMARISE_TEXT",
|
||||||
|
CLEAN_DATA = "CLEAN_DATA",
|
||||||
|
TRANSLATE = "TRANSLATE",
|
||||||
|
CATEGORISE_TEXT = "CATEGORISE_TEXT",
|
||||||
|
SENTIMENT_ANALYSIS = "SENTIMENT_ANALYSIS",
|
||||||
|
PROMPT = "PROMPT",
|
||||||
|
SEARCH_WEB = "SEARCH_WEB",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OperationFieldTypeEnum {
|
||||||
|
MULTI_COLUMN = "columns",
|
||||||
|
COLUMN = "column",
|
||||||
|
BINDABLE_TEXT = "prompt",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OperationFieldsType = {
|
||||||
|
[AIOperationEnum.SUMMARISE_TEXT]: {
|
||||||
|
columns: OperationFieldTypeEnum.MULTI_COLUMN
|
||||||
|
}
|
||||||
|
[AIOperationEnum.CLEAN_DATA]: {
|
||||||
|
column: OperationFieldTypeEnum.COLUMN
|
||||||
|
}
|
||||||
|
[AIOperationEnum.TRANSLATE]: {
|
||||||
|
column: OperationFieldTypeEnum.COLUMN
|
||||||
|
language: OperationFieldTypeEnum.BINDABLE_TEXT
|
||||||
|
}
|
||||||
|
[AIOperationEnum.CATEGORISE_TEXT]: {
|
||||||
|
columns: OperationFieldTypeEnum.MULTI_COLUMN
|
||||||
|
categories: OperationFieldTypeEnum.BINDABLE_TEXT
|
||||||
|
}
|
||||||
|
[AIOperationEnum.SENTIMENT_ANALYSIS]: {
|
||||||
|
column: OperationFieldTypeEnum.COLUMN
|
||||||
|
}
|
||||||
|
[AIOperationEnum.PROMPT]: {
|
||||||
|
prompt: OperationFieldTypeEnum.BINDABLE_TEXT
|
||||||
|
}
|
||||||
|
[AIOperationEnum.SEARCH_WEB]: {
|
||||||
|
columns: OperationFieldTypeEnum.MULTI_COLUMN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseSchema = {
|
||||||
|
operation: AIOperationEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
type SummariseTextSchema = BaseSchema & {
|
||||||
|
operation: AIOperationEnum.SUMMARISE_TEXT
|
||||||
|
columns: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type CleanDataSchema = BaseSchema & {
|
||||||
|
operation: AIOperationEnum.CLEAN_DATA
|
||||||
|
column: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TranslateSchema = BaseSchema & {
|
||||||
|
operation: AIOperationEnum.TRANSLATE
|
||||||
|
column: string
|
||||||
|
language: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoriseTextSchema = BaseSchema & {
|
||||||
|
operation: AIOperationEnum.CATEGORISE_TEXT
|
||||||
|
columns: string[]
|
||||||
|
categories: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SentimentAnalysisSchema = BaseSchema & {
|
||||||
|
operation: AIOperationEnum.SENTIMENT_ANALYSIS
|
||||||
|
column: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PromptSchema = BaseSchema & {
|
||||||
|
operation: AIOperationEnum.PROMPT
|
||||||
|
prompt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchWebSchema = BaseSchema & {
|
||||||
|
operation: AIOperationEnum.SEARCH_WEB
|
||||||
|
columns: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AIColumnSchema =
|
||||||
|
| SummariseTextSchema
|
||||||
|
| CleanDataSchema
|
||||||
|
| TranslateSchema
|
||||||
|
| CategoriseTextSchema
|
||||||
|
| SentimentAnalysisSchema
|
||||||
|
| PromptSchema
|
||||||
|
| SearchWebSchema
|
|
@ -129,7 +129,12 @@ export interface Database {
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
exists(): Promise<boolean>
|
exists(): Promise<boolean>
|
||||||
|
/**
|
||||||
|
* @deprecated the plan is to get everything using `tryGet` instead, then rename
|
||||||
|
* `tryGet` to `get`.
|
||||||
|
*/
|
||||||
get<T extends Document>(id?: string): Promise<T>
|
get<T extends Document>(id?: string): Promise<T>
|
||||||
|
tryGet<T extends Document>(id?: string): Promise<T | undefined>
|
||||||
exists(docId: string): Promise<boolean>
|
exists(docId: string): Promise<boolean>
|
||||||
getMultiple<T extends Document>(
|
getMultiple<T extends Document>(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
|
|
|
@ -3,6 +3,7 @@ export enum FeatureFlag {
|
||||||
PER_CREATOR_PER_USER_PRICE_ALERT = "PER_CREATOR_PER_USER_PRICE_ALERT",
|
PER_CREATOR_PER_USER_PRICE_ALERT = "PER_CREATOR_PER_USER_PRICE_ALERT",
|
||||||
AI_CUSTOM_CONFIGS = "AI_CUSTOM_CONFIGS",
|
AI_CUSTOM_CONFIGS = "AI_CUSTOM_CONFIGS",
|
||||||
ENRICHED_RELATIONSHIPS = "ENRICHED_RELATIONSHIPS",
|
ENRICHED_RELATIONSHIPS = "ENRICHED_RELATIONSHIPS",
|
||||||
|
TABLES_DEFAULT_ADMIN = "TABLES_DEFAULT_ADMIN",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantFeatureFlags {
|
export interface TenantFeatureFlags {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from "./ai"
|
||||||
export * from "./automations"
|
export * from "./automations"
|
||||||
export * from "./hosting"
|
export * from "./hosting"
|
||||||
export * from "./context"
|
export * from "./context"
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { FieldSchema, RelationSchemaField, ViewV2 } from "../documents"
|
import {
|
||||||
|
FieldSchema,
|
||||||
|
FieldSubType,
|
||||||
|
FieldType,
|
||||||
|
RelationSchemaField,
|
||||||
|
ViewV2,
|
||||||
|
} from "../documents"
|
||||||
|
|
||||||
export interface ViewV2Enriched extends ViewV2 {
|
export interface ViewV2Enriched extends ViewV2 {
|
||||||
schema?: {
|
schema?: {
|
||||||
|
@ -8,4 +14,7 @@ export interface ViewV2Enriched extends ViewV2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewV2ColumnEnriched = RelationSchemaField & FieldSchema
|
export interface ViewV2ColumnEnriched extends RelationSchemaField {
|
||||||
|
type: FieldType
|
||||||
|
subtype?: FieldSubType
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue