@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 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. In addition to the contract, this module provides support for textual query language that can be checked and parsed into the proper query structure.
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.7",
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.7",
31
- "@travetto/model": "^5.0.0-rc.7",
32
- "@travetto/schema": "^5.0.0-rc.7"
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.7"
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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>, o: WhereClause<T> | string | undefined, checkExpiry = true): WhereClause<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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
51
- clauses.push({ [subTypeField]: subTypeName } as WhereClauseRaw<T>);
46
+ clauses.push(castTo({ [subTypeField]: subTypeName }));
52
47
  }
53
48
  if (checkExpiry && conf.expiresAt) {
54
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
- } as WhereClauseRaw<T>);
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
- * Enrich query where clause, and verify query is correct
72
- */
73
- static getQueryAndVerify<T extends ModelType, U extends Query<T> | ModelQuery<T>>(
74
- cls: Class<T>, query: U, checkExpiry = true
75
- ): U & { where: WhereClause<T> } {
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
- }) as Query<T>;
84
+ select: castTo({ [field]: true, ...(config.subType ? { [SchemaRegistry.get(cls).subTypeField]: true } : {}) })
85
+ });
89
86
  }
90
87
  }
@@ -22,7 +22,7 @@ type QueryOptionsRaw<T> = {
22
22
 
23
23
  type QueryMain<T> = {
24
24
  select?: SelectClauseRaw<T>;
25
- where?: WhereClauseRaw<T> | string;
25
+ where?: WhereClauseRaw<T>;
26
26
  // TODO: Add grouping in later
27
27
  // group?: GroupClauseRaw<T>;
28
28
  };
@@ -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 '../../model/query';
4
+ import { ModelQuery, Query, PageableModelQuery } from './model/query';
5
5
 
6
- import { TypeUtil } from '../util/types';
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: ModelQuery<T> | Query<T> | PageableModelQuery<T>): void {
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
291
- const queryKey = key as keyof typeof query;
294
+ if (key === 'sort') {
295
+ continue;
296
+ }
292
297
 
293
298
  if (!(key in query)
294
- || query[queryKey] === undefined
295
- || query[queryKey] === null
299
+ || query[key] === undefined
300
+ || query[key] === null
296
301
  ) {
297
302
  continue;
298
303
  }
299
304
 
300
- const val = query[queryKey];
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
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
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
 
@@ -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: 'age <= 3' });
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: 'gender == "m"' }) === 5);
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: 'gender == "m"' }, { gender: 'f' });
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: 'gender == "m"' }) === 0);
153
+ assert(await svc.queryCount(Person, { where: { gender: 'm' } }) === 0);
154
154
 
155
155
  }
156
156
  }
@@ -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 ['m', 'f'];
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', 'Samuels'];
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: 'name == "bob"' }) === 1);
37
- assert(await svc.queryCount(Doctor, { where: 'name == "bob"' }) === 1);
38
- assert(await svc.queryCount(Engineer, { where: 'name == "bob"' }) === 0);
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 as unknown as ModelQueryCrudSupport & ModelQuerySupport;
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 as unknown as ModelQuerySuggestSupport & ModelQuerySupport;
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 as unknown as ModelQueryFacetSupport & ModelQuerySupport;
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' }),
@@ -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 Ombo',
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: 'name ~ /\\bomb.*/i' });
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: 'name ~ /\\bmbo.*/i' });
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: 'name ~ /\\bomb.*/' });
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));