From 11f49c95dc343179f030c60d5c27edcee812e721 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 1 Jul 2024 09:47:58 +0100 Subject: [PATCH 1/4] Fix contains search on multi-user column. --- packages/backend-core/src/sql/sql.ts | 18 +++++++++++++++--- .../server/src/api/routes/tests/search.spec.ts | 9 +++++++++ packages/types/src/sdk/search.ts | 2 +- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 34b950bf2c..3ba073aef0 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -246,7 +246,11 @@ class InternalBuilder { return `[${value.join(",")}]` } if (this.client === SqlClient.POSTGRES) { - iterate(mode, (key: string, value: Array) => { + iterate(mode, (key: string, value: any) => { + if (!Array.isArray(value)) { + value = [value] + } + const wrap = any ? "" : "'" const op = any ? "\\?| array" : "@>" const fieldNames = key.split(/\./g) @@ -261,7 +265,11 @@ class InternalBuilder { }) } else if (this.client === SqlClient.MY_SQL) { const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS" - iterate(mode, (key: string, value: Array) => { + iterate(mode, (key: string, value: any) => { + if (!Array.isArray(value)) { + value = [value] + } + query = query[rawFnc]( `${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray( value @@ -270,7 +278,11 @@ class InternalBuilder { }) } else { const andOr = mode === filters?.containsAny ? " OR " : " AND " - iterate(mode, (key: string, value: Array) => { + iterate(mode, (key: string, value: any) => { + if (!Array.isArray(value)) { + value = [value] + } + let statement = "" for (let i in value) { if (typeof value[i] === "string") { diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 589f129f31..bc253ad78e 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1938,6 +1938,15 @@ describe.each([ ]) }) + it("successfully finds a row searching with a string", async () => { + await expectQuery({ + contains: { "1:users": user1._id }, + }).toContainExactly([ + { users: [{ _id: user1._id }] }, + { users: [{ _id: user1._id }, { _id: user2._id }] }, + ]) + }) + it("fails to find nonexistent row", async () => { await expectQuery({ contains: { users: ["us_none"] } }).toFindNothing() }) diff --git a/packages/types/src/sdk/search.ts b/packages/types/src/sdk/search.ts index e5cbccf5c1..52428579b7 100644 --- a/packages/types/src/sdk/search.ts +++ b/packages/types/src/sdk/search.ts @@ -54,7 +54,7 @@ export interface SearchFilters { [key: string]: any[] } [SearchFilterOperator.CONTAINS]?: { - [key: string]: any[] + [key: string]: any[] | any } [SearchFilterOperator.NOT_CONTAINS]?: { [key: string]: any[] From 6eb37df92d455d1b3492fdacde6572c53f89053e Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 1 Jul 2024 11:41:21 +0100 Subject: [PATCH 2/4] Respond to PR feedback. --- packages/backend-core/src/sql/sql.ts | 18 +++------------ packages/server/src/sdk/app/rows/search.ts | 27 ++++++++++++++++++++++ packages/types/src/sdk/search.ts | 2 +- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 3ba073aef0..34b950bf2c 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -246,11 +246,7 @@ class InternalBuilder { return `[${value.join(",")}]` } if (this.client === SqlClient.POSTGRES) { - iterate(mode, (key: string, value: any) => { - if (!Array.isArray(value)) { - value = [value] - } - + iterate(mode, (key: string, value: Array) => { const wrap = any ? "" : "'" const op = any ? "\\?| array" : "@>" const fieldNames = key.split(/\./g) @@ -265,11 +261,7 @@ class InternalBuilder { }) } else if (this.client === SqlClient.MY_SQL) { const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS" - iterate(mode, (key: string, value: any) => { - if (!Array.isArray(value)) { - value = [value] - } - + iterate(mode, (key: string, value: Array) => { query = query[rawFnc]( `${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray( value @@ -278,11 +270,7 @@ class InternalBuilder { }) } else { const andOr = mode === filters?.containsAny ? " OR " : " AND " - iterate(mode, (key: string, value: any) => { - if (!Array.isArray(value)) { - value = [value] - } - + iterate(mode, (key: string, value: Array) => { let statement = "" for (let i in value) { if (typeof value[i] === "string") { diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index 1dc0e37a0c..286a88054c 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -2,6 +2,7 @@ import { EmptyFilterOption, Row, RowSearchParams, + SearchFilterOperator, SearchFilters, SearchResponse, SortOrder, @@ -65,11 +66,37 @@ export function removeEmptyFilters(filters: SearchFilters) { return filters } +// The frontend can send single values for array fields sometimes, so to handle +// this we convert them to arrays at the controller level so that nothing below +// this has to worry about the non-array values. +function fixupFilterArrays(filters: SearchFilters) { + const arrayFields = [ + SearchFilterOperator.ONE_OF, + SearchFilterOperator.CONTAINS, + SearchFilterOperator.NOT_CONTAINS, + SearchFilterOperator.CONTAINS_ANY, + ] + for (const searchField of arrayFields) { + const field = filters[searchField] + if (field == null) { + continue + } + + for (const key of Object.keys(field)) { + if (!Array.isArray(field[key])) { + field[key] = [field[key]] + } + } + } + return filters +} + export async function search( options: RowSearchParams ): Promise> { const isExternalTable = isExternalTableID(options.tableId) options.query = removeEmptyFilters(options.query || {}) + options.query = fixupFilterArrays(options.query) if ( !dataFilters.hasFilters(options.query) && options.query.onEmptyFilter === EmptyFilterOption.RETURN_NONE diff --git a/packages/types/src/sdk/search.ts b/packages/types/src/sdk/search.ts index 52428579b7..e5cbccf5c1 100644 --- a/packages/types/src/sdk/search.ts +++ b/packages/types/src/sdk/search.ts @@ -54,7 +54,7 @@ export interface SearchFilters { [key: string]: any[] } [SearchFilterOperator.CONTAINS]?: { - [key: string]: any[] | any + [key: string]: any[] } [SearchFilterOperator.NOT_CONTAINS]?: { [key: string]: any[] From 5c09d6f41fcd22ffaf87905eabc625ef2f2f24c7 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 1 Jul 2024 11:46:35 +0100 Subject: [PATCH 3/4] Fix type error. --- packages/server/src/api/routes/tests/search.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index bc253ad78e..5b98347953 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1940,6 +1940,8 @@ describe.each([ it("successfully finds a row searching with a string", async () => { await expectQuery({ + // @ts-expect-error this test specifically goes against the type to + // test that we coerce the string to an array. contains: { "1:users": user1._id }, }).toContainExactly([ { users: [{ _id: user1._id }] }, From ba6dc9ff397d25395d6cd790c6d833e94b3c4bfe Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Mon, 1 Jul 2024 13:06:38 +0000 Subject: [PATCH 4/4] Bump version to 2.29.5 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index c218525324..1bce056679 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.29.4", + "version": "2.29.5", "npmClient": "yarn", "packages": [ "packages/*",