From 5f353a85f7f9668d59e6cd9fa1e01a0a8e7891ff Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Wed, 8 Jan 2025 10:55:11 +0000 Subject: [PATCH 01/17] permissions store --- .../builder/src/stores/builder/permissions.js | 27 ----------- .../builder/src/stores/builder/permissions.ts | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 27 deletions(-) delete mode 100644 packages/builder/src/stores/builder/permissions.js create mode 100644 packages/builder/src/stores/builder/permissions.ts diff --git a/packages/builder/src/stores/builder/permissions.js b/packages/builder/src/stores/builder/permissions.js deleted file mode 100644 index a303cd713b..0000000000 --- a/packages/builder/src/stores/builder/permissions.js +++ /dev/null @@ -1,27 +0,0 @@ -import { writable } from "svelte/store" -import { API } from "@/api" - -export function createPermissionStore() { - const { subscribe } = writable([]) - - return { - subscribe, - save: async ({ level, role, resource }) => { - return await API.updatePermissionForResource(resource, role, level) - }, - remove: async ({ level, role, resource }) => { - return await API.removePermissionFromResource(resource, role, level) - }, - forResource: async resourceId => { - return (await API.getPermissionForResource(resourceId)).permissions - }, - forResourceDetailed: async resourceId => { - return await API.getPermissionForResource(resourceId) - }, - getDependantsInfo: async resourceId => { - return await API.getDependants(resourceId) - }, - } -} - -export const permissions = createPermissionStore() diff --git a/packages/builder/src/stores/builder/permissions.ts b/packages/builder/src/stores/builder/permissions.ts new file mode 100644 index 0000000000..002b73893e --- /dev/null +++ b/packages/builder/src/stores/builder/permissions.ts @@ -0,0 +1,47 @@ +import { BudiStore } from "../BudiStore" +import { API } from "@/api" +import { + PermissionLevel, + GetResourcePermsResponse, + GetDependantResourcesResponse, +} from "@budibase/types" + +interface Permission { + level: PermissionLevel + role: string + resource: string +} + +export class PermissionStore extends BudiStore { + constructor() { + super([]) + } + + save = async (permission: Permission) => { + const { level, role, resource } = permission + return await API.updatePermissionForResource(resource, role, level) + } + + remove = async (permission: Permission) => { + const { level, role, resource } = permission + return await API.removePermissionFromResource(resource, role, level) + } + + forResource = async (resourceId: string): Promise => { + return (await API.getPermissionForResource(resourceId)).permissions + } + + forResourceDetailed = async ( + resourceId: string + ): Promise => { + return await API.getPermissionForResource(resourceId) + } + + getDependantsInfo = async ( + resourceId: string + ): Promise => { + return await API.getDependants(resourceId) + } +} + +export const permissions = new PermissionStore() From a983292865fee9980342d23af16a8d9ed20c6a64 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Wed, 8 Jan 2025 11:14:56 +0000 Subject: [PATCH 02/17] fix return type --- packages/builder/src/stores/builder/permissions.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/stores/builder/permissions.ts b/packages/builder/src/stores/builder/permissions.ts index 002b73893e..6056449150 100644 --- a/packages/builder/src/stores/builder/permissions.ts +++ b/packages/builder/src/stores/builder/permissions.ts @@ -4,6 +4,7 @@ import { PermissionLevel, GetResourcePermsResponse, GetDependantResourcesResponse, + ResourcePermissionInfo, } from "@budibase/types" interface Permission { @@ -27,7 +28,9 @@ export class PermissionStore extends BudiStore { return await API.removePermissionFromResource(resource, role, level) } - forResource = async (resourceId: string): Promise => { + forResource = async ( + resourceId: string + ): Promise> => { return (await API.getPermissionForResource(resourceId)).permissions } From b6418c333cf422857f64db84f963f96e12a061d4 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 9 Jan 2025 09:20:16 +0000 Subject: [PATCH 03/17] views v1 --- packages/builder/src/stores/builder/views.js | 67 -------------- packages/builder/src/stores/builder/views.ts | 94 ++++++++++++++++++++ 2 files changed, 94 insertions(+), 67 deletions(-) delete mode 100644 packages/builder/src/stores/builder/views.js create mode 100644 packages/builder/src/stores/builder/views.ts diff --git a/packages/builder/src/stores/builder/views.js b/packages/builder/src/stores/builder/views.js deleted file mode 100644 index 07c356f56d..0000000000 --- a/packages/builder/src/stores/builder/views.js +++ /dev/null @@ -1,67 +0,0 @@ -import { writable, derived } from "svelte/store" -import { tables } from "./tables" -import { API } from "@/api" - -export function createViewsStore() { - const store = writable({ - selectedViewName: null, - }) - const derivedStore = derived([store, tables], ([$store, $tables]) => { - let list = [] - $tables.list?.forEach(table => { - const views = Object.values(table?.views || {}).filter(view => { - return view.version !== 2 - }) - list = list.concat(views) - }) - return { - ...$store, - list, - selected: list.find(view => view.name === $store.selectedViewName), - } - }) - - const select = name => { - store.update(state => ({ - ...state, - selectedViewName: name, - })) - } - - const deleteView = async view => { - await API.deleteView(view.name) - - // Update tables - tables.update(state => { - const table = state.list.find(table => table._id === view.tableId) - delete table.views[view.name] - return { ...state } - }) - } - - const save = async view => { - const savedView = await API.saveView(view) - select(view.name) - - // Update tables - tables.update(state => { - const table = state.list.find(table => table._id === view.tableId) - if (table) { - if (view.originalName) { - delete table.views[view.originalName] - } - table.views[view.name] = savedView - } - return { ...state } - }) - } - - return { - subscribe: derivedStore.subscribe, - select, - delete: deleteView, - save, - } -} - -export const views = createViewsStore() diff --git a/packages/builder/src/stores/builder/views.ts b/packages/builder/src/stores/builder/views.ts new file mode 100644 index 0000000000..4e309e180d --- /dev/null +++ b/packages/builder/src/stores/builder/views.ts @@ -0,0 +1,94 @@ +import { DerivedBudiStore } from "../BudiStore" +import { tables } from "./tables" +import { API } from "@/api" +import { View } from "@budibase/types" +import { helpers } from "@budibase/shared-core" +import { derived, Writable } from "svelte/store" + +interface BuilderViewStore { + selectedViewName: string | null +} + +interface DerivedViewStore extends BuilderViewStore { + list: View[] + selected?: View +} + +export class ViewsStore extends DerivedBudiStore< + BuilderViewStore, + DerivedViewStore +> { + constructor() { + const makeDerivedStore = (store: Writable) => { + return derived([store, tables], ([$store, $tables]): DerivedViewStore => { + let list: View[] = [] + $tables.list?.forEach(table => { + const views = Object.values(table?.views || {}).filter( + (view): view is View => !helpers.views.isV2(view) + ) + list = list.concat(views) + }) + return { + selectedViewName: $store.selectedViewName, + list, + selected: list.find(view => view.name === $store.selectedViewName), + } + }) + } + + super( + { + selectedViewName: null, + }, + makeDerivedStore + ) + + this.select = this.select.bind(this) + } + + select = (name: string) => { + this.store.update(state => ({ + ...state, + selectedViewName: name, + })) + } + + delete = async (view: View) => { + if (!view.name) { + return + } + await API.deleteView(view.name) + + // Update tables + tables.update(state => { + const table = state.list.find(table => table._id === view.tableId) + if (table?.views && view.name) { + delete table.views[view.name] + } + return { ...state } + }) + } + + save = async (view: View & { originalName?: string }) => { + if (!view.name) { + return + } + + const savedView = await API.saveView(view) + this.select(view.name) + + // Update tables + tables.update(state => { + const table = state.list.find(table => table._id === view.tableId) + if (table?.views && view.name) { + if (view.originalName) { + delete table.views[view.originalName] + } + table.views[view.name] = savedView + } + return { ...state } + }) + } +} + +export const views = new ViewsStore() From d06b22d4b8eaded1b62c855f04dafc8cb6fbbb1c Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 13 Jan 2025 11:56:33 +0000 Subject: [PATCH 04/17] Add sorting tests for dateonly fields. --- .../src/api/routes/tests/search.spec.ts | 318 +++++++++++------- 1 file changed, 192 insertions(+), 126 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 4de92f21e5..18221f9c12 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1690,138 +1690,204 @@ if (descriptions.length) { describe.each([true, false])( "search with timestamp: %s", searchWithTimestamp => { - const SAVE_SUFFIX = saveWithTimestamp - ? "T00:00:00.000Z" - : "" - const SEARCH_SUFFIX = searchWithTimestamp - ? "T00:00:00.000Z" - : "" + describe.each(["/", "-"])( + "date separator: %s", + separator => { + const SAVE_SUFFIX = saveWithTimestamp + ? "T00:00:00.000Z" + : "" + const SEARCH_SUFFIX = searchWithTimestamp + ? "T00:00:00.000Z" + : "" - const JAN_1ST = `2020-01-01` - const JAN_10TH = `2020-01-10` - const JAN_30TH = `2020-01-30` - const UNEXISTING_DATE = `2020-01-03` - const NULL_DATE__ID = `null_date__id` + const JAN_1ST = `2020-01-01` + const JAN_10TH = `2020-01-10` + const JAN_30TH = `2020-01-30` + const UNEXISTING_DATE = `2020-01-03` + const NULL_DATE__ID = `null_date__id` - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - dateid: { name: "dateid", type: FieldType.STRING }, - date: { - name: "date", - type: FieldType.DATETIME, - dateOnly: true, - }, - }) - - await createRows([ - { dateid: NULL_DATE__ID, date: null }, - { date: `${JAN_1ST}${SAVE_SUFFIX}` }, - { date: `${JAN_10TH}${SAVE_SUFFIX}` }, - ]) - }) - - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ - equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, - }).toContainExactly([{ date: JAN_1ST }]) - }) - - it("successfully finds an ISO8601 row", async () => { - await expectQuery({ - equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` }, - }).toContainExactly([{ date: JAN_10TH }]) - }) - - it("finds a row with ISO8601 timestamp", async () => { - await expectQuery({ - equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, - }).toContainExactly([{ date: JAN_1ST }]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - equal: { - date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`, - }, - }).toFindNothing() - }) - }) - - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ - notEqual: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, - }).toContainExactly([ - { date: JAN_10TH }, - { dateid: NULL_DATE__ID }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - notEqual: { date: `${JAN_30TH}${SEARCH_SUFFIX}` }, - }).toContainExactly([ - { date: JAN_1ST }, - { date: JAN_10TH }, - { dateid: NULL_DATE__ID }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ - oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] }, - }).toContainExactly([{ date: JAN_1ST }]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - oneOf: { - date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`], - }, - }).toFindNothing() - }) - }) - - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { - date: { - low: `${JAN_1ST}${SEARCH_SUFFIX}`, - high: `${JAN_1ST}${SEARCH_SUFFIX}`, + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + dateid: { + name: "dateid", + type: FieldType.STRING, }, - }, - }).toContainExactly([{ date: JAN_1ST }]) - }) - - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { date: { - low: `${JAN_1ST}${SEARCH_SUFFIX}`, - high: `${JAN_10TH}${SEARCH_SUFFIX}`, + name: "date", + type: FieldType.DATETIME, + dateOnly: true, }, - }, - }).toContainExactly([ - { date: JAN_1ST }, - { date: JAN_10TH }, - ]) - }) + }) - it("successfully finds no rows", async () => { - await expectQuery({ - range: { - date: { - low: `${JAN_30TH}${SEARCH_SUFFIX}`, - high: `${JAN_30TH}${SEARCH_SUFFIX}`, - }, - }, - }).toFindNothing() - }) - }) + await createRows([ + { dateid: NULL_DATE__ID, date: null }, + { date: `${JAN_1ST}${SAVE_SUFFIX}` }, + { date: `${JAN_10TH}${SAVE_SUFFIX}` }, + ]) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("successfully finds an ISO8601 row", async () => { + await expectQuery({ + equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_10TH }]) + }) + + it("finds a row with ISO8601 timestamp", async () => { + await expectQuery({ + equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { + date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`, + }, + }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { + date: `${JAN_1ST}${SEARCH_SUFFIX}`, + }, + }).toContainExactly([ + { date: JAN_10TH }, + { dateid: NULL_DATE__ID }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notEqual: { + date: `${JAN_30TH}${SEARCH_SUFFIX}`, + }, + }).toContainExactly([ + { date: JAN_1ST }, + { date: JAN_10TH }, + { dateid: NULL_DATE__ID }, + ]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { + date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`], + }, + }).toFindNothing() + }) + }) + + describe("range", () => { + it("successfully finds a row", async () => { + await expectQuery({ + range: { + date: { + low: `${JAN_1ST}${SEARCH_SUFFIX}`, + high: `${JAN_1ST}${SEARCH_SUFFIX}`, + }, + }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { + date: { + low: `${JAN_1ST}${SEARCH_SUFFIX}`, + high: `${JAN_10TH}${SEARCH_SUFFIX}`, + }, + }, + }).toContainExactly([ + { date: JAN_1ST }, + { date: JAN_10TH }, + ]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { + date: { + low: `${JAN_30TH}${SEARCH_SUFFIX}`, + high: `${JAN_30TH}${SEARCH_SUFFIX}`, + }, + }, + }).toFindNothing() + }) + }) + + describe.only("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { dateid: NULL_DATE__ID }, + { date: JAN_1ST }, + { date: JAN_10TH }, + ]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { date: JAN_10TH }, + { date: JAN_1ST }, + { dateid: NULL_DATE__ID }, + ]) + }) + + describe("sortType STRING", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { dateid: NULL_DATE__ID }, + { date: JAN_1ST }, + { date: JAN_10TH }, + ]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { date: JAN_10TH }, + { date: JAN_1ST }, + { dateid: NULL_DATE__ID }, + ]) + }) + }) + }) + } + ) } ) } From e55874a6988e552086530db60e22972d898be44e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 13 Jan 2025 12:51:13 +0000 Subject: [PATCH 05/17] More validation around datetime columns and bulk importing. --- .../server/src/api/routes/tests/row.spec.ts | 98 +++++ .../src/api/routes/tests/search.spec.ts | 373 +++++++++--------- packages/server/src/utilities/schema.ts | 24 +- 3 files changed, 302 insertions(+), 193 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index e5cd54e5a5..737ac2863a 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -2043,6 +2043,104 @@ if (descriptions.length) { expect(rows[0].name).toEqual("Clare updated") expect(rows[1].name).toEqual("Jeff updated") }) + + it("should reject bulkImport date only fields with wrong format", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + date: { + type: FieldType.DATETIME, + dateOnly: true, + name: "date", + }, + }, + }) + ) + + await config.api.row.bulkImport( + table._id!, + { + rows: [ + { + date: "01.02.2024", + }, + ], + }, + { + status: 400, + body: { + message: + 'Invalid format for field "date": "01.02.2024". Date-only fields must be in the format "YYYY-MM-DD".', + }, + } + ) + }) + + it("should reject bulkImport date time fields with wrong format", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + date: { + type: FieldType.DATETIME, + name: "date", + }, + }, + }) + ) + + await config.api.row.bulkImport( + table._id!, + { + rows: [ + { + date: "01.02.2024", + }, + ], + }, + { + status: 400, + body: { + message: + 'Invalid format for field "date": "01.02.2024". Datetime fields must be in ISO format, e.g. "YYYY-MM-DDTHH:MM:SSZ".', + }, + } + ) + }) + + it("should reject bulkImport time fields with wrong format", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + time: { + type: FieldType.DATETIME, + timeOnly: true, + name: "time", + }, + }, + }) + ) + + await config.api.row.bulkImport( + table._id!, + { + rows: [ + { + time: "3pm", + }, + ], + }, + { + // This isn't ideal atm because it doesn't line up with datetime + // and date only error messages, but there's a check earlier in + // the stack than when those errors happen that produces this one, + // and it's not easy to bypass. The key is that this fails. + status: 500, + body: { + message: 'Invalid date value: "3pm"', + }, + } + ) + }) }) describe("enrich", () => { diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 18221f9c12..c3b274d5f4 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1690,204 +1690,199 @@ if (descriptions.length) { describe.each([true, false])( "search with timestamp: %s", searchWithTimestamp => { - describe.each(["/", "-"])( - "date separator: %s", - separator => { - const SAVE_SUFFIX = saveWithTimestamp - ? "T00:00:00.000Z" - : "" - const SEARCH_SUFFIX = searchWithTimestamp - ? "T00:00:00.000Z" - : "" + const SAVE_SUFFIX = saveWithTimestamp + ? "T00:00:00.000Z" + : "" + const SEARCH_SUFFIX = searchWithTimestamp + ? "T00:00:00.000Z" + : "" - const JAN_1ST = `2020-01-01` - const JAN_10TH = `2020-01-10` - const JAN_30TH = `2020-01-30` - const UNEXISTING_DATE = `2020-01-03` - const NULL_DATE__ID = `null_date__id` + const JAN_1ST = `2020-01-01` + const JAN_10TH = `2020-01-10` + const JAN_30TH = `2020-01-30` + const UNEXISTING_DATE = `2020-01-03` + const NULL_DATE__ID = `null_date__id` - beforeAll(async () => { - tableOrViewId = await createTableOrView({ - dateid: { - name: "dateid", - type: FieldType.STRING, - }, + beforeAll(async () => { + tableOrViewId = await createTableOrView({ + dateid: { + name: "dateid", + type: FieldType.STRING, + }, + date: { + name: "date", + type: FieldType.DATETIME, + dateOnly: true, + }, + }) + + await createRows([ + { dateid: NULL_DATE__ID, date: null }, + { date: `${JAN_1ST}${SAVE_SUFFIX}` }, + { date: `${JAN_10TH}${SAVE_SUFFIX}` }, + ]) + }) + + describe("equal", () => { + it("successfully finds a row", async () => { + await expectQuery({ + equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("successfully finds an ISO8601 row", async () => { + await expectQuery({ + equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_10TH }]) + }) + + it("finds a row with ISO8601 timestamp", async () => { + await expectQuery({ + equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + equal: { + date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`, + }, + }).toFindNothing() + }) + }) + + describe("notEqual", () => { + it("successfully finds a row", async () => { + await expectQuery({ + notEqual: { + date: `${JAN_1ST}${SEARCH_SUFFIX}`, + }, + }).toContainExactly([ + { date: JAN_10TH }, + { dateid: NULL_DATE__ID }, + ]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + notEqual: { + date: `${JAN_30TH}${SEARCH_SUFFIX}`, + }, + }).toContainExactly([ + { date: JAN_1ST }, + { date: JAN_10TH }, + { dateid: NULL_DATE__ID }, + ]) + }) + }) + + describe("oneOf", () => { + it("successfully finds a row", async () => { + await expectQuery({ + oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] }, + }).toContainExactly([{ date: JAN_1ST }]) + }) + + it("fails to find nonexistent row", async () => { + await expectQuery({ + oneOf: { + date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`], + }, + }).toFindNothing() + }) + }) + + describe("range", () => { + it("successfully finds a row", async () => { + await expectQuery({ + range: { date: { - name: "date", - type: FieldType.DATETIME, - dateOnly: true, + low: `${JAN_1ST}${SEARCH_SUFFIX}`, + high: `${JAN_1ST}${SEARCH_SUFFIX}`, }, - }) + }, + }).toContainExactly([{ date: JAN_1ST }]) + }) - await createRows([ - { dateid: NULL_DATE__ID, date: null }, - { date: `${JAN_1ST}${SAVE_SUFFIX}` }, - { date: `${JAN_10TH}${SAVE_SUFFIX}` }, + it("successfully finds multiple rows", async () => { + await expectQuery({ + range: { + date: { + low: `${JAN_1ST}${SEARCH_SUFFIX}`, + high: `${JAN_10TH}${SEARCH_SUFFIX}`, + }, + }, + }).toContainExactly([ + { date: JAN_1ST }, + { date: JAN_10TH }, + ]) + }) + + it("successfully finds no rows", async () => { + await expectQuery({ + range: { + date: { + low: `${JAN_30TH}${SEARCH_SUFFIX}`, + high: `${JAN_30TH}${SEARCH_SUFFIX}`, + }, + }, + }).toFindNothing() + }) + }) + + describe("sort", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { dateid: NULL_DATE__ID }, + { date: JAN_1ST }, + { date: JAN_10TH }, + ]) + }) + + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { date: JAN_10TH }, + { date: JAN_1ST }, + { dateid: NULL_DATE__ID }, + ]) + }) + + describe("sortType STRING", () => { + it("sorts ascending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([ + { dateid: NULL_DATE__ID }, + { date: JAN_1ST }, + { date: JAN_10TH }, ]) }) - describe("equal", () => { - it("successfully finds a row", async () => { - await expectQuery({ - equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, - }).toContainExactly([{ date: JAN_1ST }]) - }) - - it("successfully finds an ISO8601 row", async () => { - await expectQuery({ - equal: { date: `${JAN_10TH}${SEARCH_SUFFIX}` }, - }).toContainExactly([{ date: JAN_10TH }]) - }) - - it("finds a row with ISO8601 timestamp", async () => { - await expectQuery({ - equal: { date: `${JAN_1ST}${SEARCH_SUFFIX}` }, - }).toContainExactly([{ date: JAN_1ST }]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - equal: { - date: `${UNEXISTING_DATE}${SEARCH_SUFFIX}`, - }, - }).toFindNothing() - }) + it("sorts descending", async () => { + await expectSearch({ + query: {}, + sort: "date", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([ + { date: JAN_10TH }, + { date: JAN_1ST }, + { dateid: NULL_DATE__ID }, + ]) }) - - describe("notEqual", () => { - it("successfully finds a row", async () => { - await expectQuery({ - notEqual: { - date: `${JAN_1ST}${SEARCH_SUFFIX}`, - }, - }).toContainExactly([ - { date: JAN_10TH }, - { dateid: NULL_DATE__ID }, - ]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - notEqual: { - date: `${JAN_30TH}${SEARCH_SUFFIX}`, - }, - }).toContainExactly([ - { date: JAN_1ST }, - { date: JAN_10TH }, - { dateid: NULL_DATE__ID }, - ]) - }) - }) - - describe("oneOf", () => { - it("successfully finds a row", async () => { - await expectQuery({ - oneOf: { date: [`${JAN_1ST}${SEARCH_SUFFIX}`] }, - }).toContainExactly([{ date: JAN_1ST }]) - }) - - it("fails to find nonexistent row", async () => { - await expectQuery({ - oneOf: { - date: [`${UNEXISTING_DATE}${SEARCH_SUFFIX}`], - }, - }).toFindNothing() - }) - }) - - describe("range", () => { - it("successfully finds a row", async () => { - await expectQuery({ - range: { - date: { - low: `${JAN_1ST}${SEARCH_SUFFIX}`, - high: `${JAN_1ST}${SEARCH_SUFFIX}`, - }, - }, - }).toContainExactly([{ date: JAN_1ST }]) - }) - - it("successfully finds multiple rows", async () => { - await expectQuery({ - range: { - date: { - low: `${JAN_1ST}${SEARCH_SUFFIX}`, - high: `${JAN_10TH}${SEARCH_SUFFIX}`, - }, - }, - }).toContainExactly([ - { date: JAN_1ST }, - { date: JAN_10TH }, - ]) - }) - - it("successfully finds no rows", async () => { - await expectQuery({ - range: { - date: { - low: `${JAN_30TH}${SEARCH_SUFFIX}`, - high: `${JAN_30TH}${SEARCH_SUFFIX}`, - }, - }, - }).toFindNothing() - }) - }) - - describe.only("sort", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "date", - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([ - { dateid: NULL_DATE__ID }, - { date: JAN_1ST }, - { date: JAN_10TH }, - ]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "date", - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([ - { date: JAN_10TH }, - { date: JAN_1ST }, - { dateid: NULL_DATE__ID }, - ]) - }) - - describe("sortType STRING", () => { - it("sorts ascending", async () => { - await expectSearch({ - query: {}, - sort: "date", - sortType: SortType.STRING, - sortOrder: SortOrder.ASCENDING, - }).toMatchExactly([ - { dateid: NULL_DATE__ID }, - { date: JAN_1ST }, - { date: JAN_10TH }, - ]) - }) - - it("sorts descending", async () => { - await expectSearch({ - query: {}, - sort: "date", - sortType: SortType.STRING, - sortOrder: SortOrder.DESCENDING, - }).toMatchExactly([ - { date: JAN_10TH }, - { date: JAN_1ST }, - { dateid: NULL_DATE__ID }, - ]) - }) - }) - }) - } - ) + }) + }) } ) } diff --git a/packages/server/src/utilities/schema.ts b/packages/server/src/utilities/schema.ts index cfdd0d753a..6a30bb5688 100644 --- a/packages/server/src/utilities/schema.ts +++ b/packages/server/src/utilities/schema.ts @@ -7,7 +7,7 @@ import { Table, } from "@budibase/types" import { ValidColumnNameRegex, helpers, utils } from "@budibase/shared-core" -import { db } from "@budibase/backend-core" +import { db, HTTPError, sql } from "@budibase/backend-core" type Rows = Array @@ -180,10 +180,26 @@ export function parse(rows: Rows, table: Table): Rows { !columnSchema.timeOnly && !columnSchema.dateOnly ) { - // If provided must be a valid date + if (columnData && !columnSchema.timeOnly) { + if (!sql.utils.isValidISODateString(columnData)) { + let message = `Invalid format for field "${columnName}": "${columnData}".` + if (columnSchema.dateOnly) { + message += ` Date-only fields must be in the format "YYYY-MM-DD".` + } else { + message += ` Datetime fields must be in ISO format, e.g. "YYYY-MM-DDTHH:MM:SSZ".` + } + throw new HTTPError(message, 400) + } + } + if (columnData && columnSchema.timeOnly) { + if (!sql.utils.isValidTime(columnData)) { + throw new HTTPError( + `Invalid format for field "${columnName}": "${columnData}". Time-only fields must be in the format "HH:MM:SS".`, + 400 + ) + } + } parsedRow[columnName] = columnData - ? new Date(columnData).toISOString() - : columnData } else if ( columnType === FieldType.JSON && typeof columnData === "string" From 0bc1e33ef3e79c3edc9485c0017674f87e2f6d16 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Mon, 13 Jan 2025 18:20:22 +0000 Subject: [PATCH 06/17] pr comments --- packages/builder/src/stores/builder/views.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/views.ts b/packages/builder/src/stores/builder/views.ts index 4e309e180d..81085fcb42 100644 --- a/packages/builder/src/stores/builder/views.ts +++ b/packages/builder/src/stores/builder/views.ts @@ -55,7 +55,7 @@ export class ViewsStore extends DerivedBudiStore< delete = async (view: View) => { if (!view.name) { - return + throw new Error("View name is required") } await API.deleteView(view.name) @@ -71,7 +71,7 @@ export class ViewsStore extends DerivedBudiStore< save = async (view: View & { originalName?: string }) => { if (!view.name) { - return + throw new Error("View name is required") } const savedView = await API.saveView(view) From 55f3dbcce54e1c5d323187eaee02f237b97b2b08 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 14 Jan 2025 14:03:59 +0000 Subject: [PATCH 07/17] Fix tests. --- packages/server/src/api/routes/tests/row.spec.ts | 9 +++------ packages/server/src/utilities/schema.ts | 6 +----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 737ac2863a..db5fcbaebb 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -2130,13 +2130,10 @@ if (descriptions.length) { ], }, { - // This isn't ideal atm because it doesn't line up with datetime - // and date only error messages, but there's a check earlier in - // the stack than when those errors happen that produces this one, - // and it's not easy to bypass. The key is that this fails. - status: 500, + status: 400, body: { - message: 'Invalid date value: "3pm"', + message: + 'Invalid format for field "time": "3pm". Time-only fields must be in the format "HH:MM:SS".', }, } ) diff --git a/packages/server/src/utilities/schema.ts b/packages/server/src/utilities/schema.ts index 6a30bb5688..b13999c842 100644 --- a/packages/server/src/utilities/schema.ts +++ b/packages/server/src/utilities/schema.ts @@ -175,11 +175,7 @@ export function parse(rows: Rows, table: Table): Rows { if ([FieldType.NUMBER].includes(columnType)) { // If provided must be a valid number parsedRow[columnName] = columnData ? Number(columnData) : columnData - } else if ( - columnType === FieldType.DATETIME && - !columnSchema.timeOnly && - !columnSchema.dateOnly - ) { + } else if (columnType === FieldType.DATETIME) { if (columnData && !columnSchema.timeOnly) { if (!sql.utils.isValidISODateString(columnData)) { let message = `Invalid format for field "${columnName}": "${columnData}".` From 3715bd0bed58432a13657126e69b222df6762a86 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 14 Jan 2025 16:51:31 +0100 Subject: [PATCH 08/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 193476cdfa..19295c7e4d 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 193476cdfade6d3c613e6972f16ee0c527e01ff6 +Subproject commit 19295c7e4d223bec493a108a905c110d5f024258 From ccf3e787011ff11838f48fa9b569d50345b2d5cf Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Tue, 14 Jan 2025 23:35:18 +0100 Subject: [PATCH 09/17] Update pro submodule --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 19295c7e4d..a4f63b2267 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 19295c7e4d223bec493a108a905c110d5f024258 +Subproject commit a4f63b22675e16dcdcaa4d9e83b298eee6466a07 From 4c1013e4d37c070eeb8ffb2b71fbd03f81b2c79d Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 15 Jan 2025 09:04:44 +0000 Subject: [PATCH 10/17] Bump version to 3.2.43 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index d0f0bd23c5..1aa1705d19 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.42", + "version": "3.2.43", "npmClient": "yarn", "concurrency": 20, "command": { From 2b0f8091e774f4046dfaaf525698e3ad72e50331 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 15 Jan 2025 10:48:03 +0100 Subject: [PATCH 11/17] Add string logging --- scripts/build.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/build.js b/scripts/build.js index a745f49cfb..c18f637fab 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -47,6 +47,9 @@ let { argv } = require("yargs") async function runBuild(entry, outfile) { const isDev = process.env.NODE_ENV !== "production" + + console.log(`Building in mode ${process.env.NODE_ENV}`) + const tsconfig = argv["p"] || `tsconfig.build.json` const { data: tsconfigPathPluginContent } = loadTsConfig( From aa488ae8942197bbed46e723b5fc296e872f882b Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 15 Jan 2025 11:03:05 +0100 Subject: [PATCH 12/17] Fix is dev --- scripts/build.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index c18f637fab..a77329765f 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -46,9 +46,9 @@ const svelteCompilePlugin = { let { argv } = require("yargs") async function runBuild(entry, outfile) { - const isDev = process.env.NODE_ENV !== "production" + const isDev = !process.env.CI - console.log(`Building in mode ${process.env.NODE_ENV}`) + console.log(`Building in mode dev mode: ${isDev}`) const tsconfig = argv["p"] || `tsconfig.build.json` From f2fba9da0888aa88aa380e72ff1704b1e9603e45 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 15 Jan 2025 11:03:20 +0100 Subject: [PATCH 13/17] Sourcemap from tsconfig.json --- scripts/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build.js b/scripts/build.js index a77329765f..e8e52d267c 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -61,7 +61,7 @@ async function runBuild(entry, outfile) { entryPoints: [entry], bundle: true, minify: !isDev, - sourcemap: isDev, + sourcemap: tsconfigPathPluginContent.compilerOptions.sourceMap, tsconfig, plugins: [ svelteCompilePlugin, From 3751886304020c20fc3862d04fa0316e94c1acdd Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 15 Jan 2025 11:03:27 +0100 Subject: [PATCH 14/17] Default on --- tsconfig.build.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.build.json b/tsconfig.build.json index a05fa2c976..0e22ddf964 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -11,6 +11,7 @@ "declaration": true, "isolatedModules": true, "baseUrl": ".", + "sourceMap": true, "paths": { "@budibase/types": ["./packages/types/src"], "@budibase/backend-core": ["./packages/backend-core/src"], From 322d3cbacb8a35c5ee624ed971e0dc411458cec5 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 15 Jan 2025 11:03:48 +0100 Subject: [PATCH 15/17] Generate meta only if in dev --- scripts/build.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index e8e52d267c..0106421fe7 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -128,10 +128,12 @@ async function runBuild(entry, outfile) { await Promise.all([hbsFiles, mainBuild, oldClientVersions]) - fs.writeFileSync( - `dist/${path.basename(outfile)}.meta.json`, - JSON.stringify((await mainBuild).metafile) - ) + if (isDev) { + fs.writeFileSync( + `dist/${path.basename(outfile)}.meta.json`, + JSON.stringify((await mainBuild).metafile) + ) + } console.log( "\x1b[32m%s\x1b[0m", From 1be1787bdc56655f92e79395abdecd481e6ae474 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 15 Jan 2025 11:04:15 +0100 Subject: [PATCH 16/17] Don't publish sourcemap for pro --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index a4f63b2267..43a5785ccb 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit a4f63b22675e16dcdcaa4d9e83b298eee6466a07 +Subproject commit 43a5785ccb4f83ce929b29f05ea0a62199fcdf23 From 29e9c9bb678c3a41d95bb5ac81107515c261422c Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 15 Jan 2025 11:16:09 +0000 Subject: [PATCH 17/17] Bump version to 3.2.44 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 1aa1705d19..8d7179dbae 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.43", + "version": "3.2.44", "npmClient": "yarn", "concurrency": 20, "command": {