Skip to content

Commit 43822ad

Browse files
Copilottypicode
andauthored
Add contains, startsWith, endsWith string query operators (#1714)
* Initial plan * Add contains, startsWith, endsWith query operators for partial string matching Co-authored-by: typicode <5502029+typicode@users.noreply.github.com> * Delete package-lock.json * docs: document contains, startsWith, endsWith operators in README Co-authored-by: typicode <5502029+typicode@users.noreply.github.com> * test: add numeric-value tests for contains, startsWith, endsWith operators Co-authored-by: typicode <5502029+typicode@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: typicode <5502029+typicode@users.noreply.github.com> Co-authored-by: typicode <typicode@gmail.com>
1 parent 9df3716 commit 43822ad

5 files changed

Lines changed: 66 additions & 1 deletion

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ Operators:
167167
- `gt` greater than, `gte` greater than or equal
168168
- `eq` equal, `ne` not equal
169169
- `in` included in comma-separated list
170+
- `contains` string contains (case-insensitive)
171+
- `startsWith` string starts with (case-insensitive)
172+
- `endsWith` string ends with (case-insensitive)
170173

171174
Examples:
172175

@@ -175,6 +178,9 @@ GET /posts?views:gt=100
175178
GET /posts?title:eq=Hello
176179
GET /posts?id:in=1,2,3
177180
GET /posts?author.name:eq=typicode
181+
GET /posts?title:contains=hello
182+
GET /posts?title:startsWith=Hello
183+
GET /posts?title:endsWith=world
178184
```
179185

180186
### Sort

src/matches-where.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ await test('matchesWhere', async (t) => {
3535
[{ a: { foo: 10 } }, true],
3636
[{ a: { foo: 10, eq: 10 } }, true],
3737
[{ missing: { foo: 1 } }, true],
38+
// contains
39+
[{ c: { contains: 'x' } }, true],
40+
[{ c: { contains: 'X' } }, true],
41+
[{ c: { contains: 'z' } }, false],
42+
[{ a: { contains: '1' } }, false],
43+
[{ c: { contains: 1 } }, false],
44+
// startsWith
45+
[{ c: { startsWith: 'x' } }, true],
46+
[{ c: { startsWith: 'X' } }, true],
47+
[{ c: { startsWith: 'z' } }, false],
48+
[{ a: { startsWith: '1' } }, false],
49+
[{ c: { startsWith: 1 } }, false],
50+
// endsWith
51+
[{ c: { endsWith: 'x' } }, true],
52+
[{ c: { endsWith: 'X' } }, true],
53+
[{ c: { endsWith: 'z' } }, false],
54+
[{ a: { endsWith: '1' } }, false],
55+
[{ c: { endsWith: 1 } }, false],
3856
]
3957

4058
for (const [query, expected] of cases) {

src/matches-where.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ export function matchesWhere(obj: JsonObject, where: JsonObject): boolean {
5757
const inValues = Array.isArray(op.in) ? op.in : [op.in]
5858
if (!inValues.some((v) => (field as any) === (v as any))) return false
5959
}
60+
if (knownOps.includes('contains')) {
61+
if (typeof field !== 'string') return false
62+
if (!field.toLowerCase().includes(String(op.contains).toLowerCase())) return false
63+
}
64+
if (knownOps.includes('startsWith')) {
65+
if (typeof field !== 'string') return false
66+
if (!field.toLowerCase().startsWith(String(op.startsWith).toLowerCase())) return false
67+
}
68+
if (knownOps.includes('endsWith')) {
69+
if (typeof field !== 'string') return false
70+
if (!field.toLowerCase().endsWith(String(op.endsWith).toLowerCase())) return false
71+
}
6072
continue
6173
}
6274

src/parse-where.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,24 @@ await test('parseWhere', async (t) => {
9595
title: { in: ['hello', 'world'] },
9696
},
9797
],
98+
[
99+
'title:contains=ello',
100+
{
101+
title: { contains: 'ello' },
102+
},
103+
],
104+
[
105+
'title:startsWith=hel',
106+
{
107+
title: { startsWith: 'hel' },
108+
},
109+
],
110+
[
111+
'title:endsWith=rld',
112+
{
113+
title: { endsWith: 'rld' },
114+
},
115+
],
98116
]
99117

100118
for (const [query, expected] of cases) {

src/where-operators.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
export const WHERE_OPERATORS = ['lt', 'lte', 'gt', 'gte', 'eq', 'ne', 'in'] as const
1+
export const WHERE_OPERATORS = [
2+
'lt',
3+
'lte',
4+
'gt',
5+
'gte',
6+
'eq',
7+
'ne',
8+
'in',
9+
'contains',
10+
'startsWith',
11+
'endsWith',
12+
] as const
213

314
export type WhereOperator = (typeof WHERE_OPERATORS)[number]
415

0 commit comments

Comments
 (0)