diff --git a/lerna.json b/lerna.json
index 87f0731cda..4b258aad77 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "3.9.5",
+ "version": "3.10.1",
"npmClient": "yarn",
"concurrency": 20,
"command": {
diff --git a/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte
index f5189c9edd..6c8c367b31 100644
--- a/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte
+++ b/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte
@@ -13,7 +13,7 @@
export let readonly = false
export let error = null
export let enableTime = true
- export let value = null
+ export let value = undefined
export let placeholder = null
export let timeOnly = false
export let ignoreTimezones = false
diff --git a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte
index 84b55c403f..794984c4b6 100644
--- a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte
+++ b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte
@@ -7,6 +7,8 @@
export let type = "number"
$: style = width ? `width:${width}px;` : ""
+
+ const selectAll = event => event.target.select()
diff --git a/packages/bbui/src/Form/Core/DateRangePicker.svelte b/packages/bbui/src/Form/Core/DateRangePicker.svelte
index 72180a98d6..af4f67679b 100644
--- a/packages/bbui/src/Form/Core/DateRangePicker.svelte
+++ b/packages/bbui/src/Form/Core/DateRangePicker.svelte
@@ -1,24 +1,87 @@
-
diff --git a/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte b/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte
index 0ba7de42c2..f1f7005800 100644
--- a/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte
+++ b/packages/builder/src/components/design/settings/controls/DraggableList/DraggableList.svelte
@@ -11,7 +11,7 @@
export let listTypeProps = {}
export let listItemKey
export let draggable = true
- export let focus
+ export let focus = undefined
let zoneType = generate()
diff --git a/packages/builder/src/components/design/settings/controls/FilterConfiguration/EditFilterPopover.svelte b/packages/builder/src/components/design/settings/controls/FilterConfiguration/EditFilterPopover.svelte
new file mode 100644
index 0000000000..e0afb94123
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/FilterConfiguration/EditFilterPopover.svelte
@@ -0,0 +1,127 @@
+
+
+
+
+
0}
+ maxHeight={600}
+ minWidth={360}
+ maxWidth={360}
+ offset={18}
+>
+
+
+
+ {
+ drawers = [...drawers, e.detail]
+ }}
+ on:drawerHide={() => {
+ drawers = drawers.slice(0, -1)
+ }}
+ />
+
+
+
+
+
diff --git a/packages/builder/src/components/design/settings/controls/FilterConfiguration/FilterConfiguration.svelte b/packages/builder/src/components/design/settings/controls/FilterConfiguration/FilterConfiguration.svelte
new file mode 100644
index 0000000000..5d6202b32c
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/FilterConfiguration/FilterConfiguration.svelte
@@ -0,0 +1,186 @@
+
+
+
+
+ Fields
+ {
+ selectedAll = !selectedAll
+ let update = parsedColumns.map(field => {
+ return {
+ ...field,
+ active: selectedAll,
+ }
+ })
+ listUpdate(update)
+ }}
+ value={selectedAll}
+ text=""
+ />
+
+
+ {#if parsedColumns?.length}
+
{
+ listUpdate(e.detail)
+ }}
+ on:itemChange={itemUpdate}
+ items={parsedColumns || []}
+ listItemKey={"field"}
+ listType={FilterSetting}
+ listTypeProps={{
+ bindings,
+ }}
+ />
+ {:else}
+
+ {/if}
+
+
+
diff --git a/packages/builder/src/components/design/settings/controls/FilterConfiguration/FilterSetting.svelte b/packages/builder/src/components/design/settings/controls/FilterConfiguration/FilterSetting.svelte
new file mode 100644
index 0000000000..626576b5db
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/FilterConfiguration/FilterSetting.svelte
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+ {item.field}
+
+
+
{item.label || item.field}
+
+
+ {
+ e.stopPropagation()
+ }}
+ on:change={onToggle(item)}
+ text=""
+ value={item.active}
+ thin
+ />
+
+
+
+
diff --git a/packages/builder/src/components/design/settings/controls/FilterableSelect.svelte b/packages/builder/src/components/design/settings/controls/FilterableSelect.svelte
new file mode 100644
index 0000000000..6904cc8435
--- /dev/null
+++ b/packages/builder/src/components/design/settings/controls/FilterableSelect.svelte
@@ -0,0 +1,36 @@
+
+
+
diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte
index fb3856d517..7dd8c88a91 100644
--- a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte
+++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/GridColumnConfiguration.svelte
@@ -9,7 +9,7 @@
import { createEventDispatcher } from "svelte"
import FieldSetting from "./FieldSetting.svelte"
import PrimaryColumnFieldSetting from "./PrimaryColumnFieldSetting.svelte"
- import getColumns from "./getColumns.js"
+ import { getColumns } from "./getColumns.js"
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
export let value
diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js
index 638c41e0ec..7a6e697952 100644
--- a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js
+++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.js
@@ -1,4 +1,4 @@
-const modernize = columns => {
+export const modernize = columns => {
if (!columns) {
return []
}
@@ -14,9 +14,9 @@ const modernize = columns => {
return columns
}
-const removeInvalidAddMissing = (
+export const removeInvalidAddMissing = (
columns = [],
- defaultColumns,
+ defaultColumns = [],
primaryDisplayColumnName
) => {
const defaultColumnNames = defaultColumns.map(column => column.field)
@@ -47,7 +47,7 @@ const removeInvalidAddMissing = (
return combinedColumns
}
-const getDefault = (schema = {}) => {
+export const getDefault = (schema = {}) => {
const defaultValues = Object.values(schema)
.filter(column => !column.nestedJSON)
.map(column => ({
@@ -93,7 +93,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
})
}
-const getColumns = ({
+export const getColumns = ({
columns,
schema,
primaryDisplayColumnName,
@@ -132,5 +132,3 @@ const getColumns = ({
},
}
}
-
-export default getColumns
diff --git a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js
index 1d5d2feaa9..0911649170 100644
--- a/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js
+++ b/packages/builder/src/components/design/settings/controls/GridColumnConfiguration/getColumns.test.js
@@ -1,5 +1,5 @@
import { it, expect, describe, beforeEach, vi } from "vitest"
-import getColumns from "./getColumns"
+import { getColumns } from "./getColumns"
describe("getColumns", () => {
beforeEach(ctx => {
diff --git a/packages/builder/src/components/integration/rest/AuthPicker.svelte b/packages/builder/src/components/integration/rest/AuthPicker.svelte
index fd7773a7d2..5f30019564 100644
--- a/packages/builder/src/components/integration/rest/AuthPicker.svelte
+++ b/packages/builder/src/components/integration/rest/AuthPicker.svelte
@@ -109,13 +109,12 @@
/>
{/each}
-
-
-
-
{/if}
+
+
+
diff --git a/packages/client/src/components/app/filter/FilterButton.svelte b/packages/client/src/components/app/filter/FilterButton.svelte
new file mode 100644
index 0000000000..02789af923
--- /dev/null
+++ b/packages/client/src/components/app/filter/FilterButton.svelte
@@ -0,0 +1,250 @@
+
+
+
+
+
+
+
diff --git a/packages/client/src/components/app/filter/FilterPopover.svelte b/packages/client/src/components/app/filter/FilterPopover.svelte
new file mode 100644
index 0000000000..8a578cf2b0
--- /dev/null
+++ b/packages/client/src/components/app/filter/FilterPopover.svelte
@@ -0,0 +1,387 @@
+
+
+
+
+
+
+
+
+
+
+
+ {#if editableFilter && fieldSchema}
+
+
+
+ {#if editableFilter?.type && [FieldType.STRING, FieldType.LONGFORM, FieldType.NUMBER, FieldType.BIGINT, FieldType.FORMULA, FieldType.AI].includes(editableFilter.type)}
+
{
+ if (!editableFilter) return
+ editableFilter = sanitizeOperator({
+ ...editableFilter,
+ value: e.detail,
+ })
+ }}
+ />
+ {:else if (editableFilter?.type && editableFilter?.type === FieldType.ARRAY) || (editableFilter.type === FieldType.OPTIONS && editableFilter.operator === ArrayOperator.ONE_OF)}
+ {@const isMulti = isArrayOperator(editableFilter.operator)}
+ {@const type = isMulti ? CoreCheckboxGroup : CoreRadioGroup}
+ {#key type}
+
{
+ if (!editableFilter) return
+ editableFilter = sanitizeOperator({
+ ...editableFilter,
+ value: e.detail,
+ })
+ }}
+ />
+ {/key}
+ {:else if editableFilter.type === FieldType.OPTIONS}
+ {
+ if (!editableFilter) return
+ editableFilter = sanitizeOperator({
+ ...editableFilter,
+ value: e.detail,
+ })
+ }}
+ />
+ {:else if editableFilter.type === FieldType.DATETIME && editableFilter.operator === "range"}
+ {
+ const [from, to] = e.detail
+ const parsedFrom = enableTime
+ ? from.utc().format()
+ : Helpers.stringifyDate(from, {
+ enableTime,
+ timeOnly,
+ ignoreTimezones,
+ })
+
+ const parsedTo = enableTime
+ ? to.utc().format()
+ : Helpers.stringifyDate(to, {
+ enableTime,
+ timeOnly,
+ ignoreTimezones,
+ })
+
+ if (!editableFilter) return
+ editableFilter = sanitizeOperator({
+ ...editableFilter,
+ value: {
+ low: parsedFrom,
+ high: parsedTo,
+ },
+ })
+ }}
+ />
+ {:else if editableFilter.type === FieldType.DATETIME}
+ {
+ if (!editableFilter) return
+ editableFilter = sanitizeOperator({
+ ...editableFilter,
+ value: e.detail,
+ })
+ }}
+ />
+ {:else if editableFilter.type === FieldType.BOOLEAN}
+
+
+
+
+
diff --git a/packages/client/src/components/app/forms/BBReferenceField.svelte b/packages/client/src/components/app/forms/BBReferenceField.svelte
index 60932ecf56..7dc220e76a 100644
--- a/packages/client/src/components/app/forms/BBReferenceField.svelte
+++ b/packages/client/src/components/app/forms/BBReferenceField.svelte
@@ -1,10 +1,12 @@
{#if user}
diff --git a/packages/frontend-core/src/components/UserAvatars.svelte b/packages/frontend-core/src/components/UserAvatars.svelte
index 2399c659cc..eaf4727811 100644
--- a/packages/frontend-core/src/components/UserAvatars.svelte
+++ b/packages/frontend-core/src/components/UserAvatars.svelte
@@ -1,27 +1,31 @@
-
{#each avatars as user}
- {#if user._id === "overflow"}
+ {#if !isUser(user)}
row.doc!)
}
-async function getScreens() {
- const db = context.getAppDB()
- return (
- await db.allDocs(
- getScreenParams(null, {
- include_docs: true,
- })
- )
- ).rows.map(row => row.doc!)
-}
-
function getUserRoleId(ctx: UserCtx) {
return !ctx.user?.role || !ctx.user.role._id
? roles.BUILTIN_ROLE_IDS.PUBLIC
@@ -241,7 +229,7 @@ export async function fetchAppDefinition(
const userRoleId = getUserRoleId(ctx)
const accessController = new roles.AccessController()
const screens = await accessController.checkScreensAccess(
- await getScreens(),
+ await sdk.screens.fetch(),
userRoleId
)
ctx.body = {
@@ -257,7 +245,7 @@ export async function fetchAppPackage(
const appId = context.getAppId()
const application = await sdk.applications.metadata.get()
const layouts = await getLayouts()
- let screens = await getScreens()
+ let screens = await sdk.screens.fetch()
const license = await licensing.cache.getCachedLicense()
// Enrich plugin URLs
@@ -915,7 +903,7 @@ async function migrateAppNavigation() {
const db = context.getAppDB()
const existing = await sdk.applications.metadata.get()
const layouts: Layout[] = await getLayouts()
- const screens: Screen[] = await getScreens()
+ const screens: Screen[] = await sdk.screens.fetch()
// Migrate all screens, removing custom layouts
for (let screen of screens) {
diff --git a/packages/server/src/api/controllers/layout.ts b/packages/server/src/api/controllers/layout.ts
index 4c996dde35..c23d8c4e2f 100644
--- a/packages/server/src/api/controllers/layout.ts
+++ b/packages/server/src/api/controllers/layout.ts
@@ -1,13 +1,13 @@
import { EMPTY_LAYOUT } from "../../constants/layouts"
-import { generateLayoutID, getScreenParams } from "../../db/utils"
+import { generateLayoutID } from "../../db/utils"
import { events, context } from "@budibase/backend-core"
import {
DeleteLayoutResponse,
- Layout,
SaveLayoutRequest,
SaveLayoutResponse,
UserCtx,
} from "@budibase/types"
+import sdk from "../../sdk"
export async function save(
ctx: UserCtx
@@ -36,13 +36,9 @@ export async function destroy(ctx: UserCtx) {
const layoutId = ctx.params.layoutId,
layoutRev = ctx.params.layoutRev
- const layoutsUsedByScreens = (
- await db.allDocs(
- getScreenParams(null, {
- include_docs: true,
- })
- )
- ).rows.map(element => element.doc!.layoutId)
+ const layoutsUsedByScreens = (await sdk.screens.fetch()).map(
+ element => element.layoutId
+ )
if (layoutsUsedByScreens.includes(layoutId)) {
ctx.throw(400, "Cannot delete a layout that's being used by a screen")
}
diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts
index 3fb882ff2f..829b7f56d2 100644
--- a/packages/server/src/api/routes/tests/row.spec.ts
+++ b/packages/server/src/api/routes/tests/row.spec.ts
@@ -918,7 +918,9 @@ if (descriptions.length) {
describe("get", () => {
it("reads an existing row successfully", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
const res = await config.api.row.get(table._id!, existing._id!)
@@ -930,7 +932,7 @@ if (descriptions.length) {
it("returns 404 when row does not exist", async () => {
const table = await config.api.table.save(defaultTable())
- await config.api.row.save(table._id!, {})
+ await config.api.row.save(table._id!, { name: "foo" })
await config.api.row.get(table._id!, "1234567", {
status: 404,
})
@@ -958,8 +960,8 @@ if (descriptions.length) {
it("fetches all rows for given tableId", async () => {
const table = await config.api.table.save(defaultTable())
const rows = await Promise.all([
- config.api.row.save(table._id!, {}),
- config.api.row.save(table._id!, {}),
+ config.api.row.save(table._id!, { name: "foo" }),
+ config.api.row.save(table._id!, { name: "bar" }),
])
const res = await config.api.row.fetch(table._id!)
@@ -975,7 +977,9 @@ if (descriptions.length) {
describe("update", () => {
it("updates an existing row successfully", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
await expectRowUsage(0, async () => {
const res = await config.api.row.save(table._id!, {
@@ -1166,7 +1170,9 @@ if (descriptions.length) {
})
it("should update only the fields that are supplied", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
await expectRowUsage(0, async () => {
const row = await config.api.row.patch(table._id!, {
@@ -1186,6 +1192,22 @@ if (descriptions.length) {
})
})
+ it("should not require the primary display", async () => {
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ description: "bar",
+ })
+ await expectRowUsage(0, async () => {
+ const row = await config.api.row.patch(table._id!, {
+ _id: existing._id!,
+ _rev: existing._rev!,
+ tableId: table._id!,
+ description: "baz",
+ })
+ expect(row.description).toEqual("baz")
+ })
+ })
+
it("should update only the fields that are supplied and emit the correct oldRow", async () => {
let beforeRow = await config.api.row.save(table._id!, {
name: "test",
@@ -1213,7 +1235,9 @@ if (descriptions.length) {
})
it("should throw an error when given improper types", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
await expectRowUsage(0, async () => {
await config.api.row.patch(
@@ -1289,6 +1313,7 @@ if (descriptions.length) {
description: "test",
})
const { _id } = await config.api.row.save(table._id!, {
+ name: "test",
relationship: [{ _id: row._id }, { _id: row2._id }],
})
const relatedRow = await config.api.row.get(table._id!, _id!, {
@@ -1440,7 +1465,9 @@ if (descriptions.length) {
})
it("should be able to delete a row", async () => {
- const createdRow = await config.api.row.save(table._id!, {})
+ const createdRow = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
await expectRowUsage(isInternal ? -1 : 0, async () => {
const res = await config.api.row.bulkDelete(table._id!, {
@@ -1451,7 +1478,9 @@ if (descriptions.length) {
})
it("should be able to delete a row with ID only", async () => {
- const createdRow = await config.api.row.save(table._id!, {})
+ const createdRow = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
await expectRowUsage(isInternal ? -1 : 0, async () => {
const res = await config.api.row.bulkDelete(table._id!, {
@@ -1463,8 +1492,12 @@ if (descriptions.length) {
})
it("should be able to bulk delete rows, including a row that doesn't exist", async () => {
- const createdRow = await config.api.row.save(table._id!, {})
- const createdRow2 = await config.api.row.save(table._id!, {})
+ const createdRow = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
+ const createdRow2 = await config.api.row.save(table._id!, {
+ name: "bar",
+ })
const res = await config.api.row.bulkDelete(table._id!, {
rows: [createdRow, createdRow2, { _id: "9999999" }],
@@ -1581,8 +1614,8 @@ if (descriptions.length) {
})
it("should be able to delete a bulk set of rows", async () => {
- const row1 = await config.api.row.save(table._id!, {})
- const row2 = await config.api.row.save(table._id!, {})
+ const row1 = await config.api.row.save(table._id!, { name: "foo" })
+ const row2 = await config.api.row.save(table._id!, { name: "bar" })
await expectRowUsage(isInternal ? -2 : 0, async () => {
const res = await config.api.row.bulkDelete(table._id!, {
@@ -1596,9 +1629,9 @@ if (descriptions.length) {
it("should be able to delete a variety of row set types", async () => {
const [row1, row2, row3] = await Promise.all([
- config.api.row.save(table._id!, {}),
- config.api.row.save(table._id!, {}),
- config.api.row.save(table._id!, {}),
+ config.api.row.save(table._id!, { name: "foo" }),
+ config.api.row.save(table._id!, { name: "bar" }),
+ config.api.row.save(table._id!, { name: "baz" }),
])
await expectRowUsage(isInternal ? -3 : 0, async () => {
@@ -1612,7 +1645,7 @@ if (descriptions.length) {
})
it("should accept a valid row object and delete the row", async () => {
- const row1 = await config.api.row.save(table._id!, {})
+ const row1 = await config.api.row.save(table._id!, { name: "foo" })
await expectRowUsage(isInternal ? -1 : 0, async () => {
const res = await config.api.row.delete(
@@ -2347,7 +2380,9 @@ if (descriptions.length) {
!isInternal &&
it("should allow exporting all columns", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
const res = await config.api.row.exportRows(table._id!, {
rows: [existing._id!],
})
@@ -2363,7 +2398,9 @@ if (descriptions.length) {
})
it("should allow exporting without filtering", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
const res = await config.api.row.exportRows(table._id!)
const results = JSON.parse(res)
expect(results.length).toEqual(1)
@@ -2373,7 +2410,9 @@ if (descriptions.length) {
})
it("should allow exporting only certain columns", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
const res = await config.api.row.exportRows(table._id!, {
rows: [existing._id!],
columns: ["_id"],
@@ -2388,7 +2427,9 @@ if (descriptions.length) {
})
it("should handle single quotes in row filtering", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
const res = await config.api.row.exportRows(table._id!, {
rows: [`['${existing._id!}']`],
})
@@ -2399,7 +2440,9 @@ if (descriptions.length) {
})
it("should return an error if no table is found", async () => {
- const existing = await config.api.row.save(table._id!, {})
+ const existing = await config.api.row.save(table._id!, {
+ name: "foo",
+ })
await config.api.row.exportRows(
"1234567",
{ rows: [existing._id!] },
diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts
index 7a7f388a2c..034e3422cb 100644
--- a/packages/server/src/api/routes/tests/search.spec.ts
+++ b/packages/server/src/api/routes/tests/search.spec.ts
@@ -1897,7 +1897,7 @@ if (descriptions.length) {
tableOrViewId = await createTableOrView({
product: { name: "product", type: FieldType.STRING },
ai: {
- name: "AI",
+ name: "ai",
type: FieldType.AI,
operation: AIOperationEnum.PROMPT,
prompt: "Translate '{{ product }}' into German",
diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts
index 6c1065e847..b5ef5d3727 100644
--- a/packages/server/src/db/utils.ts
+++ b/packages/server/src/db/utils.ts
@@ -10,6 +10,7 @@ import {
SourceName,
VirtualDocumentType,
LinkDocument,
+ AIFieldMetadata,
} from "@budibase/types"
export { DocumentType, VirtualDocumentType } from "@budibase/types"
@@ -338,6 +339,10 @@ export function isRelationshipColumn(
return column.type === FieldType.LINK
}
+export function isAIColumn(column: FieldSchema): column is AIFieldMetadata {
+ return column.type === FieldType.AI
+}
+
/**
* Generates a new row actions ID.
* @returns The new row actions ID which the row actions doc can be stored under.
diff --git a/packages/server/src/sdk/app/backups/statistics.ts b/packages/server/src/sdk/app/backups/statistics.ts
index aecb3de423..7169d38512 100644
--- a/packages/server/src/sdk/app/backups/statistics.ts
+++ b/packages/server/src/sdk/app/backups/statistics.ts
@@ -4,8 +4,8 @@ import {
getDatasourceParams,
getTableParams,
getAutomationParams,
- getScreenParams,
} from "../../../db/utils"
+import sdk from "../.."
async function runInContext(appId: string, cb: any, db?: Database) {
if (db) {
@@ -46,8 +46,8 @@ export async function calculateScreenCount(appId: string, db?: Database) {
return runInContext(
appId,
async (db: Database) => {
- const screenList = await db.allDocs(getScreenParams())
- return screenList.rows.length
+ const screenList = await sdk.screens.fetch(db)
+ return screenList.length
},
db
)
diff --git a/packages/server/src/sdk/app/rows/tests/utils.spec.ts b/packages/server/src/sdk/app/rows/tests/utils.spec.ts
index 516334d31d..0ae9e0c079 100644
--- a/packages/server/src/sdk/app/rows/tests/utils.spec.ts
+++ b/packages/server/src/sdk/app/rows/tests/utils.spec.ts
@@ -375,4 +375,31 @@ describe("validate", () => {
})
})
})
+
+ describe("primary display", () => {
+ const getTable = (): Table => ({
+ type: "table",
+ _id: generateTableID(),
+ name: "table",
+ sourceId: INTERNAL_TABLE_SOURCE_ID,
+ sourceType: TableSourceType.INTERNAL,
+ primaryDisplay: "foo",
+ schema: {
+ foo: {
+ name: "foo",
+ type: FieldType.STRING,
+ },
+ },
+ })
+
+ it("should always require primary display column", async () => {
+ const row = {}
+ const table = getTable()
+ const output = await validate({ source: table, row })
+ expect(output.valid).toBe(false)
+ expect(output.errors).toStrictEqual({
+ foo: ["can't be blank"],
+ })
+ })
+ })
})
diff --git a/packages/server/src/sdk/app/rows/utils.ts b/packages/server/src/sdk/app/rows/utils.ts
index c19654d817..cac44aaac9 100644
--- a/packages/server/src/sdk/app/rows/utils.ts
+++ b/packages/server/src/sdk/app/rows/utils.ts
@@ -206,8 +206,14 @@ export async function validate({
]
for (let fieldName of Object.keys(table.schema)) {
const column = table.schema[fieldName]
- const constraints = cloneDeep(column.constraints)
const type = column.type
+ let constraints = cloneDeep(column.constraints)
+
+ // Ensure display column is required
+ if (table.primaryDisplay === fieldName) {
+ constraints = { ...constraints, presence: true }
+ }
+
// foreign keys are likely to be enriched
if (isForeignKey(fieldName, table)) {
continue
diff --git a/packages/server/src/sdk/app/screens/screens.ts b/packages/server/src/sdk/app/screens/screens.ts
index c600825efb..49554fb28a 100644
--- a/packages/server/src/sdk/app/screens/screens.ts
+++ b/packages/server/src/sdk/app/screens/screens.ts
@@ -2,9 +2,7 @@ import { getScreenParams } from "../../../db/utils"
import { context } from "@budibase/backend-core"
import { Screen } from "@budibase/types"
-export async function fetch(): Promise {
- const db = context.getAppDB()
-
+export async function fetch(db = context.getAppDB()): Promise {
return (
await db.allDocs(
getScreenParams(null, {
diff --git a/packages/server/src/sdk/app/tables/getters.ts b/packages/server/src/sdk/app/tables/getters.ts
index f32150c63d..d472aaef22 100644
--- a/packages/server/src/sdk/app/tables/getters.ts
+++ b/packages/server/src/sdk/app/tables/getters.ts
@@ -17,177 +17,234 @@ import datasources from "../datasources"
import sdk from "../../../sdk"
import { ensureQueryUISet } from "../views/utils"
import { isV2 } from "../views"
+import { tracer } from "dd-trace"
export async function processTable(table: Table): Promise {
- if (!table) {
- return table
- }
+ return await tracer.trace("processTable", async span => {
+ if (!table) {
+ return table
+ }
- table = { ...table }
- if (table.views) {
- for (const [key, view] of Object.entries(table.views)) {
- if (!isV2(view)) {
- continue
+ span.addTags({ tableId: table._id })
+
+ table = { ...table }
+ if (table.views) {
+ span.addTags({ numViews: Object.keys(table.views).length })
+ for (const [key, view] of Object.entries(table.views)) {
+ if (!isV2(view)) {
+ continue
+ }
+ table.views[key] = ensureQueryUISet(view)
}
- table.views[key] = ensureQueryUISet(view)
}
- }
- 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"
+ if (table._id && isExternalTableID(table._id)) {
+ span.addTags({ isExternal: true })
+ // 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 {
+ ...table,
+ type: "table",
+ sourceType: TableSourceType.EXTERNAL,
+ }
+ } else {
+ span.addTags({ isExternal: false })
+ const processed: Table = {
+ ...table,
+ type: "table",
+ primary: ["_id"], // internal tables must always use _id as primary key
+ sourceId: table.sourceId || INTERNAL_TABLE_SOURCE_ID,
+ sourceType: TableSourceType.INTERNAL,
+ sql: true,
+ }
+ return processed
}
- return {
- ...table,
- type: "table",
- sourceType: TableSourceType.EXTERNAL,
- }
- } else {
- const processed: Table = {
- ...table,
- type: "table",
- primary: ["_id"], // internal tables must always use _id as primary key
- sourceId: table.sourceId || INTERNAL_TABLE_SOURCE_ID,
- sourceType: TableSourceType.INTERNAL,
- sql: true,
- }
- return processed
- }
+ })
}
export async function processTables(tables: Table[]): Promise {
- return await Promise.all(tables.map(table => processTable(table)))
+ return await tracer.trace("processTables", async span => {
+ span.addTags({ numTables: tables.length })
+ return await Promise.all(tables.map(table => processTable(table)))
+ })
}
async function processEntities(tables: Record) {
- for (let key of Object.keys(tables)) {
- tables[key] = await processTable(tables[key])
- }
- return tables
+ return await tracer.trace("processEntities", async span => {
+ span.addTags({ numTables: Object.keys(tables).length })
+ for (let key of Object.keys(tables)) {
+ tables[key] = await processTable(tables[key])
+ }
+ return tables
+ })
}
export async function getAllInternalTables(db?: Database): Promise {
- if (!db) {
- db = context.getAppDB()
- }
- const internalTables = await db.allDocs(
- getTableParams(null, {
- include_docs: true,
- })
- )
- return await processTables(internalTables.rows.map(row => row.doc!))
+ return await tracer.trace("getAllInternalTables", async span => {
+ if (!db) {
+ db = context.getAppDB()
+ }
+ span.addTags({ db: db.name })
+ const internalTables = await db.allDocs(
+ getTableParams(null, {
+ include_docs: true,
+ })
+ )
+ span.addTags({ numTables: internalTables.rows.length })
+ return await processTables(internalTables.rows.map(row => row.doc!))
+ })
}
async function getAllExternalTables(): Promise {
- // this is all datasources, we'll need to filter out internal
- const datasources = await sdk.datasources.fetch({ enriched: true })
- const allEntities = datasources
- .filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID)
- .map(datasource => datasource.entities)
- let final: Table[] = []
- for (let entities of allEntities) {
- if (entities) {
- final = final.concat(Object.values(entities))
+ return await tracer.trace("getAllExternalTables", async span => {
+ // this is all datasources, we'll need to filter out internal
+ const datasources = await sdk.datasources.fetch({ enriched: true })
+ span.addTags({ numDatasources: datasources.length })
+
+ const allEntities = datasources
+ .filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID)
+ .map(datasource => datasource.entities)
+ span.addTags({ numEntities: allEntities.length })
+
+ let final: Table[] = []
+ for (let entities of allEntities) {
+ if (entities) {
+ final = final.concat(Object.values(entities))
+ }
}
- }
- return await processTables(final)
+ span.addTags({ numTables: final.length })
+ return await processTables(final)
+ })
}
export async function getExternalTable(
datasourceId: string,
tableName: string
): Promise {
- const entities = await getExternalTablesInDatasource(datasourceId)
- if (!entities[tableName]) {
- throw new Error(`Unable to find table named "${tableName}"`)
- }
- const table = await processTable(entities[tableName])
- if (!table.sourceId) {
- table.sourceId = datasourceId
- }
- return table
+ return await tracer.trace("getExternalTable", async span => {
+ span.addTags({ datasourceId, tableName })
+ const entities = await getExternalTablesInDatasource(datasourceId)
+ if (!entities[tableName]) {
+ throw new Error(`Unable to find table named "${tableName}"`)
+ }
+ const table = await processTable(entities[tableName])
+ if (!table.sourceId) {
+ table.sourceId = datasourceId
+ }
+ return table
+ })
}
export async function getTable(tableId: string): Promise {
- const db = context.getAppDB()
- let output: Table
- if (tableId && isExternalTableID(tableId)) {
- let { datasourceId, tableName } = breakExternalTableId(tableId)
- const datasource = await datasources.get(datasourceId)
- const table = await getExternalTable(datasourceId, tableName)
- output = { ...table, sql: isSQL(datasource) }
- } else {
- output = await db.get(tableId)
- }
- return await processTable(output)
+ return await tracer.trace("getTable", async span => {
+ const db = context.getAppDB()
+ span.addTags({ tableId, db: db.name })
+ let output: Table
+ if (tableId && isExternalTableID(tableId)) {
+ let { datasourceId, tableName } = breakExternalTableId(tableId)
+ span.addTags({ isExternal: true, datasourceId, tableName })
+ const datasource = await datasources.get(datasourceId)
+ const table = await getExternalTable(datasourceId, tableName)
+ output = { ...table, sql: isSQL(datasource) }
+ span.addTags({ isSQL: isSQL(datasource) })
+ } else {
+ output = await db.get(tableId)
+ }
+ return await processTable(output)
+ })
}
export async function doesTableExist(tableId: string): Promise {
- try {
- const table = await getTable(tableId)
- return !!table
- } catch (err) {
- return false
- }
+ return await tracer.trace("doesTableExist", async span => {
+ span.addTags({ tableId })
+ try {
+ const table = await getTable(tableId)
+ span.addTags({ tableExists: !!table })
+ return !!table
+ } catch (err) {
+ span.addTags({ tableExists: false })
+ return false
+ }
+ })
}
export async function getAllTables() {
- const [internal, external] = await Promise.all([
- getAllInternalTables(),
- getAllExternalTables(),
- ])
- return await processTables([...internal, ...external])
+ return await tracer.trace("getAllTables", async span => {
+ const [internal, external] = await Promise.all([
+ getAllInternalTables(),
+ getAllExternalTables(),
+ ])
+ span.addTags({
+ numInternalTables: internal.length,
+ numExternalTables: external.length,
+ })
+ return await processTables([...internal, ...external])
+ })
}
export async function getExternalTablesInDatasource(
datasourceId: string
): Promise> {
- const datasource = await datasources.get(datasourceId, { enriched: true })
- if (!datasource || !datasource.entities) {
- throw new Error("Datasource is not configured fully.")
- }
- return await processEntities(datasource.entities)
+ return await tracer.trace("getExternalTablesInDatasource", async span => {
+ const datasource = await datasources.get(datasourceId, { enriched: true })
+ if (!datasource || !datasource.entities) {
+ throw new Error("Datasource is not configured fully.")
+ }
+ span.addTags({
+ datasourceId,
+ numEntities: Object.keys(datasource.entities).length,
+ })
+ return await processEntities(datasource.entities)
+ })
}
export async function getTables(tableIds: string[]): Promise {
- const externalTableIds = tableIds.filter(tableId =>
- isExternalTableID(tableId)
- ),
- internalTableIds = tableIds.filter(tableId => !isExternalTableID(tableId))
- let tables: Table[] = []
- if (externalTableIds.length) {
- const externalTables = await getAllExternalTables()
- tables = tables.concat(
- externalTables.filter(
- table => externalTableIds.indexOf(table._id!) !== -1
+ return tracer.trace("getTables", async span => {
+ span.addTags({ numTableIds: tableIds.length })
+ const externalTableIds = tableIds.filter(tableId =>
+ isExternalTableID(tableId)
+ ),
+ internalTableIds = tableIds.filter(tableId => !isExternalTableID(tableId))
+ let tables: Table[] = []
+ if (externalTableIds.length) {
+ const externalTables = await getAllExternalTables()
+ tables = tables.concat(
+ externalTables.filter(
+ table => externalTableIds.indexOf(table._id!) !== -1
+ )
)
- )
- }
- if (internalTableIds.length) {
- const db = context.getAppDB()
- const internalTables = await db.getMultiple(internalTableIds, {
- allowMissing: true,
- })
- tables = tables.concat(internalTables)
- }
- return await processTables(tables)
+ }
+ if (internalTableIds.length) {
+ const db = context.getAppDB()
+ const internalTables = await db.getMultiple(internalTableIds, {
+ allowMissing: true,
+ })
+ tables = tables.concat(internalTables)
+ }
+ span.addTags({ numTables: tables.length })
+ return await processTables(tables)
+ })
}
export async function enrichViewSchemas(
table: Table
): Promise {
- const views = []
- for (const view of Object.values(table.views ?? [])) {
- if (sdk.views.isV2(view)) {
- views.push(await sdk.views.enrichSchema(view, table.schema))
- } else views.push(view)
- }
+ return await tracer.trace("enrichViewSchemas", async span => {
+ span.addTags({ tableId: table._id })
+ const views = []
+ for (const view of Object.values(table.views ?? [])) {
+ if (sdk.views.isV2(view)) {
+ views.push(await sdk.views.enrichSchema(view, table.schema))
+ } else views.push(view)
+ }
- return {
- ...table,
- views: views.reduce((p, v) => {
- p[v.name!] = v
- return p
- }, {} as TableViewsResponse),
- }
+ return {
+ ...table,
+ views: views.reduce((p, v) => {
+ p[v.name!] = v
+ return p
+ }, {} as TableViewsResponse),
+ }
+ })
}
diff --git a/packages/server/src/utilities/rowProcessor/utils.ts b/packages/server/src/utilities/rowProcessor/utils.ts
index 8344ca5ce1..921a848afd 100644
--- a/packages/server/src/utilities/rowProcessor/utils.ts
+++ b/packages/server/src/utilities/rowProcessor/utils.ts
@@ -1,21 +1,20 @@
-import { AutoFieldDefaultNames } from "../../constants"
+import { context } from "@budibase/backend-core"
+import { ai } from "@budibase/pro"
+import { OperationFields } from "@budibase/shared-core"
import { processStringSync } from "@budibase/string-templates"
import {
AutoColumnFieldMetadata,
+ AutoFieldSubType,
FieldSchema,
+ FieldType,
+ FormulaType,
+ OperationFieldTypeEnum,
Row,
Table,
- FormulaType,
- AutoFieldSubType,
- FieldType,
- OperationFieldTypeEnum,
- AIOperationEnum,
- AIFieldMetadata,
} from "@budibase/types"
-import { OperationFields } from "@budibase/shared-core"
import tracer from "dd-trace"
-import { context } from "@budibase/backend-core"
-import { ai } from "@budibase/pro"
+import { AutoFieldDefaultNames } from "../../constants"
+import { isAIColumn } from "../../db/utils"
import { coerce } from "./index"
interface FormulaOpts {
@@ -122,52 +121,56 @@ export async function processAIColumns(
inputRows: T,
{ contextRows }: FormulaOpts
): Promise {
+ const aiColumns = Object.values(table.schema).filter(isAIColumn)
+ if (!aiColumns.length) {
+ return inputRows
+ }
+
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 ai.getLLM()
if (rows && llm) {
// Ensure we have snippet context
await context.ensureSnippetContext()
- for (let [column, schema] of Object.entries(table.schema)) {
- if (schema.type !== FieldType.AI) {
- continue
- }
+ const aiColumns = Object.values(table.schema).filter(isAIColumn)
- const operation = schema.operation
- const aiSchema: AIFieldMetadata = schema
- const rowUpdates = rows.map((row, i) => {
- const contextRow = contextRows ? contextRows[i] : row
+ const rowUpdates = rows.flatMap((row, i) => {
+ const contextRow = contextRows ? contextRows[i] : row
+
+ return aiColumns.map(aiColumn => {
+ const column = aiColumn.name
// Check if the type is bindable and pass through HBS if so
- const operationField = OperationFields[operation as AIOperationEnum]
- for (const key in schema) {
+ const operationField = OperationFields[aiColumn.operation]
+ for (const key in aiColumn) {
const fieldType = operationField[key as keyof typeof operationField]
if (fieldType === OperationFieldTypeEnum.BINDABLE_TEXT) {
- // @ts-ignore
- schema[key] = processStringSync(schema[key], contextRow)
+ // @ts-expect-error: keys are not casted
+ aiColumn[key] = processStringSync(aiColumn[key], contextRow)
}
}
return tracer.trace("processAIColumn", {}, async span => {
span?.addTags({ table_id: table._id, column })
- const llmResponse = await llm.operation(aiSchema, row)
+ const llmResponse = await llm.operation(aiColumn, row)
return {
- ...row,
- [column]: llmResponse.message,
+ rowIndex: i,
+ columnName: column,
+ value: llmResponse.message,
}
})
})
+ })
- const processedRows = await Promise.all(rowUpdates)
+ const processedAIColumns = await Promise.all(rowUpdates)
- // Promise.all is deterministic so can rely on the indexing here
- processedRows.forEach(
- (processedRow, index) => (rows[index] = processedRow)
- )
- }
+ processedAIColumns.forEach(aiColumn => {
+ rows[aiColumn.rowIndex][aiColumn.columnName] = aiColumn.value
+ })
}
return Array.isArray(inputRows) ? rows : rows[0]
})
diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts
index a0bd54fee6..e4ad413aed 100644
--- a/packages/shared-core/src/filters.ts
+++ b/packages/shared-core/src/filters.ts
@@ -323,7 +323,13 @@ function buildCondition(filter?: SearchFilter): SearchFilters | undefined {
if (!value) {
return
}
- value = new Date(value).toISOString()
+ if (typeof value === "string") {
+ value = new Date(value).toISOString()
+ } else if (isRangeSearchOperator(operator)) {
+ query[operator] ??= {}
+ query[operator][field] = value
+ return query
+ }
}
break
case FieldType.NUMBER:
@@ -349,7 +355,6 @@ function buildCondition(filter?: SearchFilter): SearchFilters | undefined {
}
break
}
-
if (isRangeSearchOperator(operator)) {
const key = externalType as keyof typeof SqlNumberTypeRangeMap
const limits = SqlNumberTypeRangeMap[key] || {
@@ -637,7 +642,6 @@ export function runQuery>(
if (docValue == null || docValue === "") {
return false
}
-
if (isPlainObject(testValue.low) && isEmpty(testValue.low)) {
testValue.low = undefined
}
diff --git a/packages/types/src/ui/components/index.ts b/packages/types/src/ui/components/index.ts
index a7990e1c12..bcc3c3fbea 100644
--- a/packages/types/src/ui/components/index.ts
+++ b/packages/types/src/ui/components/index.ts
@@ -1,3 +1,5 @@
+import { FieldType } from "@budibase/types"
+
export * from "./codeEditor"
export * from "./errors"
@@ -83,3 +85,11 @@ export const enum ComponentContextScopes {
Local = "local",
Global = "global",
}
+
+export type FilterConfig = {
+ active: boolean
+ field: string
+ label?: string
+ _id?: string
+ columnType?: FieldType
+}