Skip to content

Commit 8ff9c13

Browse files
committed
feat(filters): add support for regex match mode
1 parent 0565978 commit 8ff9c13

7 files changed

Lines changed: 357 additions & 14 deletions

File tree

docs/features/filtering.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,5 @@ ArrayQuery supports various match modes for different types of comparisons. Each
174174
- 'lessThanOrEqual'
175175
- 'exists'
176176
- 'arrayLength'
177+
- 'regex'
177178
- 'objectMatch'

docs/filter-match-modes/regex.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Regex Match Mode
2+
3+
The regex match mode allows you to filter data using regular expressions. This powerful feature enables complex pattern matching across your dataset.
4+
5+
## Basic Usage
6+
7+
To use the regex match mode, set the `matchMode` to `'regex'` in your filter configuration:
8+
9+
```ts twoslash
10+
import { query } from '@chronicstone/array-query'
11+
12+
const data = [
13+
{ id: 1, email: 'john@example.com' },
14+
{ id: 2, email: 'jane@test.com' },
15+
{ id: 3, email: 'bob@example.org' }
16+
]
17+
18+
const result = query(data, {
19+
filter: [
20+
{ key: 'email', matchMode: 'regex', value: '@example\\.(com|org)' }
21+
]
22+
})
23+
```
24+
25+
This query will return all items where the email matches the pattern `@example.com` or `@example.org`.
26+
27+
## Flags
28+
29+
You can modify the behavior of the regex matching by providing flags. Flags are specified using the `params` option:
30+
31+
```ts twoslash
32+
import { query } from '@chronicstone/array-query'
33+
34+
const data = [
35+
{ id: 1, name: 'John Doe' },
36+
{ id: 2, name: 'jane smith' },
37+
{ id: 3, name: 'Bob Johnson' }
38+
]
39+
40+
const result = query(data, {
41+
filter: [
42+
{
43+
key: 'name',
44+
matchMode: 'regex',
45+
value: '^j.*',
46+
params: { flags: 'i' }
47+
}
48+
]
49+
})
50+
```
51+
52+
## Raw RegExp
53+
54+
Instead of defining the regex pattern as a string, you can also pass a regular expression object directly:
55+
56+
```ts twoslash
57+
import { query } from '@chronicstone/array-query'
58+
59+
const data = [
60+
{ id: 1, name: 'John Doe' },
61+
{ id: 2, name: 'jane smith' },
62+
{ id: 3, name: 'Bob Johnson' }
63+
]
64+
65+
const result = query(data, {
66+
filter: [
67+
{
68+
key: 'name',
69+
matchMode: 'regex',
70+
value: /^j.*/i
71+
}
72+
]
73+
})
74+
```
75+
76+
For a detailed explanation of available flags and their usage, please refer to the [MDN documentation on Regular Expression Flags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#advanced_searching_with_flags).
77+
78+
## Examples
79+
80+
### Case-insensitive Matching
81+
82+
```ts twoslash
83+
import { query } from '@chronicstone/array-query'
84+
85+
const data = [
86+
{ id: 1, name: 'John Doe' },
87+
{ id: 2, name: 'jane smith' },
88+
{ id: 3, name: 'Bob Johnson' }
89+
]
90+
91+
const result = query(data, {
92+
filter: [
93+
{
94+
key: 'name',
95+
matchMode: 'regex',
96+
value: '^j.*',
97+
params: { flags: 'i' }
98+
}
99+
]
100+
})
101+
```
102+
103+
This will match both "John Doe" and "jane smith".
104+
105+
### Multi-line Matching
106+
107+
```ts twoslash
108+
import { query } from '@chronicstone/array-query'
109+
110+
const data = [
111+
{ id: 1, description: 'First line\nSecond line' },
112+
{ id: 2, description: 'One line only' },
113+
{ id: 3, description: 'First line\nLast line' }
114+
]
115+
116+
const result = query(data, {
117+
filter: [
118+
{
119+
key: 'description',
120+
matchMode: 'regex',
121+
value: '^Last',
122+
params: { flags: 'm' }
123+
}
124+
]
125+
})
126+
```
127+
128+
This will match the item with id 3, where "Last" appears at the start of a line.
129+
130+
## Combining with Other Filters
131+
132+
You can combine regex filters with other filter types:
133+
134+
```typescript
135+
const result = query(data, {
136+
filter: [
137+
{
138+
key: 'name',
139+
matchMode: 'regex',
140+
value: '^j.*',
141+
params: { flags: 'i' }
142+
},
143+
{ key: 'age', matchMode: 'greaterThan', value: 28 }
144+
]
145+
})
146+
```
147+
148+
This will find items where the name starts with 'j' (case-insensitive) AND the age is greater than 28.
149+
150+
## Performance Considerations
151+
152+
While regex matching is powerful, it can be computationally expensive, especially on large datasets or with complex patterns. Use it judiciously and consider performance implications in your use case.
153+
154+
## Escaping Special Characters
155+
156+
Remember to properly escape special regex characters in your pattern strings. For example, to match a literal period, use `\\.` instead of `.`.
157+
158+
## Further Reading
159+
160+
For more information on JavaScript regular expressions, including pattern syntax and usage, refer to the [MDN Regular Expressions Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions).

src/query.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ export function query<T extends GenericObject, P extends QueryParams<T>>(
137137
})
138138
}
139139

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+
140150
if (filter.matchMode === 'objectMatch') {
141151
const params = typeof filter.params === 'function' ? filter.params(filter.value) : filter.params
142152
const filterValue = params?.transformFilterValue?.(filter.value) ?? filter.value

src/types.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,20 @@ export type FilterMatchMode =
3737
| 'lessThan'
3838
| 'lessThanOrEqual'
3939
| 'exists'
40+
| 'regex'
4041
| 'arrayLength'
4142
| 'objectMatch'
4243

4344
export type NonObjectMatchMode = Exclude<FilterMatchMode, 'objectMatch'>
4445
export type ComparatorMatchMode = Extract<FilterMatchMode, 'between' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual'>
45-
46+
export type RegexMatchMode = Extract<FilterMatchMode, 'regex'>
4647
export interface ComparatorParams {
4748
dateMode?: boolean
4849
}
4950

51+
export interface RegexParams {
52+
flags?: string
53+
}
5054
export interface ObjectMapFilterParams {
5155
operator: 'AND' | 'OR'
5256
properties: Array<{
@@ -59,10 +63,13 @@ export interface ObjectMapFilterParams {
5963
}
6064

6165
export type MatchModeCore = ({
62-
matchMode: Exclude<FilterMatchMode, 'objectStringMap' | 'objectMatch' | ComparatorMatchMode>
66+
matchMode: Exclude<FilterMatchMode, RegexMatchMode | 'objectMatch' | ComparatorMatchMode>
6367
} | {
6468
matchMode: ComparatorMatchMode
6569
params?: ComparatorParams
70+
} | {
71+
matchMode: 'regex'
72+
params?: RegexParams
6673
} | {
6774
matchMode: 'objectMatch'
6875
params: ObjectMapFilterParams | ((value: any) => ObjectMapFilterParams)
@@ -108,15 +115,16 @@ export interface QueryParams<
108115
export type QueryResult<T extends GenericObject, P extends QueryParams<T>> = P extends { limit: number } ? { totalRows: number, totalPages: number, rows: T[], unpaginatedRows: T[] } : { rows: T[] }
109116

110117
export interface MatchModeProcessorMap {
111-
equals: ({ value, filter }: { value: any, filter: any }) => boolean
112-
notEquals: ({ value, filter }: { value: any, filter: any }) => boolean
113-
exists: ({ value, filter }: { value: any, filter: any }) => boolean
114-
contains: ({ value, filter }: { value: any, filter: any }) => boolean
115-
greaterThan: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
116-
greaterThanOrEqual: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
117-
lessThan: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
118-
lessThanOrEqual: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
119-
between: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
120-
arrayLength: ({ value, filter }: { value: any, filter: any }) => boolean
121-
objectMatch: ({ value, filter, params }: { value: any, filter: any, params: ObjectMapFilterParams, index?: number }) => boolean
118+
equals: (f: { value: any, filter: any }) => boolean
119+
notEquals: (f: { value: any, filter: any }) => boolean
120+
exists: (f: { value: any, filter: any }) => boolean
121+
contains: (f: { value: any, filter: any }) => boolean
122+
greaterThan: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
123+
greaterThanOrEqual: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
124+
lessThan: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
125+
lessThanOrEqual: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
126+
between: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
127+
arrayLength: (f: { value: any, filter: any }) => boolean
128+
objectMatch: (f: { value: any, filter: any, params: ObjectMapFilterParams, index?: number }) => boolean
129+
regex: (f: { value: any, filter: any, params?: RegexParams }) => boolean
122130
}

src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export const MatchModeProcessor: MatchModeProcessorMap = {
3838
between: ({ value, filter, params }) => {
3939
return params?.dateMode ? new Date(value) >= new Date(filter[0]) && new Date(value) <= new Date(filter[1]) : value >= filter[0] && value <= filter[1]
4040
},
41+
regex: ({ value, filter, params }) =>
42+
typeof value === 'string' && new RegExp(filter, params?.flags ?? '').test(value),
4143
arrayLength: ({ value, filter }) => Array.isArray(value) && value.length === filter,
4244
objectMatch: ({ value, filter, params, index }) => {
4345
const properties = typeof index !== 'undefined' && params.matchPropertyAtIndex ? [params.properties[index]] : params.properties

0 commit comments

Comments
 (0)