From 22bf0d05ad3dfca3a1deec7975d14dab547421c8 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 12 Jun 2024 17:58:13 +0100 Subject: [PATCH] Making progress. --- .../src/api/routes/tests/search.spec.ts | 19 +++-- packages/shared-core/src/filters.ts | 70 +++++++++++-------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 3cafec1b9a..8f4d96d5e8 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -1706,7 +1706,7 @@ describe.each([ }) describe("contains", () => { - it.only("successfully finds a row", () => + it("successfully finds a row", () => expectQuery({ contains: { users: [user1._id] } }).toContainExactly([ { users: [{ _id: user1._id }] }, { users: [{ _id: user1._id }, { _id: user2._id }] }, @@ -1763,9 +1763,12 @@ describe.each([ // This will never work for Lucene. !isLucene && + // It also can't work for in-memory searching because the related table name + // isn't available. + !isInMemory && describe("relations", () => { let otherTable: Table - let rows: Row[] + let otherRows: Row[] beforeAll(async () => { otherTable = await createTable({ @@ -1785,7 +1788,7 @@ describe.each([ }, }) - rows = await Promise.all([ + otherRows = await Promise.all([ config.api.row.save(otherTable._id!, { one: "foo" }), config.api.row.save(otherTable._id!, { one: "bar" }), ]) @@ -1793,18 +1796,22 @@ describe.each([ await Promise.all([ config.api.row.save(table._id!, { two: "foo", - other: [rows[0]._id], + other: [otherRows[0]._id], }), config.api.row.save(table._id!, { two: "bar", - other: [rows[1]._id], + other: [otherRows[1]._id], }), ]) + + rows = await config.api.row.fetch(table._id!) }) it("can search through relations", () => expectQuery({ equal: { [`${otherTable.name}.one`]: "foo" }, - }).toContainExactly([{ two: "foo", other: [{ _id: rows[0]._id }] }])) + }).toContainExactly([ + { two: "foo", other: [{ _id: otherRows[0]._id }] }, + ])) }) }) diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index e95015d340..4ccbc60641 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -14,6 +14,7 @@ import { import dayjs from "dayjs" import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants" import { deepGet, schema } from "./helpers" +import _ from "lodash" const HBS_REGEX = /{{([^{].*?)}}/g @@ -339,15 +340,36 @@ export const runQuery = ( } ) + // This function exists to check that either the docValue is equal to the + // testValue, or if the docValue is an object or array of objects, that the + // _id of the docValue is equal to the testValue. + const _valueMatches = (docValue: any, testValue: any) => { + if (Array.isArray(docValue)) { + for (const item of docValue) { + if (_valueMatches(item, testValue)) { + return true + } + } + return false + } + + if (typeof docValue === "object" && typeof testValue === "string") { + return docValue._id === testValue + } + + return docValue === testValue + } + const not = (f: (...args: T) => boolean) => (...args: T): boolean => !f(...args) - const _equal = (docValue: any, testValue: any) => docValue === testValue - - const equalMatch = match(SearchFilterOperator.EQUAL, _equal) - const notEqualMatch = match(SearchFilterOperator.NOT_EQUAL, not(_equal)) + const equalMatch = match(SearchFilterOperator.EQUAL, _valueMatches) + const notEqualMatch = match( + SearchFilterOperator.NOT_EQUAL, + not(_valueMatches) + ) const _empty = (docValue: any) => { if (typeof docValue === "string") { @@ -379,13 +401,12 @@ export const runQuery = ( return false } - return testValue.includes(docValue) + return testValue.some(item => _valueMatches(docValue, item)) } ) - const containsAny = match( - SearchFilterOperator.CONTAINS_ANY, - (docValue: any, testValue: any) => { + const _contains = + (f: "some" | "every") => (docValue: any, testValue: any) => { if (!Array.isArray(docValue)) { return false } @@ -401,31 +422,18 @@ export const runQuery = ( return false } - return testValue.some(item => docValue.includes(item)) + return testValue[f](item => _valueMatches(docValue, item)) } + + const contains = match(SearchFilterOperator.CONTAINS, _contains("every")) + const notContains = match( + SearchFilterOperator.NOT_CONTAINS, + not(_contains("every")) + ) + const containsAny = match( + SearchFilterOperator.CONTAINS_ANY, + _contains("some") ) - - const _contains = (docValue: any, testValue: any) => { - if (!Array.isArray(docValue)) { - return false - } - - if (typeof testValue === "string") { - testValue = testValue.split(",") - if (typeof docValue[0] === "number") { - testValue = testValue.map((item: string) => parseFloat(item)) - } - } - - if (!Array.isArray(testValue)) { - return false - } - - return testValue.every(item => docValue.includes(item)) - } - - const contains = match(SearchFilterOperator.CONTAINS, _contains) - const notContains = match(SearchFilterOperator.NOT_CONTAINS, not(_contains)) const docMatch = (doc: Record) => { const filterFunctions = {