Skip to content

Commit 99426b2

Browse files
committed
feat(types): provide type-safe key paths for filter / search / sort query options
1 parent 6e3d07b commit 99426b2

3 files changed

Lines changed: 45 additions & 17 deletions

File tree

src/query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FilterMatchMode, GenericObject, MatchModeProcessorMap, QueryFilter, QueryFilterGroup, QueryParams, QueryResult } from './types'
22
import { MatchModeProcessor, getObjectProperty, validateBetweenPayload } from './utils'
33

4-
export function query<T extends GenericObject, P extends QueryParams>(
4+
export function query<T extends GenericObject, P extends QueryParams<T>>(
55
data: T[],
66
params: P,
77
): QueryResult<T, P> {

src/types.ts

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
11
export type Primitive = string | number | symbol
22
export type GenericObject = Record<Primitive, unknown>
33

4+
export type NestedPaths<T> = T extends Array<infer U>
5+
? `${NestedPaths<U>}`
6+
: T extends object
7+
? {
8+
[K in keyof T & (string | number)]: K extends string
9+
? `${K}` | `${K}.${NestedPaths<T[K]>}`
10+
: string;
11+
}[keyof T & (string | number)]
12+
: string
13+
14+
export type NestedPathsForType<T, P> = T extends Array<infer U>
15+
? NestedPathsForType<U, P>
16+
: T extends object
17+
? {
18+
[K in keyof T & (string | number)]: K extends string
19+
? T[K] extends P
20+
? `${K}` | `${K}.${NestedPathsForType<T[K], P>}`
21+
: T[K] extends object
22+
? `${K}.${NestedPathsForType<T[K], P>}`
23+
: string
24+
: string;
25+
}[keyof T & (string | number)]
26+
: string
27+
428
export type Operator = 'AND' | 'OR' | (() => 'AND' | 'OR')
529

630
export type FilterMatchMode =
@@ -44,40 +68,44 @@ export type MatchModeCore = ({
4468
params: ObjectMapFilterParams | ((value: any) => ObjectMapFilterParams)
4569
})
4670

47-
export type QueryFilter = {
48-
key: string
71+
export type QueryFilter<Paths extends string = string> = {
72+
key: Paths
4973
value: any
5074
operator?: Operator
5175
} & MatchModeCore
5276

53-
export interface QueryFilterGroup {
77+
export interface QueryFilterGroup<Paths extends string = string> {
5478
operator: Operator
55-
filters: QueryFilter[]
79+
filters: QueryFilter<Paths>[]
5680
}
5781

58-
export type FilterOptions = Array<QueryFilterGroup> | Array<QueryFilter>
82+
export type FilterOptions<Paths extends string = string> = Array<QueryFilterGroup<Paths>> | Array<QueryFilter<Paths>>
5983

60-
export interface SearchOptions {
84+
export interface SearchOptions<Paths extends string = string> {
6185
value: string
62-
keys: string[] | Array<{ key: string, caseSensitive?: boolean }>
86+
keys: Paths[] | Array<{ key: Paths, caseSensitive?: boolean }>
6387
caseSensitive?: boolean
6488
}
6589

66-
export interface SortOptions {
67-
key: string
90+
export interface SortOption<Paths extends string = string> {
91+
key: Paths
6892
dir?: 'asc' | 'desc'
6993
parser?: 'number' | 'boolean' | 'string' | ((value: any) => string | number | boolean | null | undefined)
7094
}
7195

72-
export interface QueryParams {
73-
sort?: SortOptions | Array<SortOptions>
74-
search?: SearchOptions
75-
filter?: FilterOptions
96+
export interface QueryParams<
97+
T extends GenericObject = GenericObject,
98+
Paths extends NestedPaths<T> = NestedPaths<T>,
99+
PrimitivePath extends string = NestedPathsForType<T, string | number | null | boolean | undefined>,
100+
> {
101+
sort?: SortOption<PrimitivePath> | Array<SortOption<PrimitivePath>>
102+
search?: SearchOptions<PrimitivePath>
103+
filter?: FilterOptions<Paths>
76104
limit?: number
77105
page?: number
78106
}
79107

80-
export type QueryResult<T extends GenericObject, P extends QueryParams> = P extends { limit: number } ? { totalRows: number, totalPages: number, rows: T[], unpaginatedRows: T[] } : { rows: T[] }
108+
export type QueryResult<T extends GenericObject, P extends QueryParams<T>> = P extends { limit: number } ? { totalRows: number, totalPages: number, rows: T[], unpaginatedRows: T[] } : { rows: T[] }
81109

82110
export interface MatchModeProcessorMap {
83111
equals: ({ value, filter }: { value: any, filter: any }) => boolean

test/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ for (const fixture of fixtures) {
3838
describe('performance check', () => {
3939
// VAL BETWEEN 1 & 100
4040
const getValue = () => Math.floor(Math.random() * 100)
41-
const items = Array.from({ length: 1000000 }, (_, i) => ({ id: i, name: `Item ${i}`, value: getValue() }))
41+
const items = Array.from({ length: 1000000 }, (_, i) => ({ id: i, name: `Item ${i}`, value: getValue(), other: [], address: { city: 'New York', country: 'USA' } }))
4242
it('query 1M rows - paginate + sort + search + filter in less than 500ms', () => {
4343
const start = performance.now()
4444
query(items, {
4545
limit: 10,
4646
sort: {
47-
key: 'id',
47+
key: 'address.country',
4848
dir: 'asc',
4949
},
5050
search: {

0 commit comments

Comments
 (0)