From 714029b9a036b16f02d3c4c4b411bc8c6f814fd2 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 17 Oct 2024 18:06:45 +0100 Subject: [PATCH] Making more progress on testing view searching. --- .../src/api/routes/tests/viewV2.spec.ts | 148 ++++++++++++++++++ packages/server/src/sdk/app/rows/search.ts | 6 - packages/shared-core/src/filters.ts | 4 +- 3 files changed, 151 insertions(+), 7 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 71506da0fa..c2626155c9 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -29,12 +29,18 @@ import { JsonTypes, FilterGroupLogicalOperator, EmptyFilterOption, + JsonFieldSubType, + SearchFilterGroup, + LegacyFilter, + SearchViewRowRequest, + SearchFilterChild, } from "@budibase/types" import { generator, mocks } from "@budibase/backend-core/tests" import { DatabaseName, getDatasource } from "../../../integrations/tests/utils" import merge from "lodash/merge" import { quotas } from "@budibase/pro" import { db, roles, features } from "@budibase/backend-core" +import { single } from "validate.js" describe.each([ ["lucene", undefined], @@ -3657,6 +3663,148 @@ describe.each([ expect(rows).toHaveLength(1) expect(rows[0].user._id).toEqual(config.getUser()._id) }) + + describe("search operators", () => { + let table: Table + beforeEach(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + string: { name: "string", type: FieldType.STRING }, + longform: { name: "longform", type: FieldType.LONGFORM }, + options: { + name: "options", + type: FieldType.OPTIONS, + constraints: { inclusion: ["a", "b", "c"] }, + }, + array: { + name: "array", + type: FieldType.ARRAY, + constraints: { + type: JsonFieldSubType.ARRAY, + inclusion: ["a", "b", "c"], + }, + }, + number: { name: "number", type: FieldType.NUMBER }, + bigint: { name: "bigint", type: FieldType.BIGINT }, + datetime: { name: "datetime", type: FieldType.DATETIME }, + timeOnly: { + name: "timeOnly", + type: FieldType.DATETIME, + timeOnly: true, + }, + boolean: { name: "boolean", type: FieldType.BOOLEAN }, + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + }, + users: { + name: "users", + type: FieldType.BB_REFERENCE, + subtype: BBReferenceFieldSubType.USER, + }, + }, + }) + ) + }) + + interface TestCase { + name: string + query: SearchFilterGroup + insert: Row[] + expected: Row[] + searchOpts?: SearchViewRowRequest + } + + function defaultQuery( + query: Partial + ): SearchFilterGroup { + return { + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + logicalOperator: FilterGroupLogicalOperator.ALL, + groups: [], + ...query, + } + } + + function defaultGroup( + group: Partial + ): SearchFilterChild { + return { + logicalOperator: FilterGroupLogicalOperator.ALL, + filters: [], + ...group, + } + } + + function simpleQuery(...filters: LegacyFilter[]): SearchFilterGroup { + return defaultQuery({ groups: [defaultGroup({ filters })] }) + } + + const testCases: TestCase[] = [ + { + name: "empty query return all", + insert: [{ string: "foo" }], + query: defaultQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + }), + expected: [{ string: "foo" }], + }, + { + name: "empty query return none", + insert: [{ string: "foo" }], + query: defaultQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + }), + expected: [], + }, + { + name: "simple string search", + insert: [{ string: "foo" }], + query: simpleQuery({ + operator: BasicOperator.EQUAL, + field: "string", + value: "foo", + }), + expected: [{ string: "foo" }], + }, + { + name: "non matching string search", + insert: [{ string: "foo" }], + query: simpleQuery({ + operator: BasicOperator.EQUAL, + field: "string", + value: "bar", + }), + expected: [], + }, + ] + + it.only.each(testCases)( + "$name", + async ({ query, insert, expected, searchOpts }) => { + await config.api.row.bulkImport(table._id!, { rows: insert }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + queryUI: query, + schema: { + string: { visible: true }, + }, + }) + + const { rows } = await config.api.viewV2.search( + view.id, + searchOpts + ) + expect(rows).toEqual( + expected.map(r => expect.objectContaining(r)) + ) + } + ) + }) }) describe("permissions", () => { diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index 7ac3bb8ead..6abfe0c681 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -89,9 +89,6 @@ export async function search( options = searchInputMapping(table, options) if (options.viewId) { - // Delete extraneous search params that cannot be overridden - delete options.query.onEmptyFilter - const view = source as ViewV2 // Enrich saved query with ephemeral query params. // We prevent searching on any fields that are saved as part of the query, as @@ -99,7 +96,6 @@ export async function search( let viewQuery = await enrichSearchContext(view.query || {}, context) viewQuery = dataFilters.buildQueryLegacy(viewQuery) || {} viewQuery = checkFilters(table, viewQuery) - delete viewQuery?.onEmptyFilter const sqsEnabled = await features.flags.isEnabled("SQS") const supportsLogicalOperators = @@ -112,8 +108,6 @@ export async function search( ? view.query : [] - delete options.query.onEmptyFilter - // Extract existing fields const existingFields = queryFilters diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index 721cc7565c..756a814710 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -600,7 +600,7 @@ export function buildQuery( const globalOperator = operatorMap[parsedFilter.logicalOperator] - return { + const ret = { ...(globalOnEmpty ? { onEmptyFilter: globalOnEmpty } : {}), [globalOperator]: { conditions: parsedFilter.groups?.map(group => { @@ -614,6 +614,8 @@ export function buildQuery( }), }, } + + return ret } // The frontend can send single values for array fields sometimes, so to handle