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[]