Get rid of negation in predicate.

This commit is contained in:
Sam Rose 2024-06-12 17:28:03 +01:00
parent 7e4f571eb3
commit ae6539161f
No known key found for this signature in database
2 changed files with 117 additions and 98 deletions

View File

@ -1706,7 +1706,7 @@ describe.each([
}) })
describe("contains", () => { describe("contains", () => {
it("successfully finds a row", () => it.only("successfully finds a row", () =>
expectQuery({ contains: { users: [user1._id] } }).toContainExactly([ expectQuery({ contains: { users: [user1._id] } }).toContainExactly([
{ users: [{ _id: user1._id }] }, { users: [{ _id: user1._id }] },
{ users: [{ _id: user1._id }, { _id: user2._id }] }, { users: [{ _id: user1._id }, { _id: user2._id }] },

View File

@ -264,7 +264,10 @@ export const buildQuery = (filter: SearchFilter[]) => {
* @param docs the data * @param docs the data
* @param query the JSON query * @param query the JSON query
*/ */
export const runQuery = (docs: any[], query?: SearchFilters) => { export const runQuery = (
docs: Record<string, any>[],
query?: SearchFilters
) => {
if (!docs || !Array.isArray(docs)) { if (!docs || !Array.isArray(docs)) {
return [] return []
} }
@ -272,50 +275,48 @@ export const runQuery = (docs: any[], query?: SearchFilters) => {
return docs return docs
} }
// Make query consistent first
query = cleanupQuery(query) query = cleanupQuery(query)
// Iterates over a set of filters and evaluates a fail function against a doc
const match = const match =
( (
type: SearchFilterOperator, type: SearchFilterOperator,
failFn: (docValue: any, testValue: any) => boolean test: (docValue: any, testValue: any) => boolean
) => ) =>
(doc: any) => { (doc: Record<string, any>) => {
const filters = Object.entries(query![type] || {}) for (const [key, testValue] of Object.entries(query[type] || {})) {
for (let i = 0; i < filters.length; i++) { if (!test(deepGet(doc, removeKeyNumbering(key)), testValue)) {
const [key, testValue] = filters[i]
const docValue = deepGet(doc, removeKeyNumbering(key))
if (failFn(docValue, testValue)) {
return false return false
} }
} }
return true return true
} }
// Process a string match (fails if the value does not start with the string)
const stringMatch = match( const stringMatch = match(
SearchFilterOperator.STRING, SearchFilterOperator.STRING,
(docValue: string, testValue: string) => { (docValue: any, testValue: any) => {
return ( if (!(typeof docValue === "string")) {
!docValue || return false
!docValue?.toLowerCase().startsWith(testValue?.toLowerCase()) }
) if (!(typeof testValue === "string")) {
return false
}
return docValue.toLowerCase().startsWith(testValue.toLowerCase())
} }
) )
// Process a fuzzy match (treat the same as starts with when running locally)
const fuzzyMatch = match( const fuzzyMatch = match(
SearchFilterOperator.FUZZY, SearchFilterOperator.FUZZY,
(docValue: string, testValue: string) => { (docValue: any, testValue: any) => {
return ( if (!(typeof docValue === "string")) {
!docValue || return false
!docValue?.toLowerCase().startsWith(testValue?.toLowerCase()) }
) if (!(typeof testValue === "string")) {
return false
}
return docValue.toLowerCase().includes(testValue.toLowerCase())
} }
) )
// Process a range match
const rangeMatch = match( const rangeMatch = match(
SearchFilterOperator.RANGE, SearchFilterOperator.RANGE,
( (
@ -323,54 +324,47 @@ export const runQuery = (docs: any[], query?: SearchFilters) => {
testValue: { low: number; high: number } testValue: { low: number; high: number }
) => { ) => {
if (docValue == null || docValue === "") { if (docValue == null || docValue === "") {
return true return false
} }
if (!isNaN(+docValue)) { if (!isNaN(+docValue)) {
return +docValue < testValue.low || +docValue > testValue.high return +docValue >= testValue.low || +docValue <= testValue.high
} }
if (dayjs(docValue).isValid()) { if (dayjs(docValue).isValid()) {
return ( return (
new Date(docValue).getTime() < new Date(testValue.low).getTime() || new Date(docValue).getTime() >= new Date(testValue.low).getTime() ||
new Date(docValue).getTime() > new Date(testValue.high).getTime() new Date(docValue).getTime() <= new Date(testValue.high).getTime()
) )
} }
return false return false
} }
) )
// Process an equal match (fails if the value is different) const not =
const equalMatch = match( <T extends any[]>(f: (...args: T) => boolean) =>
SearchFilterOperator.EQUAL, (...args: T): boolean =>
(docValue: any, testValue: string | null) => { !f(...args)
return testValue != null && testValue !== "" && docValue !== testValue
}
)
// Process a not-equal match (fails if the value is the same) const _equal = (docValue: any, testValue: any) => docValue === testValue
const notEqualMatch = match(
SearchFilterOperator.NOT_EQUAL,
(docValue: any, testValue: string | null) => {
return testValue != null && testValue !== "" && docValue === testValue
}
)
// Process an empty match (fails if the value is not empty) const equalMatch = match(SearchFilterOperator.EQUAL, _equal)
const emptyMatch = match( const notEqualMatch = match(SearchFilterOperator.NOT_EQUAL, not(_equal))
SearchFilterOperator.EMPTY,
(docValue: string | null) => {
return docValue != null && docValue !== ""
}
)
// Process a not-empty match (fails is the value is empty) const _empty = (docValue: any) => {
const notEmptyMatch = match( if (typeof docValue === "string") {
SearchFilterOperator.NOT_EMPTY, return docValue === ""
(docValue: string | null) => {
return docValue == null || docValue === ""
} }
) if (Array.isArray(docValue)) {
return docValue.length === 0
}
if (typeof docValue === "object") {
return Object.keys(docValue).length === 0
}
return docValue == null
}
const emptyMatch = match(SearchFilterOperator.EMPTY, _empty)
const notEmptyMatch = match(SearchFilterOperator.NOT_EMPTY, not(_empty))
// Process an includes match (fails if the value is not included)
const oneOf = match( const oneOf = match(
SearchFilterOperator.ONE_OF, SearchFilterOperator.ONE_OF,
(docValue: any, testValue: any) => { (docValue: any, testValue: any) => {
@ -380,61 +374,86 @@ export const runQuery = (docs: any[], query?: SearchFilters) => {
testValue = testValue.map((item: string) => parseFloat(item)) testValue = testValue.map((item: string) => parseFloat(item))
} }
} }
return !testValue?.includes(docValue)
if (!Array.isArray(testValue)) {
return false
}
return testValue.includes(docValue)
} }
) )
const containsAny = match( const containsAny = match(
SearchFilterOperator.CONTAINS_ANY, SearchFilterOperator.CONTAINS_ANY,
(docValue: any, testValue: any) => { (docValue: any, testValue: any) => {
return !docValue?.includes(...testValue) if (!Array.isArray(docValue)) {
} return false
)
const contains = match(
SearchFilterOperator.CONTAINS,
(docValue: string | any[], testValue: any[]) => {
return !testValue?.every((item: any) => docValue?.includes(item))
}
)
const notContains = match(
SearchFilterOperator.NOT_CONTAINS,
(docValue: string | any[], testValue: any[]) => {
return testValue?.every((item: any) => docValue?.includes(item))
}
)
const docMatch = (doc: any) => {
const filterFunctions: Record<SearchFilterOperator, (doc: any) => boolean> =
{
string: stringMatch,
fuzzy: fuzzyMatch,
range: rangeMatch,
equal: equalMatch,
notEqual: notEqualMatch,
empty: emptyMatch,
notEmpty: notEmptyMatch,
oneOf: oneOf,
contains: contains,
containsAny: containsAny,
notContains: notContains,
} }
const activeFilterKeys: SearchFilterOperator[] = Object.entries(query || {}) 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.some(item => docValue.includes(item))
}
)
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<string, any>) => {
const filterFunctions = {
string: stringMatch,
fuzzy: fuzzyMatch,
range: rangeMatch,
equal: equalMatch,
notEqual: notEqualMatch,
empty: emptyMatch,
notEmpty: notEmptyMatch,
oneOf: oneOf,
contains: contains,
containsAny: containsAny,
notContains: notContains,
}
const results = Object.entries(query || {})
.filter( .filter(
([key, value]: [string, any]) => ([key, value]) =>
!["allOr", "onEmptyFilter"].includes(key) && !["allOr", "onEmptyFilter"].includes(key) &&
value && value &&
Object.keys(value as Record<string, any>).length > 0 Object.keys(value).length > 0
) )
.map(([key]) => key as any) .map(([key]) => {
return filterFunctions[key as SearchFilterOperator]?.(doc) ?? false
})
const results: boolean[] = activeFilterKeys.map(filterKey => { if (query.allOr) {
return filterFunctions[filterKey]?.(doc) ?? false
})
if (query!.allOr) {
return results.some(result => result === true) return results.some(result => result === true)
} else { } else {
return results.every(result => result === true) return results.every(result => result === true)