Skip to content

Commit 829a024

Browse files
committed
refactor(query): clean-up implementation, lazily evaluate filters in less iterations
1 parent 411e981 commit 829a024

4 files changed

Lines changed: 151 additions & 744 deletions

File tree

src/query.ts

Lines changed: 60 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +1,14 @@
1-
import type { FilterMatchMode, GenericObject, MatchModeProcessorMap, QueryFilter, QueryFilterGroup, QueryParams, QueryResult } from './types'
2-
import { MatchModeProcessor, getObjectProperty, validateBetweenPayload } from './utils'
1+
import type { GenericObject, QueryFilter, QueryFilterGroup, QueryParams, QueryResult } from './types'
2+
import { getObjectProperty, processFilterWithLookup, processSearchQuery } from './utils'
33

44
export function query<T extends GenericObject, P extends QueryParams<T>>(
55
data: T[],
66
params: P,
77
): QueryResult<T, P> {
8-
let result: T[] = [...data]
8+
let result = Array.from(lazyQuery(data, params))
99

10-
if (params.search && params.search.value) {
11-
result = result.filter((item) => {
12-
for (const key of params.search?.keys ?? []) {
13-
const field = typeof key === 'string' ? key : key.key
14-
const caseSensitive = typeof key === 'string' ? (params.search?.caseSensitive ?? false) : key.caseSensitive ?? false
15-
if (processSearchQuery({ key: field, caseSensitive, object: item, value: params.search!.value }))
16-
return true
17-
}
18-
return false
19-
})
20-
}
21-
22-
if (params.sort) {
23-
const sortArray = Array.isArray(params.sort) ? params.sort : [params.sort]
24-
25-
result = result.sort((a, b) => {
26-
for (const { key, dir, parser } of sortArray) {
27-
const parserHandler = typeof parser === 'function' ? parser : (v: any) => parser === 'number' ? Number(v) : parser === 'boolean' ? Boolean(v) : parser === 'string' ? String(v) : v
28-
const aParsed = parserHandler(getObjectProperty(a, key)) ?? null
29-
const bParsed = parserHandler(getObjectProperty(b, key)) ?? null
30-
31-
if (aParsed !== bParsed) {
32-
const comparison = (aParsed < bParsed) ? -1 : 1
33-
return dir === 'asc' ? comparison : -comparison
34-
}
35-
}
36-
return 0
37-
})
38-
}
39-
40-
if (Array.isArray(params.filter) && params.filter.length) {
41-
result = result.filter((item) => {
42-
const IS_GROUP = params.filter?.every(filter => 'filters' in filter) ?? false
43-
const METHOD = IS_GROUP ? 'some' : 'every' as const
44-
const FILTERS = (params?.filter ?? []) as any[]
45-
return FILTERS[METHOD]((group: QueryFilter | QueryFilterGroup) => {
46-
const filters = 'filters' in group ? group.filters : [group]
47-
const op = 'filters' in group ? group.operator : 'OR'
48-
return filters[op === 'AND' ? 'every' : 'some' as const]((filter) => {
49-
const value = getObjectProperty(item, filter.key)
50-
const operator = typeof filter.operator === 'function' ? filter.operator() : filter.operator ?? 'OR'
51-
if (filter.matchMode === 'equals') {
52-
return processFilterWithLookup({
53-
type: 'equals',
54-
params: null,
55-
operator,
56-
value,
57-
filter: filter.value,
58-
})
59-
}
60-
61-
if (filter.matchMode === 'contains') {
62-
return processFilterWithLookup({
63-
type: 'contains',
64-
params: null,
65-
operator,
66-
value,
67-
filter: filter.value,
68-
})
69-
}
70-
if (filter.matchMode === 'between') {
71-
return processFilterWithLookup({
72-
type: 'between',
73-
params: filter?.params ?? null,
74-
operator,
75-
value,
76-
filter: filter.value,
77-
})
78-
}
79-
80-
if (filter.matchMode === 'greaterThan') {
81-
return processFilterWithLookup({
82-
type: 'greaterThan',
83-
params: filter?.params ?? null,
84-
operator,
85-
value,
86-
filter: filter.value,
87-
})
88-
}
89-
90-
if (filter.matchMode === 'greaterThanOrEqual') {
91-
return processFilterWithLookup({
92-
type: 'greaterThanOrEqual',
93-
params: filter?.params ?? null,
94-
operator,
95-
value,
96-
filter: filter.value,
97-
})
98-
}
99-
100-
if (filter.matchMode === 'lessThan') {
101-
return processFilterWithLookup({
102-
type: 'lessThan',
103-
params: filter?.params ?? null,
104-
operator,
105-
value,
106-
filter: filter.value,
107-
})
108-
}
109-
110-
if (filter.matchMode === 'lessThanOrEqual') {
111-
return processFilterWithLookup({
112-
type: 'lessThanOrEqual',
113-
params: filter?.params ?? null,
114-
operator,
115-
value,
116-
filter: filter.value,
117-
})
118-
}
119-
120-
if (filter.matchMode === 'exists') {
121-
return processFilterWithLookup({
122-
type: 'exists',
123-
params: null,
124-
operator,
125-
value,
126-
filter: filter.value,
127-
})
128-
}
129-
130-
if (filter.matchMode === 'arrayLength') {
131-
return processFilterWithLookup({
132-
type: 'arrayLength',
133-
params: null,
134-
operator,
135-
value,
136-
filter: filter.value,
137-
})
138-
}
139-
140-
if (filter.matchMode === 'regex') {
141-
return processFilterWithLookup({
142-
type: 'regex',
143-
params: filter?.params ?? null,
144-
operator,
145-
value,
146-
filter: filter.value,
147-
})
148-
}
149-
150-
if (filter.matchMode === 'objectMatch') {
151-
const params = typeof filter.params === 'function' ? filter.params(filter.value) : filter.params
152-
const filterValue = params?.transformFilterValue?.(filter.value) ?? filter.value
153-
return processFilterWithLookup({
154-
type: 'objectMatch',
155-
params,
156-
operator,
157-
value: params?.applyAtRoot ? item : value,
158-
filter: filterValue,
159-
})
160-
}
161-
162-
return false
163-
})
164-
})
165-
})
166-
}
10+
if (params.sort)
11+
result = sortedQuery(result, params.sort)
16712

16813
if (typeof params.limit === 'undefined') {
16914
return { rows: result } as QueryResult<T, P>
@@ -186,88 +31,68 @@ export function query<T extends GenericObject, P extends QueryParams<T>>(
18631
}
18732
}
18833

189-
function parseSearchValue(value: any, caseSensitive: boolean): string {
190-
return (caseSensitive ? value?.toString() : value?.toString()?.toLowerCase?.()) ?? ''
34+
function* lazyQuery<T extends GenericObject>(data: T[], params: QueryParams<T>): Generator<T> {
35+
for (const item of data) {
36+
if (matchesSearchAndFilters(item, params)) {
37+
yield item
38+
}
39+
}
19140
}
19241

193-
function processSearchQuery(params: { key: string, object: Record<string, any>, value: string, caseSensitive: boolean }): boolean {
194-
const { key, object, value, caseSensitive } = params
195-
const keys = key.split('.')
196-
197-
let current: any = object
198-
for (let i = 0; i < keys.length; i++) {
199-
if (Array.isArray(current))
200-
return current.some(item => processSearchQuery({ key: keys.slice(i).join('.'), object: item, value, caseSensitive }))
201-
202-
else if (current && Object.prototype.hasOwnProperty.call(current, keys[i]))
203-
current = current[keys[i]]
204-
205-
else
206-
return false
207-
}
42+
function matchesSearchAndFilters<T extends GenericObject>(item: T, params: QueryParams<T>): boolean {
43+
return matchesSearch(item, params.search) && matchesFilters(item, params.filter)
44+
}
20845

209-
if (Array.isArray(current))
210-
return current.some(element => parseSearchValue(element, caseSensitive).includes(parseSearchValue(value, caseSensitive)))
46+
function matchesSearch<T extends GenericObject>(item: T, search?: QueryParams<T>['search']): boolean {
47+
if (!search || !search.value)
48+
return true
21149

212-
else
213-
return parseSearchValue(current, caseSensitive).includes(parseSearchValue(value, caseSensitive)) ?? false
50+
return search.keys.some((key) => {
51+
const field = typeof key === 'string' ? key : key.key
52+
const caseSensitive = typeof key === 'string' ? (search.caseSensitive ?? false) : key.caseSensitive ?? false
53+
return processSearchQuery({ key: field, caseSensitive, object: item, value: search.value })
54+
})
21455
}
21556

216-
function processFilterWithLookup<
217-
T extends FilterMatchMode,
218-
P = Parameters<MatchModeProcessorMap[T]>[0],
219-
>(params: {
220-
type: FilterMatchMode
221-
operator: 'AND' | 'OR'
222-
value: any
223-
filter: any
224-
params: P extends { params: infer U } ? U : P extends { params?: infer U } ? U : null
225-
lookupFrom?: 'value' | 'filter'
226-
}) {
227-
if (!Array.isArray(params.filter) || (params.type === 'between' && validateBetweenPayload(params.filter))) {
228-
return Array.isArray(params.value)
229-
? params.value.some(value =>
230-
MatchModeProcessor[params.type]({
231-
params: params.params as any,
232-
value,
233-
filter: params.filter,
234-
}),
235-
)
236-
: MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter: params.filter })
237-
}
238-
239-
else if (params.operator === 'AND') {
240-
return Array.isArray(params.filter) && params.filter.every((filter, index) => {
241-
if (Array.isArray(params.value)) {
242-
return params.value.some(value =>
243-
MatchModeProcessor[params.type]({
244-
params: params.params as any,
245-
value,
246-
filter,
247-
index,
248-
}),
249-
)
250-
}
251-
else {
252-
return MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter, index })
253-
}
57+
function matchesFilters<T extends GenericObject>(item: T, filters?: (QueryFilter | QueryFilterGroup)[]): boolean {
58+
if (!filters || filters.length === 0)
59+
return true
60+
const isGroup = filters.every(filter => 'filters' in filter)
61+
const method = isGroup ? 'some' : 'every'
62+
return filters[method]((group: QueryFilter | QueryFilterGroup) => {
63+
const groupFilters = 'filters' in group ? group.filters : [group]
64+
const op = 'filters' in group ? group.operator : 'OR'
65+
return groupFilters[op === 'AND' ? 'every' : 'some']((filter: QueryFilter) => {
66+
const value = getObjectProperty(item, filter.key)
67+
const operator = typeof filter.operator === 'function' ? filter.operator() : filter.operator ?? 'OR'
68+
const params = (!('params' in filter) ? null : typeof filter.params === 'function' ? filter.params(filter.value) : filter.params) ?? null
69+
return processFilterWithLookup({
70+
type: filter.matchMode,
71+
params,
72+
operator,
73+
value,
74+
filter: filter.value,
75+
})
25476
})
255-
}
77+
})
78+
}
25679

257-
else if (params.operator === 'OR') {
258-
return Array.isArray(params.filter) && params.filter.some((filter, index) =>
259-
Array.isArray(params.value)
260-
? params.value.some(value =>
261-
MatchModeProcessor[params.type]({
262-
params: params.params as any,
263-
value,
264-
filter,
265-
index,
266-
}),
267-
)
268-
: MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter, index }),
269-
)
270-
}
80+
function sortedQuery<T extends GenericObject>(data: T[], sortOptions?: QueryParams<T>['sort']): T[] {
81+
if (!sortOptions)
82+
return data
27183

272-
return false
84+
const sortArray = Array.isArray(sortOptions) ? sortOptions : [sortOptions]
85+
return data.slice().sort((a, b) => {
86+
for (const { key, dir, parser } of sortArray) {
87+
const parserHandler = typeof parser === 'function' ? parser : (v: any) => parser === 'number' ? Number(v) : parser === 'boolean' ? Boolean(v) : parser === 'string' ? String(v) : v
88+
const aParsed = parserHandler(getObjectProperty(a, key)) ?? null
89+
const bParsed = parserHandler(getObjectProperty(b, key)) ?? null
90+
91+
if (aParsed !== bParsed) {
92+
const comparison = (aParsed < bParsed) ? -1 : 1
93+
return dir === 'asc' ? comparison : -comparison
94+
}
95+
}
96+
return 0
97+
})
27398
}

0 commit comments

Comments
 (0)