Get rid of negation in predicate.
This commit is contained in:
parent
7e4f571eb3
commit
ae6539161f
|
@ -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 }] },
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue