@travetto/model-query 5.0.0-rc.7 → 5.0.0-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -28
- package/__index__.ts +2 -1
- package/package.json +6 -7
- package/src/internal/service/common.ts +4 -8
- package/src/internal/service/query.ts +11 -37
- package/src/internal/service/suggest.ts +7 -10
- package/src/model/query.ts +1 -1
- package/src/model/where-clause.ts +1 -2
- package/src/{internal/query/verifier.ts → verifier.ts} +15 -11
- package/support/test/crud.ts +4 -4
- package/support/test/facet.ts +3 -3
- package/support/test/polymorphism.ts +7 -6
- package/support/test/query.ts +4 -4
- package/src/internal/query/parser.ts +0 -210
- package/src/internal/query/tokenizer.ts +0 -187
- package/src/internal/query/types.ts +0 -86
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install @travetto/model-query
|
|
|
13
13
|
yarn add @travetto/model-query
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module provides an enhanced query contract for [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") implementations. This contract has been externalized due to it being more complex than many implementations can natively support.
|
|
16
|
+
This module provides an enhanced query contract for [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") implementations. This contract has been externalized due to it being more complex than many implementations can natively support.
|
|
17
17
|
|
|
18
18
|
## Contracts
|
|
19
19
|
|
|
@@ -208,33 +208,6 @@ export class UserSearch {
|
|
|
208
208
|
|
|
209
209
|
This would find all users who are over `35` and that have the `contact` field specified.
|
|
210
210
|
|
|
211
|
-
## Query Language
|
|
212
|
-
In addition to the standard query interface, the module also supports querying by query language to facilitate end - user queries.This is meant to act as an interface that is simpler to write than the default object structure. The language itself is fairly simple, boolean logic, with parenthetical support.The operators supported are:
|
|
213
|
-
* `<`, `<=` - Less than, and less than or equal to
|
|
214
|
-
* `>`, `>=` - Greater than, and greater than or equal to
|
|
215
|
-
* `!=`, `==` - Not equal to, and equal to
|
|
216
|
-
* `~` - Matches regular expression, supports the `i` flag to trigger case insensitive searches
|
|
217
|
-
* `!`, `not` - Negates a clause
|
|
218
|
-
* `in`, `not-in` - Supports checking if a field is in a list of literal values
|
|
219
|
-
* `and`, `&&` - Intersection of clauses
|
|
220
|
-
* `or`, `||` - Union of clauses
|
|
221
|
-
All sub fields are dot separated for access, e.g. `user.address.city`.A query language version of the previous query could look like:
|
|
222
|
-
|
|
223
|
-
**Code: Query language with boolean checks and exists check**
|
|
224
|
-
```sql
|
|
225
|
-
not (age < 35) and contact != null
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
A more complex query would look like:
|
|
229
|
-
|
|
230
|
-
**Code: Query language with more complex needs**
|
|
231
|
-
```sql
|
|
232
|
-
user.role in ['admin', 'root'] && (user.address.state == 'VA' || user.address.city == 'Springfield')
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Regular Expression
|
|
236
|
-
When querying with regular expressions, patterns can be specified as `'strings'` or as `/patterns/`. The latter allows for the case insensitive modifier: `/pattern/i`. Supporting the insensitive flag is up to the underlying model implementation.
|
|
237
|
-
|
|
238
211
|
## Custom Model Query Service
|
|
239
212
|
In addition to the provided contracts, the module also provides common utilities and shared test suites.The common utilities are useful for repetitive functionality, that is unable to be shared due to not relying upon inheritance(this was an intentional design decision).This allows for all the [Data Model Querying](https://github.com/travetto/travetto/tree/main/module/model-query#readme "Datastore abstraction for advanced query support.") implementations to completely own the functionality and also to be able to provide additional / unique functionality that goes beyond the interface. To enforce that these contracts are honored, the module provides shared test suites to allow for custom implementations to ensure they are adhering to the contract's expected behavior.
|
|
240
213
|
|
package/__index__.ts
CHANGED
|
@@ -3,4 +3,5 @@ export * from './src/model/where-clause';
|
|
|
3
3
|
export * from './src/service/crud';
|
|
4
4
|
export * from './src/service/query';
|
|
5
5
|
export * from './src/service/facet';
|
|
6
|
-
export * from './src/service/suggest';
|
|
6
|
+
export * from './src/service/suggest';
|
|
7
|
+
export * from './src/verifier';
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-query",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.9",
|
|
4
4
|
"description": "Datastore abstraction for advanced query support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datastore",
|
|
7
7
|
"decorators",
|
|
8
8
|
"schema",
|
|
9
9
|
"typescript",
|
|
10
|
-
"travetto"
|
|
11
|
-
"query-language"
|
|
10
|
+
"travetto"
|
|
12
11
|
],
|
|
13
12
|
"homepage": "https://travetto.io",
|
|
14
13
|
"license": "MIT",
|
|
@@ -27,12 +26,12 @@
|
|
|
27
26
|
"directory": "module/model-query"
|
|
28
27
|
},
|
|
29
28
|
"dependencies": {
|
|
30
|
-
"@travetto/di": "^5.0.0-rc.
|
|
31
|
-
"@travetto/model": "^5.0.0-rc.
|
|
32
|
-
"@travetto/schema": "^5.0.0-rc.
|
|
29
|
+
"@travetto/di": "^5.0.0-rc.9",
|
|
30
|
+
"@travetto/model": "^5.0.0-rc.9",
|
|
31
|
+
"@travetto/schema": "^5.0.0-rc.9"
|
|
33
32
|
},
|
|
34
33
|
"peerDependencies": {
|
|
35
|
-
"@travetto/test": "^5.0.0-rc.
|
|
34
|
+
"@travetto/test": "^5.0.0-rc.9"
|
|
36
35
|
},
|
|
37
36
|
"peerDependenciesMeta": {
|
|
38
37
|
"@travetto/test": {
|
|
@@ -13,8 +13,7 @@ export class ModelQuerySuggestSupportTarget { }
|
|
|
13
13
|
* @param o
|
|
14
14
|
*/
|
|
15
15
|
export function isQuerySupported(o: unknown): o is ModelQuerySupport {
|
|
16
|
-
|
|
17
|
-
return !!o && !!(o as Record<string, unknown>)['query'];
|
|
16
|
+
return !!o && typeof o === 'object' && 'query' in o && !!o.query;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -22,8 +21,7 @@ export function isQuerySupported(o: unknown): o is ModelQuerySupport {
|
|
|
22
21
|
* @param o
|
|
23
22
|
*/
|
|
24
23
|
export function isQueryCrudSupported(o: unknown): o is ModelQueryCrudSupport {
|
|
25
|
-
|
|
26
|
-
return !!o && !!(o as Record<string, unknown>)['deleteByQuery'];
|
|
24
|
+
return !!o && typeof o === 'object' && 'deleteByQuery' in o && !!o.deleteByQuery;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
/**
|
|
@@ -31,8 +29,7 @@ export function isQueryCrudSupported(o: unknown): o is ModelQueryCrudSupport {
|
|
|
31
29
|
* @param o
|
|
32
30
|
*/
|
|
33
31
|
export function isQueryFacetSupported(o: unknown): o is ModelQueryFacetSupport {
|
|
34
|
-
|
|
35
|
-
return !!o && !!(o as Record<string, unknown>)['facet'];
|
|
32
|
+
return !!o && typeof o === 'object' && 'facet' in o && !!o.facet;
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
/**
|
|
@@ -40,6 +37,5 @@ export function isQueryFacetSupported(o: unknown): o is ModelQueryFacetSupport {
|
|
|
40
37
|
* @param o
|
|
41
38
|
*/
|
|
42
39
|
export function isQuerySuggestSupported(o: unknown): o is ModelQuerySuggestSupport {
|
|
43
|
-
|
|
44
|
-
return !!o && !!(o as Record<string, unknown>)['suggest'];
|
|
40
|
+
return !!o && typeof o === 'object' && 'suggest' in o && !!o.suggest;
|
|
45
41
|
}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { Class, AppError, TimeUtil } from '@travetto/runtime';
|
|
1
|
+
import { Class, AppError, TimeUtil, castTo } from '@travetto/runtime';
|
|
2
2
|
import { ModelRegistry, NotFoundError } from '@travetto/model';
|
|
3
3
|
import { ModelType } from '@travetto/model/src/types/model';
|
|
4
4
|
import { SchemaRegistry } from '@travetto/schema';
|
|
5
5
|
|
|
6
|
-
import { ModelQuery, Query } from '../../model/query';
|
|
7
6
|
import { WhereClause, WhereClauseRaw } from '../../model/where-clause';
|
|
8
|
-
import { QueryLanguageParser } from '../query/parser';
|
|
9
|
-
import { QueryVerifier } from '../query/verifier';
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* Common model utils, that should be usable by end users
|
|
@@ -40,24 +37,21 @@ export class ModelQueryUtil {
|
|
|
40
37
|
/**
|
|
41
38
|
* Get a where clause with type
|
|
42
39
|
*/
|
|
43
|
-
static getWhereClause<T extends ModelType>(cls: Class<T>,
|
|
44
|
-
let q: WhereClause<T> | undefined = o ? (typeof o === 'string' ? QueryLanguageParser.parseToQuery(o) : o) : undefined;
|
|
40
|
+
static getWhereClause<T extends ModelType>(cls: Class<T>, q: WhereClause<T> | undefined, checkExpiry = true): WhereClause<T> {
|
|
45
41
|
const clauses: WhereClauseRaw<T>[] = (q ? [q] : []);
|
|
46
42
|
|
|
47
43
|
const conf = ModelRegistry.get(cls);
|
|
48
44
|
if (conf.subType) {
|
|
49
45
|
const { subTypeField, subTypeName } = SchemaRegistry.get(cls);
|
|
50
|
-
|
|
51
|
-
clauses.push({ [subTypeField]: subTypeName } as WhereClauseRaw<T>);
|
|
46
|
+
clauses.push(castTo({ [subTypeField]: subTypeName }));
|
|
52
47
|
}
|
|
53
48
|
if (checkExpiry && conf.expiresAt) {
|
|
54
|
-
|
|
55
|
-
clauses.push({
|
|
49
|
+
clauses.push(castTo({
|
|
56
50
|
$or: [
|
|
57
51
|
{ [conf.expiresAt]: { $exists: false } },
|
|
58
52
|
{ [conf.expiresAt]: { $gte: new Date() } },
|
|
59
53
|
]
|
|
60
|
-
}
|
|
54
|
+
}));
|
|
61
55
|
}
|
|
62
56
|
if (clauses.length > 1) {
|
|
63
57
|
q = { $and: clauses };
|
|
@@ -67,30 +61,10 @@ export class ModelQueryUtil {
|
|
|
67
61
|
return q!;
|
|
68
62
|
}
|
|
69
63
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
query.where = this.getWhereClause(cls, query.where, checkExpiry);
|
|
77
|
-
QueryVerifier.verify(cls, query);
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
79
|
-
return query as U & { where: WhereClause<T> };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get query with an id enforced
|
|
84
|
-
*/
|
|
85
|
-
static getQueryWithId<T extends ModelType, U extends Query<T> | ModelQuery<T>>(
|
|
86
|
-
cls: Class<T>,
|
|
87
|
-
item: T,
|
|
88
|
-
query: U
|
|
89
|
-
): U & { where: WhereClause<T> & { id: string } } {
|
|
90
|
-
query.where = this.getWhereClause(cls, query.where);
|
|
91
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
92
|
-
(query.where as WhereClauseRaw<ModelType>).id = item.id;
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
94
|
-
return query as U & { where: WhereClause<T> & { id: string } };
|
|
95
|
-
}
|
|
64
|
+
static has$And = (o: unknown): o is ({ $and: WhereClause<unknown>[] }) =>
|
|
65
|
+
!!o && typeof o === 'object' && '$and' in o;
|
|
66
|
+
static has$Or = (o: unknown): o is ({ $or: WhereClause<unknown>[] }) =>
|
|
67
|
+
!!o && typeof o === 'object' && '$or' in o;
|
|
68
|
+
static has$Not = (o: unknown): o is ({ $not: WhereClause<unknown> }) =>
|
|
69
|
+
!!o && typeof o === 'object' && '$not' in o;
|
|
96
70
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ModelRegistry, ModelType } from '@travetto/model';
|
|
2
|
-
import { Class } from '@travetto/runtime';
|
|
2
|
+
import { castTo, Class } from '@travetto/runtime';
|
|
3
3
|
import { SchemaRegistry } from '@travetto/schema';
|
|
4
4
|
|
|
5
5
|
import { PageableModelQuery, Query } from '../../model/query';
|
|
@@ -35,8 +35,7 @@ export class ModelQuerySuggestUtil {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (query?.where) {
|
|
38
|
-
|
|
39
|
-
clauses.push(query.where! as WhereClauseRaw<ModelType>);
|
|
38
|
+
clauses.push(query.where);
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
return {
|
|
@@ -59,12 +58,11 @@ export class ModelQuerySuggestUtil {
|
|
|
59
58
|
): U[] {
|
|
60
59
|
const pattern = this.getSuggestRegex(prefix);
|
|
61
60
|
|
|
62
|
-
const out: [string, U][] = [];
|
|
61
|
+
const out: ([string, U] | readonly [string, U])[] = [];
|
|
63
62
|
for (const r of results) {
|
|
64
63
|
const val = r[field];
|
|
65
64
|
if (Array.isArray(val)) {
|
|
66
|
-
|
|
67
|
-
out.push(...val.filter(f => pattern.test(f)).map((f: string) => [f, transform(f, r)] as [string, U]));
|
|
65
|
+
out.push(...val.filter(f => pattern.test(f)).map((f: string) => [f, transform(f, r)] as const));
|
|
68
66
|
} else if (typeof val === 'string') {
|
|
69
67
|
out.push([val, transform(val, r)]);
|
|
70
68
|
}
|
|
@@ -81,10 +79,9 @@ export class ModelQuerySuggestUtil {
|
|
|
81
79
|
*/
|
|
82
80
|
static getSuggestFieldQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Query<T> {
|
|
83
81
|
const config = ModelRegistry.get(cls);
|
|
84
|
-
|
|
85
|
-
return this.getSuggestQuery<ModelType>(cls, field as ValidStringFields<ModelType>, prefix, {
|
|
82
|
+
return this.getSuggestQuery<T>(cls, castTo(field), prefix, {
|
|
86
83
|
...(query ?? {}),
|
|
87
|
-
select: { [field]: true, ...(config.subType ? { [SchemaRegistry.get(cls).subTypeField]: true } : {}) }
|
|
88
|
-
})
|
|
84
|
+
select: castTo({ [field]: true, ...(config.subType ? { [SchemaRegistry.get(cls).subTypeField]: true } : {}) })
|
|
85
|
+
});
|
|
89
86
|
}
|
|
90
87
|
}
|
package/src/model/query.ts
CHANGED
|
@@ -87,5 +87,4 @@ export type WhereClause<T> = WhereClauseRaw<RetainFields<T>>;
|
|
|
87
87
|
export type ValidStringFields<T> = {
|
|
88
88
|
[K in Extract<keyof T, string>]:
|
|
89
89
|
(T[K] extends (String | string | string[] | String[] | undefined) ? K : never)
|
|
90
|
-
}[Extract<keyof T, string>];
|
|
91
|
-
|
|
90
|
+
}[Extract<keyof T, string>];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DataUtil, SchemaRegistry, ValidationResultError, ValidationError } from '@travetto/schema';
|
|
2
2
|
import { Class } from '@travetto/runtime';
|
|
3
3
|
|
|
4
|
-
import { ModelQuery, Query, PageableModelQuery } from '
|
|
4
|
+
import { ModelQuery, Query, PageableModelQuery } from './model/query';
|
|
5
5
|
|
|
6
|
-
import { TypeUtil } from '
|
|
6
|
+
import { TypeUtil } from './internal/util/types';
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
type SimpleType = keyof typeof TypeUtil.OPERATORS;
|
|
@@ -269,7 +269,11 @@ export class QueryVerifier {
|
|
|
269
269
|
/**
|
|
270
270
|
* Verify the query
|
|
271
271
|
*/
|
|
272
|
-
static verify<T>(cls: Class<T>, query
|
|
272
|
+
static verify<T>(cls: Class<T>, query?: ModelQuery<T> | Query<T> | PageableModelQuery<T>): void {
|
|
273
|
+
if (!query) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
273
277
|
const errors: ValidationError[] = [];
|
|
274
278
|
|
|
275
279
|
const state = {
|
|
@@ -287,26 +291,26 @@ export class QueryVerifier {
|
|
|
287
291
|
|
|
288
292
|
// Check all the clauses
|
|
289
293
|
for (const [key, fn] of this.#mapping) {
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
if (key === 'sort') {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
292
297
|
|
|
293
298
|
if (!(key in query)
|
|
294
|
-
|| query[
|
|
295
|
-
|| query[
|
|
299
|
+
|| query[key] === undefined
|
|
300
|
+
|| query[key] === null
|
|
296
301
|
) {
|
|
297
302
|
continue;
|
|
298
303
|
}
|
|
299
304
|
|
|
300
|
-
const val = query[
|
|
305
|
+
const val = query[key];
|
|
301
306
|
const subState = state.extend(key);
|
|
302
307
|
|
|
303
308
|
if (Array.isArray(val)) {
|
|
304
309
|
for (const el of val) {
|
|
305
310
|
this[fn](subState, cls, el);
|
|
306
311
|
}
|
|
307
|
-
} else {
|
|
308
|
-
|
|
309
|
-
this[fn](subState, cls, val as object);
|
|
312
|
+
} else if (typeof val !== 'string') {
|
|
313
|
+
this[fn](subState, cls, val);
|
|
310
314
|
}
|
|
311
315
|
}
|
|
312
316
|
|
package/support/test/crud.ts
CHANGED
|
@@ -111,7 +111,7 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
111
111
|
|
|
112
112
|
assert(await svc.queryCount(Person, {}) === 3);
|
|
113
113
|
|
|
114
|
-
const c2 = await svc.deleteByQuery(Person, { where:
|
|
114
|
+
const c2 = await svc.deleteByQuery(Person, { where: { age: { $lte: 3 } } });
|
|
115
115
|
|
|
116
116
|
assert(c2 === 3);
|
|
117
117
|
|
|
@@ -135,7 +135,7 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
135
135
|
|
|
136
136
|
assert(count === 5);
|
|
137
137
|
|
|
138
|
-
assert(await svc.queryCount(Person, { where:
|
|
138
|
+
assert(await svc.queryCount(Person, { where: { gender: 'm' } }) === 5);
|
|
139
139
|
|
|
140
140
|
const c = await svc.updateByQuery(Person, { where: { age: { $gt: 3 } } }, { gender: 'f' });
|
|
141
141
|
|
|
@@ -145,12 +145,12 @@ export abstract class ModelQueryCrudSuite extends BaseModelSuite<ModelQueryCrudS
|
|
|
145
145
|
|
|
146
146
|
assert(await svc.queryCount(Person, { where: { gender: 'm' } }) === 3);
|
|
147
147
|
|
|
148
|
-
const c2 = await svc.updateByQuery(Person, { where:
|
|
148
|
+
const c2 = await svc.updateByQuery(Person, { where: { gender: 'm' } }, { gender: 'f' });
|
|
149
149
|
|
|
150
150
|
assert(c2 === 3);
|
|
151
151
|
|
|
152
152
|
assert(await svc.queryCount(Person, { where: { gender: 'f' } }) === 5);
|
|
153
|
-
assert(await svc.queryCount(Person, { where:
|
|
153
|
+
assert(await svc.queryCount(Person, { where: { gender: 'm' } }) === 0);
|
|
154
154
|
|
|
155
155
|
}
|
|
156
156
|
}
|
package/support/test/facet.ts
CHANGED
|
@@ -7,11 +7,11 @@ import { Suite, Test } from '@travetto/test';
|
|
|
7
7
|
import { Person } from './types';
|
|
8
8
|
import { ModelQueryFacetSupport } from '../../src/service/facet';
|
|
9
9
|
|
|
10
|
-
const pick = <T>(arr: T[]): T => arr[Math.trunc(Math.random() * arr.length)]!;
|
|
10
|
+
const pick = <T>(arr: T[] | readonly T[]): T => arr[Math.trunc(Math.random() * arr.length)]!;
|
|
11
11
|
|
|
12
|
-
const GENDERS = ['m', 'f'] as
|
|
12
|
+
const GENDERS = ['m', 'f'] as const;
|
|
13
13
|
const FNAME = ['Bob', 'Tom', 'Sarah', 'Leo', 'Alice', 'Jennifer', 'Tommy', 'George', 'Paula', 'Sam'];
|
|
14
|
-
const LNAME = ['Smith', 'Sampson', 'Thompson', 'Oscar', 'Washington', 'Jefferson', '
|
|
14
|
+
const LNAME = ['Smith', 'Sampson', 'Thompson', 'Oscar', 'Washington', 'Jefferson', 'Samuel'];
|
|
15
15
|
const AGES = new Array(100).fill(0).map((x, i) => i + 10);
|
|
16
16
|
|
|
17
17
|
@Suite()
|
|
@@ -12,6 +12,7 @@ import { ModelQueryFacetSupport } from '../../src/service/facet';
|
|
|
12
12
|
import { ModelQuerySuggestSupport } from '../../src/service/suggest';
|
|
13
13
|
|
|
14
14
|
import { isQueryCrudSupported, isQueryFacetSupported, isQuerySuggestSupported } from '../../src/internal/service/common';
|
|
15
|
+
import { castTo } from '@travetto/runtime';
|
|
15
16
|
|
|
16
17
|
@Suite()
|
|
17
18
|
export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQuerySupport & ModelCrudSupport> {
|
|
@@ -33,9 +34,9 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
|
|
|
33
34
|
assert((await svc.query(Doctor, {})).length === 2);
|
|
34
35
|
assert((await svc.query(Engineer, {})).length === 1);
|
|
35
36
|
|
|
36
|
-
assert(await svc.queryCount(Worker, { where:
|
|
37
|
-
assert(await svc.queryCount(Doctor, { where:
|
|
38
|
-
assert(await svc.queryCount(Engineer, { where:
|
|
37
|
+
assert(await svc.queryCount(Worker, { where: { name: 'bob' } }) === 1);
|
|
38
|
+
assert(await svc.queryCount(Doctor, { where: { name: 'bob' } }) === 1);
|
|
39
|
+
assert(await svc.queryCount(Engineer, { where: { name: 'bob' } }) === 0);
|
|
39
40
|
|
|
40
41
|
assert((await svc.queryOne(Worker, { where: { name: 'bob' } })) instanceof Doctor);
|
|
41
42
|
await assert.rejects(() => svc.queryOne(Firefighter, { where: { name: 'bob' } }), NotFoundError);
|
|
@@ -43,7 +44,7 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
|
|
|
43
44
|
|
|
44
45
|
@Test({ skip: ModelQueryPolymorphismSuite.ifNot(isQueryCrudSupported) })
|
|
45
46
|
async testCrudQuery() {
|
|
46
|
-
const svc = await this.service
|
|
47
|
+
const svc: ModelQueryCrudSupport & ModelQuerySupport = castTo(await this.service);
|
|
47
48
|
const [doc, doc2, fire, eng] = [
|
|
48
49
|
Doctor.from({ name: 'bob', specialty: 'feet' }),
|
|
49
50
|
Doctor.from({ name: 'nob', specialty: 'eyes' }),
|
|
@@ -69,7 +70,7 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
|
|
|
69
70
|
|
|
70
71
|
@Test({ skip: ModelQueryPolymorphismSuite.ifNot(isQuerySuggestSupported) })
|
|
71
72
|
async testSuggestQuery() {
|
|
72
|
-
const svc = await this.service
|
|
73
|
+
const svc: ModelQuerySuggestSupport & ModelQuerySupport = castTo(await this.service);
|
|
73
74
|
const [doc, doc2, fire, eng] = [
|
|
74
75
|
Doctor.from({ name: 'bob', specialty: 'eyes' }),
|
|
75
76
|
Doctor.from({ name: 'nob', specialty: 'eyes' }),
|
|
@@ -93,7 +94,7 @@ export abstract class ModelQueryPolymorphismSuite extends BaseModelSuite<ModelQu
|
|
|
93
94
|
|
|
94
95
|
@Test({ skip: ModelQueryPolymorphismSuite.ifNot(isQueryFacetSupported) })
|
|
95
96
|
async testFacetQuery() {
|
|
96
|
-
const svc = await this.service
|
|
97
|
+
const svc: ModelQueryFacetSupport & ModelQuerySupport = castTo(await this.service);
|
|
97
98
|
const [doc, doc2, fire, eng] = [
|
|
98
99
|
Doctor.from({ name: 'bob', specialty: 'eyes' }),
|
|
99
100
|
Doctor.from({ name: 'nob', specialty: 'eyes' }),
|
package/support/test/query.ts
CHANGED
|
@@ -18,7 +18,7 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
|
|
|
18
18
|
async testWordBoundary() {
|
|
19
19
|
const service = await this.service;
|
|
20
20
|
await this.saveAll(Person, [1, 2, 3, 8].map(x => Person.from({
|
|
21
|
-
name: 'Bob
|
|
21
|
+
name: 'Bob Omber',
|
|
22
22
|
age: 20 + x,
|
|
23
23
|
gender: 'm',
|
|
24
24
|
address: {
|
|
@@ -27,13 +27,13 @@ export abstract class ModelQuerySuite extends BaseModelSuite<ModelQuerySupport &
|
|
|
27
27
|
}
|
|
28
28
|
})));
|
|
29
29
|
|
|
30
|
-
const results = await service.query(Person, { where:
|
|
30
|
+
const results = await service.query(Person, { where: { name: { $regex: /\bomb.*/i } } });
|
|
31
31
|
assert(results.length === 4);
|
|
32
32
|
|
|
33
|
-
const results2 = await service.query(Person, { where:
|
|
33
|
+
const results2 = await service.query(Person, { where: { name: { $regex: /\bmbo.*/i } } });
|
|
34
34
|
assert(results2.length === 0);
|
|
35
35
|
|
|
36
|
-
const results3 = await service.query(Person, { where:
|
|
36
|
+
const results3 = await service.query(Person, { where: { name: { $regex: /\bomb.*/ } } });
|
|
37
37
|
assert(results3.length === 0);
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { WhereClauseRaw } from '../../model/where-clause';
|
|
2
|
-
import { QueryLanguageTokenizer } from './tokenizer';
|
|
3
|
-
import { Token, Literal, GroupNode, OP_TRANSLATION, ArrayNode, AllNode } from './types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Determine if a token is boolean
|
|
7
|
-
*/
|
|
8
|
-
function isBoolean(o: unknown): o is Token & { type: 'boolean' } {
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
10
|
-
return !!o && (o as { type: string }).type === 'boolean';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Language parser
|
|
15
|
-
*/
|
|
16
|
-
export class QueryLanguageParser {
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Handle all clauses
|
|
20
|
-
*/
|
|
21
|
-
static handleClause(nodes: (AllNode | Token)[]): void {
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
23
|
-
const val = nodes.pop()! as Token | ArrayNode;
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
25
|
-
const op = nodes.pop()! as Token;
|
|
26
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
27
|
-
const ident = nodes.pop()! as Token;
|
|
28
|
-
|
|
29
|
-
// value isn't a literal or a list, bail
|
|
30
|
-
if (val.type !== 'literal' && val.type !== 'list') {
|
|
31
|
-
throw new Error(`Unexpected token: ${val.value}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// If operator is not an operator, bail
|
|
35
|
-
if (op.type !== 'operator') {
|
|
36
|
-
throw new Error(`Unexpected token: ${op.value}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// If operator is not known, bail
|
|
40
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
41
|
-
const finalOp = OP_TRANSLATION[op.value as string];
|
|
42
|
-
if (!finalOp) {
|
|
43
|
-
throw new Error(`Unexpected operator: ${op.value}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
nodes.push({
|
|
47
|
-
type: 'clause',
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
49
|
-
field: ident.value as string,
|
|
50
|
-
op: finalOp,
|
|
51
|
-
value: val.value
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Handle unary support
|
|
55
|
-
this.unary(nodes);
|
|
56
|
-
// Simplify as we go along
|
|
57
|
-
this.condense(nodes, 'and');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Condense nodes to remove unnecessary groupings
|
|
62
|
-
* (a AND (b AND (c AND d))) => (a AND b AND c)
|
|
63
|
-
*/
|
|
64
|
-
static condense(nodes: (AllNode | Token)[], op: 'and' | 'or'): void {
|
|
65
|
-
let second = nodes[nodes.length - 2];
|
|
66
|
-
|
|
67
|
-
while (isBoolean(second) && second.value === op) {
|
|
68
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
69
|
-
const right = nodes.pop()! as AllNode;
|
|
70
|
-
nodes.pop()!;
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
72
|
-
const left = nodes.pop()! as AllNode;
|
|
73
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
74
|
-
const rg = right as GroupNode;
|
|
75
|
-
if (rg.type === 'group' && rg.op === op) {
|
|
76
|
-
rg.value.unshift(left);
|
|
77
|
-
nodes.push(rg);
|
|
78
|
-
} else {
|
|
79
|
-
nodes.push({
|
|
80
|
-
type: 'group',
|
|
81
|
-
op,
|
|
82
|
-
value: [left, right]
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
second = nodes[nodes.length - 2];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Remove unnecessary unary nodes
|
|
91
|
-
* (((5))) => 5
|
|
92
|
-
*/
|
|
93
|
-
static unary(nodes: (AllNode | Token)[]): void {
|
|
94
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
95
|
-
const second = nodes[nodes.length - 2] as Token;
|
|
96
|
-
if (second && second.type === 'unary' && second.value === 'not') {
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
98
|
-
const node = nodes.pop()! as AllNode;
|
|
99
|
-
nodes.pop();
|
|
100
|
-
nodes.push({
|
|
101
|
-
type: 'unary',
|
|
102
|
-
op: 'not',
|
|
103
|
-
value: node
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Parse all tokens
|
|
110
|
-
*/
|
|
111
|
-
static parse(tokens: Token[], pos: number = 0): AllNode {
|
|
112
|
-
|
|
113
|
-
let top: (AllNode | Token)[] = [];
|
|
114
|
-
const stack: (typeof top)[] = [top];
|
|
115
|
-
let arr: Literal[] | undefined;
|
|
116
|
-
|
|
117
|
-
let token = tokens[pos];
|
|
118
|
-
while (token) {
|
|
119
|
-
switch (token.type) {
|
|
120
|
-
case 'grouping':
|
|
121
|
-
if (token.value === 'start') {
|
|
122
|
-
stack.push(top = []);
|
|
123
|
-
} else {
|
|
124
|
-
const group = stack.pop()!;
|
|
125
|
-
top = stack[stack.length - 1];
|
|
126
|
-
this.condense(group, 'or');
|
|
127
|
-
top.push(group[0]);
|
|
128
|
-
this.unary(top);
|
|
129
|
-
this.condense(top, 'and');
|
|
130
|
-
}
|
|
131
|
-
break;
|
|
132
|
-
case 'array':
|
|
133
|
-
if (token.value === 'start') {
|
|
134
|
-
arr = [];
|
|
135
|
-
} else {
|
|
136
|
-
const arrNode: ArrayNode = { type: 'list', value: arr! };
|
|
137
|
-
top.push(arrNode);
|
|
138
|
-
arr = undefined;
|
|
139
|
-
this.handleClause(top);
|
|
140
|
-
}
|
|
141
|
-
break;
|
|
142
|
-
case 'literal':
|
|
143
|
-
if (arr !== undefined) {
|
|
144
|
-
arr.push(token.value);
|
|
145
|
-
} else {
|
|
146
|
-
top.push(token);
|
|
147
|
-
this.handleClause(top);
|
|
148
|
-
}
|
|
149
|
-
break;
|
|
150
|
-
case 'punctuation':
|
|
151
|
-
if (!arr) {
|
|
152
|
-
throw new Error(`Invalid token: ${token.value}`);
|
|
153
|
-
}
|
|
154
|
-
break;
|
|
155
|
-
default:
|
|
156
|
-
top.push(token);
|
|
157
|
-
}
|
|
158
|
-
token = tokens[++pos];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
this.condense(top, 'or');
|
|
162
|
-
|
|
163
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
164
|
-
return top[0] as AllNode;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Convert Query AST to output
|
|
169
|
-
*/
|
|
170
|
-
static convert<T = unknown>(node: AllNode): WhereClauseRaw<T> {
|
|
171
|
-
switch (node.type) {
|
|
172
|
-
case 'unary': {
|
|
173
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
174
|
-
return { [`$${node.op!}`]: this.convert(node.value) } as WhereClauseRaw<T>;
|
|
175
|
-
}
|
|
176
|
-
case 'group': {
|
|
177
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
178
|
-
return { [`$${node.op!}`]: node.value.map(x => this.convert(x)) } as WhereClauseRaw<T>;
|
|
179
|
-
}
|
|
180
|
-
case 'clause': {
|
|
181
|
-
const parts = node.field!.split('.');
|
|
182
|
-
const top: WhereClauseRaw<T> = {};
|
|
183
|
-
let sub: Record<string, unknown> = top;
|
|
184
|
-
for (const p of parts) {
|
|
185
|
-
sub = sub[p] = {};
|
|
186
|
-
}
|
|
187
|
-
if (node.op === '$regex' && typeof node.value === 'string') {
|
|
188
|
-
sub[node.op!] = new RegExp(`^${node.value}`);
|
|
189
|
-
} else if ((node.op === '$eq' || node.op === '$ne') && node.value === null) {
|
|
190
|
-
sub.$exists = node.op !== '$eq';
|
|
191
|
-
} else if ((node.op === '$in' || node.op === '$nin') && !Array.isArray(node.value)) {
|
|
192
|
-
throw new Error(`Expected array literal for ${node.op}`);
|
|
193
|
-
} else {
|
|
194
|
-
sub[node.op!] = node.value;
|
|
195
|
-
}
|
|
196
|
-
return top;
|
|
197
|
-
}
|
|
198
|
-
default: throw new Error(`Unexpected node type: ${node.type}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Tokenize and parse text
|
|
204
|
-
*/
|
|
205
|
-
static parseToQuery<T = unknown>(text: string): WhereClauseRaw<T> {
|
|
206
|
-
const tokens = QueryLanguageTokenizer.tokenize(text);
|
|
207
|
-
const node = this.parse(tokens);
|
|
208
|
-
return this.convert(node);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { TimeUtil } from '@travetto/runtime';
|
|
2
|
-
import { Token, TokenizeState, TokenType } from './types';
|
|
3
|
-
|
|
4
|
-
const OPEN_PARENS = 0x28, CLOSE_PARENS = 0x29, OPEN_BRACKET = 0x5b, CLOSE_BRACKET = 0x5d, COMMA = 0x2c;
|
|
5
|
-
const GREATER_THAN = 0x3e, LESS_THAN = 0x3c, EQUAL = 0x3d, NOT = 0x21, MODULO = 0x25, TILDE = 0x7e, AND = 0x26, OR = 0x7c;
|
|
6
|
-
const SPACE = 0x20, TAB = 0x09;
|
|
7
|
-
const DBL_QUOTE = 0x22, SGL_QUOTE = 0x27, FORWARD_SLASH = 0x2f, BACKSLASH = 0x5c;
|
|
8
|
-
const PERIOD = 0x2e, UNDERSCORE = 0x54, DOLLAR_SIGN = 0x24, DASH = 0x2d;
|
|
9
|
-
const ZERO = 0x30, NINE = 0x39, UPPER_A = 0x41, UPPER_Z = 0x5a, LOWER_A = 0x61, LOWER_Z = 0x7a;
|
|
10
|
-
const LOWER_I = 0x69, LOWER_G = 0x67, LOWER_M = 0x6d, LOWER_S = 0x73;
|
|
11
|
-
|
|
12
|
-
const ESCAPE: Record<string, string> = {
|
|
13
|
-
'\\n': '\n',
|
|
14
|
-
'\\r': '\r',
|
|
15
|
-
'\\t': '\t',
|
|
16
|
-
'\\"': '"',
|
|
17
|
-
"\\'": "'"
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Mapping of keywords to node types and values
|
|
22
|
-
*/
|
|
23
|
-
const TOKEN_MAPPING: Record<string, Token> = {
|
|
24
|
-
and: { type: 'boolean', value: 'and' },
|
|
25
|
-
'&&': { type: 'boolean', value: 'and' },
|
|
26
|
-
or: { type: 'boolean', value: 'or' },
|
|
27
|
-
'||': { type: 'boolean', value: 'or' },
|
|
28
|
-
in: { type: 'operator', value: 'in' },
|
|
29
|
-
['not-in']: { type: 'operator', value: 'not-in' },
|
|
30
|
-
not: { type: 'unary', value: 'not' },
|
|
31
|
-
'[': { type: 'array', value: 'start' },
|
|
32
|
-
']': { type: 'array', value: 'end' },
|
|
33
|
-
'(': { type: 'grouping', value: 'start' },
|
|
34
|
-
')': { type: 'grouping', value: 'end' },
|
|
35
|
-
null: { type: 'literal', value: null },
|
|
36
|
-
true: { type: 'literal', value: true },
|
|
37
|
-
false: { type: 'literal', value: false },
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Tokenizer for the query language
|
|
42
|
-
*/
|
|
43
|
-
export class QueryLanguageTokenizer {
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Process the next token. Can specify expected type as needed
|
|
47
|
-
*/
|
|
48
|
-
static #processToken(state: TokenizeState, mode?: TokenType): Token {
|
|
49
|
-
const text = state.text.substring(state.start, state.pos);
|
|
50
|
-
const res = TOKEN_MAPPING[text.toLowerCase()];
|
|
51
|
-
let value: unknown = text;
|
|
52
|
-
if (!res && state.mode === 'literal') {
|
|
53
|
-
if (/^["']/.test(text)) {
|
|
54
|
-
value = text.substring(1, text.length - 1)
|
|
55
|
-
.replace(/\\[.]/g, (a, b) => ESCAPE[a] || b);
|
|
56
|
-
} else if (/^\//.test(text)) {
|
|
57
|
-
const start = 1;
|
|
58
|
-
const end = text.lastIndexOf('/');
|
|
59
|
-
value = new RegExp(text.substring(start, end), text.substring(end + 1));
|
|
60
|
-
} else if (/^-?\d+$/.test(text)) {
|
|
61
|
-
value = parseInt(text, 10);
|
|
62
|
-
} else if (/^-?\d+[.]\d+$/.test(text)) {
|
|
63
|
-
value = parseFloat(text);
|
|
64
|
-
} else if (TimeUtil.isTimeSpan(text)) {
|
|
65
|
-
value = text;
|
|
66
|
-
} else {
|
|
67
|
-
state.mode = 'identifier';
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return res ?? { value, type: state.mode || mode };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Flush state to output
|
|
75
|
-
*/
|
|
76
|
-
static #flush(state: TokenizeState, mode?: TokenType): void {
|
|
77
|
-
if ((!mode || !state.mode || mode !== state.mode) && state.start !== state.pos) {
|
|
78
|
-
if (state.mode !== 'whitespace') {
|
|
79
|
-
state.out.push(this.#processToken(state, mode));
|
|
80
|
-
}
|
|
81
|
-
state.start = state.pos;
|
|
82
|
-
}
|
|
83
|
-
state.mode = mode || state.mode;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Determine if valid regex flag
|
|
88
|
-
*/
|
|
89
|
-
static #isValidRegexFlag(ch: number): boolean {
|
|
90
|
-
return ch === LOWER_I || ch === LOWER_G || ch === LOWER_M || ch === LOWER_S;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Determine if valid token identifier
|
|
95
|
-
*/
|
|
96
|
-
static #isValidIdentToken(ch: number): boolean {
|
|
97
|
-
return (ch >= ZERO && ch <= NINE) ||
|
|
98
|
-
(ch >= UPPER_A && ch <= UPPER_Z) ||
|
|
99
|
-
(ch >= LOWER_A && ch <= LOWER_Z) ||
|
|
100
|
-
(ch === UNDERSCORE) ||
|
|
101
|
-
(ch === DASH) ||
|
|
102
|
-
(ch === DOLLAR_SIGN) ||
|
|
103
|
-
(ch === PERIOD);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Read string until quote
|
|
108
|
-
*/
|
|
109
|
-
static readString(text: string, pos: number): number {
|
|
110
|
-
const len = text.length;
|
|
111
|
-
const ch = text.charCodeAt(pos);
|
|
112
|
-
const q = ch;
|
|
113
|
-
pos += 1;
|
|
114
|
-
while (pos < len) {
|
|
115
|
-
if (text.charCodeAt(pos) === q) {
|
|
116
|
-
break;
|
|
117
|
-
} else if (text.charCodeAt(pos) === BACKSLASH) {
|
|
118
|
-
pos += 1;
|
|
119
|
-
}
|
|
120
|
-
pos += 1;
|
|
121
|
-
}
|
|
122
|
-
if (pos === len && text.charCodeAt(pos) !== q) {
|
|
123
|
-
throw new Error('Unterminated string literal');
|
|
124
|
-
}
|
|
125
|
-
return pos;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Tokenize a text string
|
|
130
|
-
*/
|
|
131
|
-
static tokenize(text: string): Token[] {
|
|
132
|
-
const state: TokenizeState = {
|
|
133
|
-
out: [],
|
|
134
|
-
pos: 0,
|
|
135
|
-
start: 0,
|
|
136
|
-
text,
|
|
137
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
138
|
-
mode: undefined! as TokenType
|
|
139
|
-
};
|
|
140
|
-
const len = text.length;
|
|
141
|
-
// Loop through each char
|
|
142
|
-
while (state.pos < len) {
|
|
143
|
-
// Read code as a number, more efficient
|
|
144
|
-
const ch = text.charCodeAt(state.pos);
|
|
145
|
-
switch (ch) {
|
|
146
|
-
// Handle punctuation
|
|
147
|
-
case OPEN_PARENS: case CLOSE_PARENS: case OPEN_BRACKET: case CLOSE_BRACKET: case COMMA:
|
|
148
|
-
this.#flush(state);
|
|
149
|
-
state.mode = 'punctuation';
|
|
150
|
-
break;
|
|
151
|
-
// Handle operator
|
|
152
|
-
case GREATER_THAN: case LESS_THAN: case EQUAL:
|
|
153
|
-
case MODULO: case NOT: case TILDE: case AND: case OR:
|
|
154
|
-
this.#flush(state, 'operator');
|
|
155
|
-
break;
|
|
156
|
-
// Handle whitespace
|
|
157
|
-
case SPACE: case TAB:
|
|
158
|
-
this.#flush(state, 'whitespace');
|
|
159
|
-
break;
|
|
160
|
-
// Handle quotes and slashes
|
|
161
|
-
case DBL_QUOTE: case SGL_QUOTE: case FORWARD_SLASH:
|
|
162
|
-
this.#flush(state);
|
|
163
|
-
state.mode = 'literal';
|
|
164
|
-
state.pos = this.readString(text, state.pos) + 1;
|
|
165
|
-
if (ch === FORWARD_SLASH) { // Read modifiers, not used by all, but useful in general
|
|
166
|
-
while (this.#isValidRegexFlag(text.charCodeAt(state.pos))) {
|
|
167
|
-
state.pos += 1;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
this.#flush(state);
|
|
171
|
-
continue;
|
|
172
|
-
// Handle literal
|
|
173
|
-
default:
|
|
174
|
-
if (this.#isValidIdentToken(ch)) {
|
|
175
|
-
this.#flush(state, 'literal');
|
|
176
|
-
} else {
|
|
177
|
-
throw new Error(`Invalid character: ${text.substring(Math.max(0, state.pos - 10), state.pos + 1)}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
state.pos += 1;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
this.#flush(state);
|
|
184
|
-
|
|
185
|
-
return state.out;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Supported token types
|
|
3
|
-
*/
|
|
4
|
-
export type TokenType =
|
|
5
|
-
'literal' | 'identifier' | 'boolean' |
|
|
6
|
-
'operator' | 'grouping' | 'array' |
|
|
7
|
-
'whitespace' | 'punctuation' | 'unary';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Tokenization state
|
|
11
|
-
*/
|
|
12
|
-
export interface TokenizeState {
|
|
13
|
-
out: Token[];
|
|
14
|
-
pos: number;
|
|
15
|
-
start: number;
|
|
16
|
-
text: string;
|
|
17
|
-
mode: TokenType;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Literal types
|
|
22
|
-
*/
|
|
23
|
-
export type Literal = boolean | null | string | number | RegExp | Date;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Token
|
|
27
|
-
*/
|
|
28
|
-
export interface Token {
|
|
29
|
-
type: TokenType;
|
|
30
|
-
value: Literal;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Base AST Node
|
|
35
|
-
*/
|
|
36
|
-
export interface Node<T extends string = string> {
|
|
37
|
-
type: T;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Simple clause
|
|
42
|
-
*/
|
|
43
|
-
export interface ClauseNode extends Node<'clause'> {
|
|
44
|
-
field?: string;
|
|
45
|
-
op?: string;
|
|
46
|
-
value?: Literal | Literal[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Grouping
|
|
51
|
-
*/
|
|
52
|
-
export interface GroupNode extends Node<'group'> {
|
|
53
|
-
op?: 'and' | 'or';
|
|
54
|
-
value: AllNode[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Unary node
|
|
59
|
-
*/
|
|
60
|
-
export interface UnaryNode extends Node<'unary'> {
|
|
61
|
-
op?: 'not';
|
|
62
|
-
value: AllNode;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Array node
|
|
67
|
-
*/
|
|
68
|
-
export interface ArrayNode extends Node<'list'> {
|
|
69
|
-
op?: 'not';
|
|
70
|
-
value: Literal[];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type AllNode = ArrayNode | UnaryNode | GroupNode | ClauseNode;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Translation of operators to model query keys
|
|
77
|
-
*/
|
|
78
|
-
export const OP_TRANSLATION: Record<string, string> = {
|
|
79
|
-
'<': '$lt', '<=': '$lte',
|
|
80
|
-
'>': '$gt', '>=': '$gte',
|
|
81
|
-
'!=': '$ne', '==': '$eq',
|
|
82
|
-
'~': '$regex', '!': '$not',
|
|
83
|
-
in: '$in', 'not-in': '$nin'
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const VALID_OPS = new Set(Object.keys(OP_TRANSLATION));
|