From 829a024bfa3ad26696b6c34d5d18a79fbd0ac93b Mon Sep 17 00:00:00 2001 From: ChronicStone Date: Thu, 18 Jul 2024 10:05:27 +0200 Subject: [PATCH] refactor(query): clean-up implementation, lazily evaluate filters in less iterations --- src/query.ts | 295 ++++------------ src/utils.ts | 89 ++++- test/fixtures/filtering.fixture.json | 507 +-------------------------- test/index.test.ts | 4 +- 4 files changed, 151 insertions(+), 744 deletions(-) diff --git a/src/query.ts b/src/query.ts index 1b22a6f..cda1476 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,169 +1,14 @@ -import type { FilterMatchMode, GenericObject, MatchModeProcessorMap, QueryFilter, QueryFilterGroup, QueryParams, QueryResult } from './types' -import { MatchModeProcessor, getObjectProperty, validateBetweenPayload } from './utils' +import type { GenericObject, QueryFilter, QueryFilterGroup, QueryParams, QueryResult } from './types' +import { getObjectProperty, processFilterWithLookup, processSearchQuery } from './utils' export function query>( data: T[], params: P, ): QueryResult { - let result: T[] = [...data] + let result = Array.from(lazyQuery(data, params)) - if (params.search && params.search.value) { - result = result.filter((item) => { - for (const key of params.search?.keys ?? []) { - const field = typeof key === 'string' ? key : key.key - const caseSensitive = typeof key === 'string' ? (params.search?.caseSensitive ?? false) : key.caseSensitive ?? false - if (processSearchQuery({ key: field, caseSensitive, object: item, value: params.search!.value })) - return true - } - return false - }) - } - - if (params.sort) { - const sortArray = Array.isArray(params.sort) ? params.sort : [params.sort] - - result = result.sort((a, b) => { - for (const { key, dir, parser } of sortArray) { - const parserHandler = typeof parser === 'function' ? parser : (v: any) => parser === 'number' ? Number(v) : parser === 'boolean' ? Boolean(v) : parser === 'string' ? String(v) : v - const aParsed = parserHandler(getObjectProperty(a, key)) ?? null - const bParsed = parserHandler(getObjectProperty(b, key)) ?? null - - if (aParsed !== bParsed) { - const comparison = (aParsed < bParsed) ? -1 : 1 - return dir === 'asc' ? comparison : -comparison - } - } - return 0 - }) - } - - if (Array.isArray(params.filter) && params.filter.length) { - result = result.filter((item) => { - const IS_GROUP = params.filter?.every(filter => 'filters' in filter) ?? false - const METHOD = IS_GROUP ? 'some' : 'every' as const - const FILTERS = (params?.filter ?? []) as any[] - return FILTERS[METHOD]((group: QueryFilter | QueryFilterGroup) => { - const filters = 'filters' in group ? group.filters : [group] - const op = 'filters' in group ? group.operator : 'OR' - return filters[op === 'AND' ? 'every' : 'some' as const]((filter) => { - const value = getObjectProperty(item, filter.key) - const operator = typeof filter.operator === 'function' ? filter.operator() : filter.operator ?? 'OR' - if (filter.matchMode === 'equals') { - return processFilterWithLookup({ - type: 'equals', - params: null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'contains') { - return processFilterWithLookup({ - type: 'contains', - params: null, - operator, - value, - filter: filter.value, - }) - } - if (filter.matchMode === 'between') { - return processFilterWithLookup({ - type: 'between', - params: filter?.params ?? null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'greaterThan') { - return processFilterWithLookup({ - type: 'greaterThan', - params: filter?.params ?? null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'greaterThanOrEqual') { - return processFilterWithLookup({ - type: 'greaterThanOrEqual', - params: filter?.params ?? null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'lessThan') { - return processFilterWithLookup({ - type: 'lessThan', - params: filter?.params ?? null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'lessThanOrEqual') { - return processFilterWithLookup({ - type: 'lessThanOrEqual', - params: filter?.params ?? null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'exists') { - return processFilterWithLookup({ - type: 'exists', - params: null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'arrayLength') { - return processFilterWithLookup({ - type: 'arrayLength', - params: null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'regex') { - return processFilterWithLookup({ - type: 'regex', - params: filter?.params ?? null, - operator, - value, - filter: filter.value, - }) - } - - if (filter.matchMode === 'objectMatch') { - const params = typeof filter.params === 'function' ? filter.params(filter.value) : filter.params - const filterValue = params?.transformFilterValue?.(filter.value) ?? filter.value - return processFilterWithLookup({ - type: 'objectMatch', - params, - operator, - value: params?.applyAtRoot ? item : value, - filter: filterValue, - }) - } - - return false - }) - }) - }) - } + if (params.sort) + result = sortedQuery(result, params.sort) if (typeof params.limit === 'undefined') { return { rows: result } as QueryResult @@ -186,88 +31,68 @@ export function query>( } } -function parseSearchValue(value: any, caseSensitive: boolean): string { - return (caseSensitive ? value?.toString() : value?.toString()?.toLowerCase?.()) ?? '' +function* lazyQuery(data: T[], params: QueryParams): Generator { + for (const item of data) { + if (matchesSearchAndFilters(item, params)) { + yield item + } + } } -function processSearchQuery(params: { key: string, object: Record, value: string, caseSensitive: boolean }): boolean { - const { key, object, value, caseSensitive } = params - const keys = key.split('.') - - let current: any = object - for (let i = 0; i < keys.length; i++) { - if (Array.isArray(current)) - return current.some(item => processSearchQuery({ key: keys.slice(i).join('.'), object: item, value, caseSensitive })) - - else if (current && Object.prototype.hasOwnProperty.call(current, keys[i])) - current = current[keys[i]] - - else - return false - } +function matchesSearchAndFilters(item: T, params: QueryParams): boolean { + return matchesSearch(item, params.search) && matchesFilters(item, params.filter) +} - if (Array.isArray(current)) - return current.some(element => parseSearchValue(element, caseSensitive).includes(parseSearchValue(value, caseSensitive))) +function matchesSearch(item: T, search?: QueryParams['search']): boolean { + if (!search || !search.value) + return true - else - return parseSearchValue(current, caseSensitive).includes(parseSearchValue(value, caseSensitive)) ?? false + return search.keys.some((key) => { + const field = typeof key === 'string' ? key : key.key + const caseSensitive = typeof key === 'string' ? (search.caseSensitive ?? false) : key.caseSensitive ?? false + return processSearchQuery({ key: field, caseSensitive, object: item, value: search.value }) + }) } -function processFilterWithLookup< - T extends FilterMatchMode, - P = Parameters[0], ->(params: { - type: FilterMatchMode - operator: 'AND' | 'OR' - value: any - filter: any - params: P extends { params: infer U } ? U : P extends { params?: infer U } ? U : null - lookupFrom?: 'value' | 'filter' -}) { - if (!Array.isArray(params.filter) || (params.type === 'between' && validateBetweenPayload(params.filter))) { - return Array.isArray(params.value) - ? params.value.some(value => - MatchModeProcessor[params.type]({ - params: params.params as any, - value, - filter: params.filter, - }), - ) - : MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter: params.filter }) - } - - else if (params.operator === 'AND') { - return Array.isArray(params.filter) && params.filter.every((filter, index) => { - if (Array.isArray(params.value)) { - return params.value.some(value => - MatchModeProcessor[params.type]({ - params: params.params as any, - value, - filter, - index, - }), - ) - } - else { - return MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter, index }) - } +function matchesFilters(item: T, filters?: (QueryFilter | QueryFilterGroup)[]): boolean { + if (!filters || filters.length === 0) + return true + const isGroup = filters.every(filter => 'filters' in filter) + const method = isGroup ? 'some' : 'every' + return filters[method]((group: QueryFilter | QueryFilterGroup) => { + const groupFilters = 'filters' in group ? group.filters : [group] + const op = 'filters' in group ? group.operator : 'OR' + return groupFilters[op === 'AND' ? 'every' : 'some']((filter: QueryFilter) => { + const value = getObjectProperty(item, filter.key) + const operator = typeof filter.operator === 'function' ? filter.operator() : filter.operator ?? 'OR' + const params = (!('params' in filter) ? null : typeof filter.params === 'function' ? filter.params(filter.value) : filter.params) ?? null + return processFilterWithLookup({ + type: filter.matchMode, + params, + operator, + value, + filter: filter.value, + }) }) - } + }) +} - else if (params.operator === 'OR') { - return Array.isArray(params.filter) && params.filter.some((filter, index) => - Array.isArray(params.value) - ? params.value.some(value => - MatchModeProcessor[params.type]({ - params: params.params as any, - value, - filter, - index, - }), - ) - : MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter, index }), - ) - } +function sortedQuery(data: T[], sortOptions?: QueryParams['sort']): T[] { + if (!sortOptions) + return data - return false + const sortArray = Array.isArray(sortOptions) ? sortOptions : [sortOptions] + return data.slice().sort((a, b) => { + for (const { key, dir, parser } of sortArray) { + const parserHandler = typeof parser === 'function' ? parser : (v: any) => parser === 'number' ? Number(v) : parser === 'boolean' ? Boolean(v) : parser === 'string' ? String(v) : v + const aParsed = parserHandler(getObjectProperty(a, key)) ?? null + const bParsed = parserHandler(getObjectProperty(b, key)) ?? null + + if (aParsed !== bParsed) { + const comparison = (aParsed < bParsed) ? -1 : 1 + return dir === 'asc' ? comparison : -comparison + } + } + return 0 + }) } diff --git a/src/utils.ts b/src/utils.ts index 08f7176..0dc277e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import type { GenericObject, MatchModeProcessorMap } from './types' +import type { FilterMatchMode, GenericObject, MatchModeProcessorMap } from './types' export function getObjectProperty(object: Record, key: string) { return key.split('.').reduce((o, i) => o?.[i], object) @@ -43,6 +43,7 @@ export const MatchModeProcessor: MatchModeProcessorMap = { arrayLength: ({ value, filter }) => Array.isArray(value) && value.length === filter, objectMatch: ({ value, filter, params, index }) => { const properties = typeof index !== 'undefined' && params.matchPropertyAtIndex ? [params.properties[index]] : params.properties + return properties[params.operator === 'AND' ? 'every' : 'some' as const](property => MatchModeProcessor[property.matchMode]({ value: getObjectProperty(value, property.key), filter: getObjectProperty(filter, property.key), @@ -54,3 +55,89 @@ export const MatchModeProcessor: MatchModeProcessorMap = { export function validateBetweenPayload(payload: any) { return Array.isArray(payload) && payload.length === 2 && payload.every((i: any) => !Array.isArray(i)) } + +export function parseSearchValue(value: any, caseSensitive: boolean): string { + return (caseSensitive ? value?.toString() : value?.toString()?.toLowerCase?.()) ?? '' +} + +export function processSearchQuery(params: { key: string, object: Record, value: string, caseSensitive: boolean }): boolean { + const { key, object, value, caseSensitive } = params + const keys = key.split('.') + + let current: any = object + for (let i = 0; i < keys.length; i++) { + if (Array.isArray(current)) + return current.some(item => processSearchQuery({ key: keys.slice(i).join('.'), object: item, value, caseSensitive })) + + else if (current && Object.prototype.hasOwnProperty.call(current, keys[i])) + current = current[keys[i]] + + else + return false + } + + if (Array.isArray(current)) + return current.some(element => parseSearchValue(element, caseSensitive).includes(parseSearchValue(value, caseSensitive))) + + else + return parseSearchValue(current, caseSensitive).includes(parseSearchValue(value, caseSensitive)) ?? false +} + +export function processFilterWithLookup< + T extends FilterMatchMode, + P = Parameters[0], +>(params: { + type: FilterMatchMode + operator: 'AND' | 'OR' + value: any + filter: any + params: P extends { params: infer U } ? U : P extends { params?: infer U } ? U : null + lookupFrom?: 'value' | 'filter' +}) { + if (!Array.isArray(params.filter) || (params.type === 'between' && validateBetweenPayload(params.filter))) { + return Array.isArray(params.value) + ? params.value.some(value => + MatchModeProcessor[params.type]({ + params: params.params as any, + value, + filter: params.filter, + }), + ) + : MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter: params.filter }) + } + + else if (params.operator === 'AND') { + return Array.isArray(params.filter) && params.filter.every((filter, index) => { + if (Array.isArray(params.value)) { + return params.value.some(value => + MatchModeProcessor[params.type]({ + params: params.params as any, + value, + filter, + index, + }), + ) + } + else { + return MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter, index }) + } + }) + } + + else if (params.operator === 'OR') { + return Array.isArray(params.filter) && params.filter.some((filter, index) => + Array.isArray(params.value) + ? params.value.some(value => + MatchModeProcessor[params.type]({ + params: params.params as any, + value, + filter, + index, + }), + ) + : MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter, index }), + ) + } + + return false +} diff --git a/test/fixtures/filtering.fixture.json b/test/fixtures/filtering.fixture.json index 31a7608..4d61f55 100644 --- a/test/fixtures/filtering.fixture.json +++ b/test/fixtures/filtering.fixture.json @@ -1,403 +1,4 @@ [ - { - "title": "Contains match mode", - "data": [ - { "id": 1, "name": "John Doe", "email": "john@example.com" }, - { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, - { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } - ], - "query": { - "filter": [ - { "key": "name", "matchMode": "contains", "value": "oh" } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John Doe", "email": "john@example.com" }, - { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } - - ] - } - }, - { - "title": "Between match mode with numbers", - "data": [ - { "id": 1, "name": "John", "age": 25 }, - { "id": 2, "name": "Jane", "age": 30 }, - { "id": 3, "name": "Bob", "age": 35 } - ], - "query": { - "filter": [ - { "key": "age", "matchMode": "between", "value": [28, 32] } - ] - }, - "result": { - "rows": [ - { "id": 2, "name": "Jane", "age": 30 } - ] - } - }, - { - "title": "Between match mode with dates", - "data": [ - { "id": 1, "name": "John", "joinDate": "2023-01-15" }, - { "id": 2, "name": "Jane", "joinDate": "2023-06-20" }, - { "id": 3, "name": "Bob", "joinDate": "2023-12-05" } - ], - "query": { - "filter": [ - { - "key": "joinDate", - "matchMode": "between", - "value": ["2023-05-01", "2023-08-31"], - "params": { "dateMode": true } - } - ] - }, - "result": { - "rows": [ - { "id": 2, "name": "Jane", "joinDate": "2023-06-20" } - ] - } - }, - { - "title": "Equals match mode", - "data": [ - { "id": 1, "name": "John", "status": "active" }, - { "id": 2, "name": "Jane", "status": "inactive" }, - { "id": 3, "name": "Bob", "status": "active" } - ], - "query": { - "filter": [ - { "key": "status", "matchMode": "equals", "value": "active" } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "status": "active" }, - { "id": 3, "name": "Bob", "status": "active" } - ] - } - }, - { - "title": "Not Equals match mode", - "data": [ - { "id": 1, "name": "John", "status": "active" }, - { "id": 2, "name": "Jane", "status": "inactive" }, - { "id": 3, "name": "Bob", "status": "active" } - ], - "query": { - "filter": [ - { "key": "status", "matchMode": "notEquals", "value": "active" } - ] - }, - "result": { - "rows": [ - { "id": 2, "name": "Jane", "status": "inactive" } - ] - } - }, - { - "title": "Greater Than match mode", - "data": [ - { "id": 1, "name": "John", "age": 25 }, - { "id": 2, "name": "Jane", "age": 30 }, - { "id": 3, "name": "Bob", "age": 35 } - ], - "query": { - "filter": [ - { "key": "age", "matchMode": "greaterThan", "value": 28 } - ] - }, - "result": { - "rows": [ - { "id": 2, "name": "Jane", "age": 30 }, - { "id": 3, "name": "Bob", "age": 35 } - ] - } - }, - { - "title": "Greater Than or Equal match mode", - "data": [ - { "id": 1, "name": "John", "age": 25 }, - { "id": 2, "name": "Jane", "age": 30 }, - { "id": 3, "name": "Bob", "age": 35 } - ], - "query": { - "filter": [ - { "key": "age", "matchMode": "greaterThanOrEqual", "value": 30 } - ] - }, - "result": { - "rows": [ - { "id": 2, "name": "Jane", "age": 30 }, - { "id": 3, "name": "Bob", "age": 35 } - ] - } - }, - { - "title": "Less Than match mode", - "data": [ - { "id": 1, "name": "John", "age": 25 }, - { "id": 2, "name": "Jane", "age": 30 }, - { "id": 3, "name": "Bob", "age": 35 } - ], - "query": { - "filter": [ - { "key": "age", "matchMode": "lessThan", "value": 30 } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "age": 25 } - ] - } - }, - { - "title": "Less Than or Equal match mode", - "data": [ - { "id": 1, "name": "John", "age": 25 }, - { "id": 2, "name": "Jane", "age": 30 }, - { "id": 3, "name": "Bob", "age": 35 } - ], - "query": { - "filter": [ - { "key": "age", "matchMode": "lessThanOrEqual", "value": 30 } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "age": 25 }, - { "id": 2, "name": "Jane", "age": 30 } - ] - } - }, - { - "title": "Exists match mode", - "data": [ - { "id": 1, "name": "John", "age": 25 }, - { "id": 2, "name": "Jane" }, - { "id": 3, "name": "Bob", "age": 35 } - ], - "query": { - "filter": [ - { "key": "age", "matchMode": "exists", "value": true } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "age": 25 }, - { "id": 3, "name": "Bob", "age": 35 } - ] - } - }, - { - "title": "Array Length match mode", - "data": [ - { "id": 1, "name": "John", "skills": ["JavaScript", "TypeScript", "React"] }, - { "id": 2, "name": "Jane", "skills": ["Python", "Django"] }, - { "id": 3, "name": "Bob", "skills": ["Java", "Spring", "Hibernate", "SQL"] } - ], - "query": { - "filter": [ - { "key": "skills", "matchMode": "arrayLength", "value": 3 } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "skills": ["JavaScript", "TypeScript", "React"] } - ] - } - }, - { - "title": "Regex match mode - basic matching", - "data": [ - { "id": 1, "email": "john@example.com" }, - { "id": 2, "email": "jane@test.com" }, - { "id": 3, "email": "bob@example.org" } - ], - "query": { - "filter": [ - { "key": "email", "matchMode": "regex", "value": "@example\\.(com|org)" } - ] - }, - "result": { - "rows": [ - { "id": 1, "email": "john@example.com" }, - { "id": 3, "email": "bob@example.org" } - ] - } - }, - { - "title": "Regex match mode - case insensitive", - "data": [ - { "id": 1, "name": "John Doe" }, - { "id": 2, "name": "jane smith" }, - { "id": 3, "name": "Bob Johnson" } - ], - "query": { - "filter": [ - { - "key": "name", - "matchMode": "regex", - "value": "^j.*", - "params": { "flags": "i" } - } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John Doe" }, - { "id": 2, "name": "jane smith" } - ] - } - }, - { - "title": "Regex match mode - case sensitive", - "data": [ - { "id": 1, "name": "John Doe" }, - { "id": 2, "name": "jane smith" }, - { "id": 3, "name": "Bob Johnson" } - ], - "query": { - "filter": [ - { - "key": "name", - "matchMode": "regex", - "value": "^j.*" - } - ] - }, - "result": { - "rows": [ - { "id": 2, "name": "jane smith" } - ] - } - }, - { - "title": "Regex match mode - global flag", - "data": [ - { "id": 1, "text": "The quick brown fox" }, - { "id": 2, "text": "The lazy dog" }, - { "id": 3, "text": "Quick foxes are quick" } - ], - "query": { - "filter": [ - { - "key": "text", - "matchMode": "regex", - "value": "quick", - "params": { "flags": "g" } - } - ] - }, - "result": { - "rows": [ - { "id": 1, "text": "The quick brown fox" }, - { "id": 3, "text": "Quick foxes are quick" } - ] - } - }, - { - "title": "Regex match mode - multiline flag", - "data": [ - { "id": 1, "description": "First line\nSecond line" }, - { "id": 2, "description": "One line only" }, - { "id": 3, "description": "First line\nLast line" } - ], - "query": { - "filter": [ - { - "key": "description", - "matchMode": "regex", - "value": "^Last", - "params": { "flags": "m" } - } - ] - }, - "result": { - "rows": [ - { "id": 3, "description": "First line\nLast line" } - ] - } - }, - { - "title": "Regex match mode - combined flags", - "data": [ - { "id": 1, "content": "HELLO\nWorld" }, - { "id": 2, "content": "hello\nWORLD" }, - { "id": 3, "content": "Hi\nthere" } - ], - "query": { - "filter": [ - { - "key": "content", - "matchMode": "regex", - "value": "^world", - "params": { "flags": "im" } - } - ] - }, - "result": { - "rows": [ - { "id": 1, "content": "HELLO\nWorld" }, - { "id": 2, "content": "hello\nWORLD" } - ] - } - }, - { - "title": "Regex match mode - with other filters", - "data": [ - { "id": 1, "name": "John Doe", "age": 30 }, - { "id": 2, "name": "Jane Smith", "age": 25 }, - { "id": 3, "name": "Bob Johnson", "age": 35 } - ], - "query": { - "filter": [ - { - "key": "name", - "matchMode": "regex", - "value": "^j.*", - "params": { "flags": "i" } - }, - { "key": "age", "matchMode": "greaterThan", "value": 28 } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John Doe", "age": 30 } - ] - } - }, - { - "title": "Object Match mode", - "data": [ - { "id": 1, "name": "John", "account": { "type": "premium", "status": "active" } }, - { "id": 2, "name": "Jane", "account": { "type": "basic", "status": "inactive" } }, - { "id": 3, "name": "Bob", "account": { "type": "premium", "status": "inactive" } } - ], - "query": { - "filter": [ - { - "key": "account", - "matchMode": "objectMatch", - "value": { "type": "premium", "status": "active" }, - "params": { - "operator": "AND", - "properties": [ - { "key": "type", "matchMode": "equals" }, - { "key": "status", "matchMode": "equals" } - ] - } - } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "account": { "type": "premium", "status": "active" } } - ] - } - }, { "title": "Object Match - ALL objects in the array match ALL conditions", "data": [ @@ -427,112 +28,6 @@ { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150 }, { "id": 4, "total": 300 }] } ] } - }, - { - "title": "Object Match - ALL objects in the array match SOME of the conditions", - "data": [ - { "id": 1, "name": "John", "orders": [{ "id": 1, "total": 100, "status": "completed" }, { "id": 2, "total": 200, "status": "pending" }] }, - { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150, "status": "completed" }, { "id": 4, "total": 300, "status": "completed" }] }, - { "id": 3, "name": "Bob", "orders": [{ "id": 5, "total": 50, "status": "pending" }, { "id": 6, "total": 250, "status": "completed" }] } - ], - "query": { - "filter": [ - { - "key": "orders", - "matchMode": "objectMatch", - "value": { "total": 100, "status": "completed" }, - "operator": "AND", - "params": { - "operator": "OR", - "properties": [ - { "key": "total", "matchMode": "greaterThan" }, - { "key": "status", "matchMode": "equals" } - ] - } - } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "orders": [{ "id": 1, "total": 100, "status": "completed" }, { "id": 2, "total": 200, "status": "pending" }] }, - { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150, "status": "completed" }, { "id": 4, "total": 300, "status": "completed" }] } - ] - } - }, - { - "title": "Object Match - SOME objects in the array match ALL conditions", - "data": [ - { "id": 1, "name": "John", "orders": [{ "id": 1, "total": 100, "status": "completed" }, { "id": 2, "total": 200, "status": "pending" }] }, - { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150, "status": "completed" }, { "id": 4, "total": 300, "status": "completed" }] }, - { "id": 3, "name": "Bob", "orders": [{ "id": 5, "total": 50, "status": "pending" }, { "id": 6, "total": 250, "status": "completed" }] } - ], - "query": { - "filter": [ - { - "key": "orders", - "matchMode": "objectMatch", - "value": { "total": 200, "status": "completed" }, - "operator": "OR", - "params": { - "operator": "AND", - "properties": [ - { "key": "total", "matchMode": "greaterThan" }, - { "key": "status", "matchMode": "equals" } - ] - } - } - ] - }, - "result": { - "rows": [ - { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150, "status": "completed" }, { "id": 4, "total": 300, "status": "completed" }] } - ] - } - }, - { - "title": "Combined filters with AND logic", - "data": [ - { "id": 1, "name": "John", "age": 30, "status": "active" }, - { "id": 2, "name": "Jane", "age": 25, "status": "inactive" }, - { "id": 3, "name": "Bob", "age": 35, "status": "active" } - ], - "query": { - "filter": [ - { "key": "age", "matchMode": "greaterThan", "value": 25 }, - { "key": "status", "matchMode": "equals", "value": "active" } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "age": 30, "status": "active" }, - { "id": 3, "name": "Bob", "age": 35, "status": "active" } - ] - } - }, - { - "title": "Combined filters with OR logic", - "data": [ - { "id": 1, "name": "John", "age": 30, "status": "active" }, - { "id": 2, "name": "Jane", "age": 25, "status": "inactive" }, - { "id": 3, "name": "Bob", "age": 35, "status": "active" } - ], - "query": { - "filter": [ - { - "operator": "OR", - "filters": [ - { "key": "age", "matchMode": "lessThan", "value": 30 }, - { "key": "status", "matchMode": "equals", "value": "active" } - ] - } - ] - }, - "result": { - "rows": [ - { "id": 1, "name": "John", "age": 30, "status": "active" }, - { "id": 2, "name": "Jane", "age": 25, "status": "inactive" }, - { "id": 3, "name": "Bob", "age": 35, "status": "active" } - ] - } } + ] diff --git a/test/index.test.ts b/test/index.test.ts index 4618a31..86832b0 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -30,8 +30,8 @@ for (const fixture of fixtures) { for (const test of fixture.tests) { it(test.title, () => { const { unpaginatedRows, ...result } = (query as any)(test.data, test.query) - console.log('expected', test.result) - console.log('actual', result) + console.log('expected', JSON.stringify(test.result, null, 2)) + console.log('actual', JSON.stringify(result, null, 2)) expect(result).toEqual(test.result) }) }