@travetto/model-query 2.1.5 → 2.2.0
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 +5 -5
- package/package.json +4 -4
- package/src/internal/model/point.ts +2 -1
- package/src/internal/query/parser.ts +41 -29
- package/src/internal/query/tokenizer.ts +9 -8
- package/src/internal/query/types.ts +10 -12
- package/src/internal/query/verifier.ts +18 -14
- package/src/internal/service/common.ts +4 -0
- package/src/internal/service/expiry.ts +1 -1
- package/src/internal/service/query.ts +12 -6
- package/src/internal/service/suggest.ts +13 -8
- package/src/internal/util/types.ts +11 -11
- package/src/model/where-clause.ts +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
npm install @travetto/model-query
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
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
|
|
11
|
+
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.
|
|
12
12
|
|
|
13
13
|
## Contracts
|
|
14
14
|
|
|
@@ -41,7 +41,7 @@ export interface ModelQuerySupport {
|
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/crud.ts#L11)
|
|
44
|
-
Reinforcing the complexity provided in these contracts, the [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/crud.ts#L11) contract allows for bulk update/deletion by query. This requires the underlying
|
|
44
|
+
Reinforcing the complexity provided in these contracts, the [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/crud.ts#L11) contract allows for bulk update/deletion by query. This requires the underlying implementation to support these operations.
|
|
45
45
|
|
|
46
46
|
**Code: Query Crud**
|
|
47
47
|
```typescript
|
|
@@ -79,7 +79,7 @@ export interface ModelQueryFacetSupport extends ModelQuerySupport {
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
### [Suggest](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/suggest.ts#L12)
|
|
82
|
-
Additionally, this same pattern avails it self in a set of suggestion methods that allow for powering auto completion and
|
|
82
|
+
Additionally, this same pattern avails it self in a set of suggestion methods that allow for powering auto completion and type-ahead functionalities.
|
|
83
83
|
|
|
84
84
|
**Code: Suggest**
|
|
85
85
|
```typescript
|
|
@@ -129,7 +129,7 @@ One of the complexities of abstracting multiple storage mechanisms, is providing
|
|
|
129
129
|
* `field: { $in: T[] }` to see if a record's value appears in the array provided to `$in`
|
|
130
130
|
* `field: { $nin: T[] }` to see if a record's value does not appear in the array provided to `$in`
|
|
131
131
|
|
|
132
|
-
### Ordered
|
|
132
|
+
### Ordered Numeric Fields
|
|
133
133
|
|
|
134
134
|
* `field: { $lt: number }` checks if value is less than
|
|
135
135
|
* `field: { $lte: number }` checks if value is less than or equal to
|
|
@@ -142,7 +142,7 @@ One of the complexities of abstracting multiple storage mechanisms, is providing
|
|
|
142
142
|
* `field: { $gt: Date | RelativeTime }` checks if value is greater than
|
|
143
143
|
* `field: { $gte: Date | RelativeTime }` checks if value is greater than or equal to
|
|
144
144
|
|
|
145
|
-
**Note**: Relative times are strings consisting of a number and a unit. e.g. -1w or 30d. These times are always relative to Date.now, but should make building
|
|
145
|
+
**Note**: Relative times are strings consisting of a number and a unit. e.g. -1w or 30d. These times are always relative to Date.now, but should make building queries more natural.
|
|
146
146
|
|
|
147
147
|
### Array Fields
|
|
148
148
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-query",
|
|
3
3
|
"displayName": "Data Model Querying",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"description": "Datastore abstraction for advanced query support.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"datastore",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"directory": "module/model-query"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/di": "^2.
|
|
32
|
-
"@travetto/model": "^2.
|
|
33
|
-
"@travetto/schema": "^2.
|
|
31
|
+
"@travetto/di": "^2.2.0",
|
|
32
|
+
"@travetto/model": "^2.2.0",
|
|
33
|
+
"@travetto/schema": "^2.2.0"
|
|
34
34
|
},
|
|
35
35
|
"docDependencies": {
|
|
36
36
|
"@travetto/model-mongo": true,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Util } from '@travetto/base';
|
|
2
2
|
|
|
3
3
|
export class PointImpl {
|
|
4
|
-
static validateSchema(input: unknown) {
|
|
4
|
+
static validateSchema(input: unknown): 'type' | undefined {
|
|
5
5
|
const ret = this.bindSchema(input);
|
|
6
6
|
return ret && !isNaN(ret[0]) && !isNaN(ret[1]) ? undefined : 'type';
|
|
7
7
|
}
|
|
8
8
|
static bindSchema(input: unknown): [number, number] | undefined {
|
|
9
9
|
if (Array.isArray(input) && input.length === 2) {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
10
11
|
return input.map(x => Util.coerceType(x, Number, false)) as [number, number];
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { WhereClauseRaw } from '../../model/where-clause';
|
|
2
2
|
import { QueryLanguageTokenizer } from './tokenizer';
|
|
3
|
-
import {
|
|
3
|
+
import { Token, Literal, GroupNode, OP_TRANSLATION, ArrayNode, AllNode } from './types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Determine if a token is boolean
|
|
7
7
|
*/
|
|
8
8
|
function isBoolean(o: unknown): o is Token & { type: 'boolean' } {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
9
10
|
return !!o && (o as { type: string }).type === 'boolean';
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -17,9 +18,12 @@ export class QueryLanguageParser {
|
|
|
17
18
|
/**
|
|
18
19
|
* Handle all clauses
|
|
19
20
|
*/
|
|
20
|
-
static handleClause(nodes: (
|
|
21
|
+
static handleClause(nodes: (AllNode | Token)[]): void {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
21
23
|
const val = nodes.pop()! as Token | ArrayNode;
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
22
25
|
const op = nodes.pop()! as Token;
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
23
27
|
const ident = nodes.pop()! as Token;
|
|
24
28
|
|
|
25
29
|
// value isn't a literal or a list, bail
|
|
@@ -33,6 +37,7 @@ export class QueryLanguageParser {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
// If operator is not known, bail
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
36
41
|
const finalOp = OP_TRANSLATION[op.value as string];
|
|
37
42
|
if (!finalOp) {
|
|
38
43
|
throw new Error(`Unexpected operator: ${op.value}`);
|
|
@@ -40,10 +45,11 @@ export class QueryLanguageParser {
|
|
|
40
45
|
|
|
41
46
|
nodes.push({
|
|
42
47
|
type: 'clause',
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
43
49
|
field: ident.value as string,
|
|
44
50
|
op: finalOp,
|
|
45
51
|
value: val.value
|
|
46
|
-
}
|
|
52
|
+
});
|
|
47
53
|
|
|
48
54
|
// Handle unary support
|
|
49
55
|
this.unary(nodes);
|
|
@@ -55,13 +61,16 @@ export class QueryLanguageParser {
|
|
|
55
61
|
* Condense nodes to remove unnecessary groupings
|
|
56
62
|
* (a AND (b AND (c AND d))) => (a AND b AND c)
|
|
57
63
|
*/
|
|
58
|
-
static condense(nodes: (
|
|
64
|
+
static condense(nodes: (AllNode | Token)[], op: 'and' | 'or'): void {
|
|
59
65
|
let second = nodes[nodes.length - 2];
|
|
60
66
|
|
|
61
67
|
while (isBoolean(second) && second.value === op) {
|
|
62
|
-
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
69
|
+
const right = nodes.pop()! as AllNode;
|
|
63
70
|
nodes.pop()!;
|
|
64
|
-
|
|
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
|
|
65
74
|
const rg = right as GroupNode;
|
|
66
75
|
if (rg.type === 'group' && rg.op === op) {
|
|
67
76
|
rg.value.unshift(left);
|
|
@@ -71,7 +80,7 @@ export class QueryLanguageParser {
|
|
|
71
80
|
type: 'group',
|
|
72
81
|
op,
|
|
73
82
|
value: [left, right]
|
|
74
|
-
}
|
|
83
|
+
});
|
|
75
84
|
}
|
|
76
85
|
second = nodes[nodes.length - 2];
|
|
77
86
|
}
|
|
@@ -81,25 +90,27 @@ export class QueryLanguageParser {
|
|
|
81
90
|
* Remove unnecessary unary nodes
|
|
82
91
|
* (((5))) => 5
|
|
83
92
|
*/
|
|
84
|
-
static unary(nodes: (
|
|
93
|
+
static unary(nodes: (AllNode | Token)[]): void {
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
85
95
|
const second = nodes[nodes.length - 2] as Token;
|
|
86
96
|
if (second && second.type === 'unary' && second.value === 'not') {
|
|
87
|
-
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
98
|
+
const node = nodes.pop()! as AllNode;
|
|
88
99
|
nodes.pop();
|
|
89
100
|
nodes.push({
|
|
90
101
|
type: 'unary',
|
|
91
102
|
op: 'not',
|
|
92
103
|
value: node
|
|
93
|
-
}
|
|
104
|
+
});
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
/**
|
|
98
109
|
* Parse all tokens
|
|
99
110
|
*/
|
|
100
|
-
static parse(tokens: Token[], pos: number = 0):
|
|
111
|
+
static parse(tokens: Token[], pos: number = 0): AllNode {
|
|
101
112
|
|
|
102
|
-
let top: (
|
|
113
|
+
let top: (AllNode | Token)[] = [];
|
|
103
114
|
const stack: (typeof top)[] = [top];
|
|
104
115
|
let arr: Literal[] | undefined;
|
|
105
116
|
|
|
@@ -122,7 +133,8 @@ export class QueryLanguageParser {
|
|
|
122
133
|
if (token.value === 'start') {
|
|
123
134
|
arr = [];
|
|
124
135
|
} else {
|
|
125
|
-
|
|
136
|
+
const arrNode: ArrayNode = { type: 'list', value: arr! };
|
|
137
|
+
top.push(arrNode);
|
|
126
138
|
arr = undefined;
|
|
127
139
|
this.handleClause(top);
|
|
128
140
|
}
|
|
@@ -148,38 +160,38 @@ export class QueryLanguageParser {
|
|
|
148
160
|
|
|
149
161
|
this.condense(top, 'or');
|
|
150
162
|
|
|
151
|
-
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
164
|
+
return top[0] as AllNode;
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
/**
|
|
155
168
|
* Convert Query AST to output
|
|
156
169
|
*/
|
|
157
|
-
static convert<T = unknown>(node:
|
|
170
|
+
static convert<T = unknown>(node: AllNode): WhereClauseRaw<T> {
|
|
158
171
|
switch (node.type) {
|
|
159
172
|
case 'unary': {
|
|
160
|
-
|
|
161
|
-
return { [`$${
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
174
|
+
return { [`$${node.op!}`]: this.convert(node.value) } as WhereClauseRaw<T>;
|
|
162
175
|
}
|
|
163
176
|
case 'group': {
|
|
164
|
-
|
|
165
|
-
return { [`$${
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
178
|
+
return { [`$${node.op!}`]: node.value.map(x => this.convert(x)) } as WhereClauseRaw<T>;
|
|
166
179
|
}
|
|
167
180
|
case 'clause': {
|
|
168
|
-
const
|
|
169
|
-
const parts = cn.field!.split('.');
|
|
181
|
+
const parts = node.field!.split('.');
|
|
170
182
|
const top: WhereClauseRaw<T> = {};
|
|
171
|
-
let sub
|
|
183
|
+
let sub: Record<string, unknown> = top;
|
|
172
184
|
for (const p of parts) {
|
|
173
185
|
sub = sub[p] = {};
|
|
174
186
|
}
|
|
175
|
-
if (
|
|
176
|
-
sub[
|
|
177
|
-
} else if ((
|
|
178
|
-
sub.$exists =
|
|
179
|
-
} else if ((
|
|
180
|
-
throw new Error(`Expected array literal for ${
|
|
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}`);
|
|
181
193
|
} else {
|
|
182
|
-
sub[
|
|
194
|
+
sub[node.op!] = node.value;
|
|
183
195
|
}
|
|
184
196
|
return top;
|
|
185
197
|
}
|
|
@@ -5,7 +5,7 @@ const OPEN_PARENS = 0x28, CLOSE_PARENS = 0x29, OPEN_BRACKET = 0x5b, CLOSE_BRACKE
|
|
|
5
5
|
const GREATER_THAN = 0x3e, LESS_THAN = 0x3c, EQUAL = 0x3d, NOT = 0x21, MODULO = 0x25, TILDE = 0x7e, AND = 0x26, OR = 0x7c;
|
|
6
6
|
const SPACE = 0x20, TAB = 0x09;
|
|
7
7
|
const DBL_QUOTE = 0x22, SGL_QUOTE = 0x27, FORWARD_SLASH = 0x2f, BACKSLASH = 0x5c;
|
|
8
|
-
const PERIOD = 0x2e, UNDERSCORE = 0x54,
|
|
8
|
+
const PERIOD = 0x2e, UNDERSCORE = 0x54, DOLLAR_SIGN = 0x24, DASH = 0x2d;
|
|
9
9
|
const ZERO = 0x30, NINE = 0x39, UPPER_A = 0x41, UPPER_Z = 0x5a, LOWER_A = 0x61, LOWER_Z = 0x7a;
|
|
10
10
|
const LOWER_I = 0x69, LOWER_G = 0x67, LOWER_M = 0x6d, LOWER_S = 0x73;
|
|
11
11
|
|
|
@@ -45,7 +45,7 @@ export class QueryLanguageTokenizer {
|
|
|
45
45
|
/**
|
|
46
46
|
* Process the next token. Can specify expected type as needed
|
|
47
47
|
*/
|
|
48
|
-
static #processToken(state: TokenizeState, mode?: TokenType) {
|
|
48
|
+
static #processToken(state: TokenizeState, mode?: TokenType): Token {
|
|
49
49
|
const text = state.text.substring(state.start, state.pos);
|
|
50
50
|
const res = TOKEN_MAPPING[text.toLowerCase()];
|
|
51
51
|
let value: unknown = text;
|
|
@@ -73,7 +73,7 @@ export class QueryLanguageTokenizer {
|
|
|
73
73
|
/**
|
|
74
74
|
* Flush state to output
|
|
75
75
|
*/
|
|
76
|
-
static #flush(state: TokenizeState, mode?: TokenType) {
|
|
76
|
+
static #flush(state: TokenizeState, mode?: TokenType): void {
|
|
77
77
|
if ((!mode || !state.mode || mode !== state.mode) && state.start !== state.pos) {
|
|
78
78
|
if (state.mode !== 'whitespace') {
|
|
79
79
|
state.out.push(this.#processToken(state, mode));
|
|
@@ -86,27 +86,27 @@ export class QueryLanguageTokenizer {
|
|
|
86
86
|
/**
|
|
87
87
|
* Determine if valid regex flag
|
|
88
88
|
*/
|
|
89
|
-
static #isValidRegexFlag(ch: number) {
|
|
89
|
+
static #isValidRegexFlag(ch: number): boolean {
|
|
90
90
|
return ch === LOWER_I || ch === LOWER_G || ch === LOWER_M || ch === LOWER_S;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Determine if valid token identifier
|
|
95
95
|
*/
|
|
96
|
-
static #isValidIdentToken(ch: number) {
|
|
96
|
+
static #isValidIdentToken(ch: number): boolean {
|
|
97
97
|
return (ch >= ZERO && ch <= NINE) ||
|
|
98
98
|
(ch >= UPPER_A && ch <= UPPER_Z) ||
|
|
99
99
|
(ch >= LOWER_A && ch <= LOWER_Z) ||
|
|
100
100
|
(ch === UNDERSCORE) ||
|
|
101
101
|
(ch === DASH) ||
|
|
102
|
-
(ch ===
|
|
102
|
+
(ch === DOLLAR_SIGN) ||
|
|
103
103
|
(ch === PERIOD);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* Read string until quote
|
|
108
108
|
*/
|
|
109
|
-
static readString(text: string, pos: number) {
|
|
109
|
+
static readString(text: string, pos: number): number {
|
|
110
110
|
const len = text.length;
|
|
111
111
|
const ch = text.charCodeAt(pos);
|
|
112
112
|
const q = ch;
|
|
@@ -130,10 +130,11 @@ export class QueryLanguageTokenizer {
|
|
|
130
130
|
*/
|
|
131
131
|
static tokenize(text: string): Token[] {
|
|
132
132
|
const state: TokenizeState = {
|
|
133
|
-
out: []
|
|
133
|
+
out: [],
|
|
134
134
|
pos: 0,
|
|
135
135
|
start: 0,
|
|
136
136
|
text,
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
137
138
|
mode: undefined! as TokenType
|
|
138
139
|
};
|
|
139
140
|
const len = text.length;
|
|
@@ -33,15 +33,14 @@ export interface Token {
|
|
|
33
33
|
/**
|
|
34
34
|
* Base AST Node
|
|
35
35
|
*/
|
|
36
|
-
export interface Node {
|
|
37
|
-
type:
|
|
36
|
+
export interface Node<T extends string = string> {
|
|
37
|
+
type: T;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Simple clause
|
|
42
42
|
*/
|
|
43
|
-
export interface ClauseNode extends Node {
|
|
44
|
-
type: 'clause';
|
|
43
|
+
export interface ClauseNode extends Node<'clause'> {
|
|
45
44
|
field?: string;
|
|
46
45
|
op?: string;
|
|
47
46
|
value?: Literal | Literal[];
|
|
@@ -50,30 +49,29 @@ export interface ClauseNode extends Node {
|
|
|
50
49
|
/**
|
|
51
50
|
* Grouping
|
|
52
51
|
*/
|
|
53
|
-
export interface GroupNode extends Node {
|
|
54
|
-
type: 'group';
|
|
52
|
+
export interface GroupNode extends Node<'group'> {
|
|
55
53
|
op?: 'and' | 'or';
|
|
56
|
-
value:
|
|
54
|
+
value: AllNode[];
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
/**
|
|
60
58
|
* Unary node
|
|
61
59
|
*/
|
|
62
|
-
export interface UnaryNode extends Node {
|
|
63
|
-
type: 'unary';
|
|
60
|
+
export interface UnaryNode extends Node<'unary'> {
|
|
64
61
|
op?: 'not';
|
|
65
|
-
value:
|
|
62
|
+
value: AllNode;
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
/**
|
|
69
66
|
* Array node
|
|
70
67
|
*/
|
|
71
|
-
export interface ArrayNode extends Node {
|
|
72
|
-
type: 'list';
|
|
68
|
+
export interface ArrayNode extends Node<'list'> {
|
|
73
69
|
op?: 'not';
|
|
74
70
|
value: Literal[];
|
|
75
71
|
}
|
|
76
72
|
|
|
73
|
+
export type AllNode = ArrayNode | UnaryNode | GroupNode | ClauseNode;
|
|
74
|
+
|
|
77
75
|
/**
|
|
78
76
|
* Translation of operators to model query keys
|
|
79
77
|
*/
|
|
@@ -51,7 +51,7 @@ class $QueryVerifier {
|
|
|
51
51
|
/**
|
|
52
52
|
* Handle generic clauses
|
|
53
53
|
*/
|
|
54
|
-
processGenericClause<T>(state: State, cls: Class<T>, val: object, handler: ProcessingHandler) {
|
|
54
|
+
processGenericClause<T>(state: State, cls: Class<T>, val: object, handler: ProcessingHandler): void {
|
|
55
55
|
const view = SchemaRegistry.getViewSchema(cls);
|
|
56
56
|
|
|
57
57
|
if (val === undefined || val === null) {
|
|
@@ -102,14 +102,14 @@ class $QueryVerifier {
|
|
|
102
102
|
/**
|
|
103
103
|
* Ensure types match
|
|
104
104
|
*/
|
|
105
|
-
typesMatch(declared: string, actual: string | undefined) {
|
|
105
|
+
typesMatch(declared: string, actual: string | undefined): boolean {
|
|
106
106
|
return declared === actual;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* Check operator clause
|
|
111
111
|
*/
|
|
112
|
-
checkOperatorClause(state: State, declaredType: SimpleType, value: unknown, allowed: Record<string, Set<string>>, isArray: boolean) {
|
|
112
|
+
checkOperatorClause(state: State, declaredType: SimpleType, value: unknown, allowed: Record<string, Set<string>>, isArray: boolean): void {
|
|
113
113
|
if (isArray) {
|
|
114
114
|
if (Array.isArray(value)) {
|
|
115
115
|
// Handle array literal
|
|
@@ -173,7 +173,7 @@ class $QueryVerifier {
|
|
|
173
173
|
/**
|
|
174
174
|
* Process where clause
|
|
175
175
|
*/
|
|
176
|
-
processWhereClause<T>(st: State, cls: Class<T>, passed: object) {
|
|
176
|
+
processWhereClause<T>(st: State, cls: Class<T>, passed: object): void {
|
|
177
177
|
return this.processGenericClause(st, cls, passed, {
|
|
178
178
|
preMember: (state: State, value: Record<string, unknown>) => {
|
|
179
179
|
const keys = Object.keys(value);
|
|
@@ -215,14 +215,14 @@ class $QueryVerifier {
|
|
|
215
215
|
/**
|
|
216
216
|
* Handle group by clause
|
|
217
217
|
*/
|
|
218
|
-
processGroupByClause(state: State, value: object) {
|
|
218
|
+
processGroupByClause(state: State, value: object): void {
|
|
219
219
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
223
|
* Handle sort clause
|
|
224
224
|
*/
|
|
225
|
-
processSortClause<T>(st: State, cls: Class<T>, passed: object) {
|
|
225
|
+
processSortClause<T>(st: State, cls: Class<T>, passed: object): void {
|
|
226
226
|
return this.processGenericClause(st, cls, passed, {
|
|
227
227
|
onSimpleType: (state, type, value) => {
|
|
228
228
|
if (value === 1 || value === -1 || typeof value === 'boolean') {
|
|
@@ -236,7 +236,7 @@ class $QueryVerifier {
|
|
|
236
236
|
/**
|
|
237
237
|
* Handle select clause
|
|
238
238
|
*/
|
|
239
|
-
processSelectClause<T>(st: State, cls: Class<T>, passed: object) {
|
|
239
|
+
processSelectClause<T>(st: State, cls: Class<T>, passed: object): void {
|
|
240
240
|
return this.processGenericClause(st, cls, passed, {
|
|
241
241
|
onSimpleType: (state, type, value) => {
|
|
242
242
|
const actual = TypeUtil.getActualType(value);
|
|
@@ -269,32 +269,35 @@ class $QueryVerifier {
|
|
|
269
269
|
/**
|
|
270
270
|
* Verify the query
|
|
271
271
|
*/
|
|
272
|
-
verify<T>(cls: Class<T>, query: ModelQuery<T> | Query<T> | PageableModelQuery<T>) {
|
|
272
|
+
verify<T>(cls: Class<T>, query: ModelQuery<T> | Query<T> | PageableModelQuery<T>): void {
|
|
273
273
|
const errors: ValidationError[] = [];
|
|
274
274
|
|
|
275
275
|
const state = {
|
|
276
276
|
path: '',
|
|
277
|
-
collect(path: string, message: string) {
|
|
277
|
+
collect(path: string, message: string): void {
|
|
278
278
|
errors.push({ message: `${path}: ${message}`, path, kind: 'model' });
|
|
279
279
|
},
|
|
280
|
-
log(err: string) {
|
|
280
|
+
log(err: string): void {
|
|
281
281
|
this.collect(this.path, err);
|
|
282
282
|
},
|
|
283
|
-
extend(sub: string) {
|
|
283
|
+
extend<S extends { path: string }>(this: S, sub: string): S {
|
|
284
284
|
return { ...this, path: !this.path ? sub : `${this.path}.${sub}` };
|
|
285
285
|
}
|
|
286
286
|
};
|
|
287
287
|
|
|
288
288
|
// Check all the clauses
|
|
289
289
|
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;
|
|
292
|
+
|
|
290
293
|
if (!(key in query)
|
|
291
|
-
|| query[
|
|
292
|
-
|| query[
|
|
294
|
+
|| query[queryKey] === undefined
|
|
295
|
+
|| query[queryKey] === null
|
|
293
296
|
) {
|
|
294
297
|
continue;
|
|
295
298
|
}
|
|
296
299
|
|
|
297
|
-
const val =
|
|
300
|
+
const val = query[queryKey];
|
|
298
301
|
const subState = state.extend(key);
|
|
299
302
|
|
|
300
303
|
if (Array.isArray(val)) {
|
|
@@ -302,6 +305,7 @@ class $QueryVerifier {
|
|
|
302
305
|
this[fn](subState, cls, el);
|
|
303
306
|
}
|
|
304
307
|
} else {
|
|
308
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
305
309
|
this[fn](subState, cls, val as object);
|
|
306
310
|
}
|
|
307
311
|
}
|
|
@@ -13,6 +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
|
|
16
17
|
return !!o && !!(o as Record<string, unknown>)['query'];
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ export function isQuerySupported(o: unknown): o is ModelQuerySupport {
|
|
|
21
22
|
* @param o
|
|
22
23
|
*/
|
|
23
24
|
export function isQueryCrudSupported(o: unknown): o is ModelQueryCrudSupport {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
24
26
|
return !!o && !!(o as Record<string, unknown>)['deleteByQuery'];
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -29,6 +31,7 @@ export function isQueryCrudSupported(o: unknown): o is ModelQueryCrudSupport {
|
|
|
29
31
|
* @param o
|
|
30
32
|
*/
|
|
31
33
|
export function isQueryFacetSupported(o: unknown): o is ModelQueryFacetSupport {
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
32
35
|
return !!o && !!(o as Record<string, unknown>)['facet'];
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -37,5 +40,6 @@ export function isQueryFacetSupported(o: unknown): o is ModelQueryFacetSupport {
|
|
|
37
40
|
* @param o
|
|
38
41
|
*/
|
|
39
42
|
export function isQuerySuggestSupported(o: unknown): o is ModelQuerySuggestSupport {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
40
44
|
return !!o && !!(o as Record<string, unknown>)['suggest'];
|
|
41
45
|
}
|
|
@@ -12,7 +12,7 @@ export class ModelQueryExpiryUtil {
|
|
|
12
12
|
/**
|
|
13
13
|
* Delete all expired
|
|
14
14
|
*/
|
|
15
|
-
static async deleteExpired<T extends ModelType>(svc: ModelQueryCrudSupport & ModelCrudSupport, cls: Class<T>) {
|
|
15
|
+
static async deleteExpired<T extends ModelType>(svc: ModelQueryCrudSupport & ModelCrudSupport, cls: Class<T>): Promise<number> {
|
|
16
16
|
const expiry = await ModelRegistry.getExpiry(cls);
|
|
17
17
|
const res = await svc.deleteByQuery<ModelType>(cls, {
|
|
18
18
|
where: {
|
|
@@ -18,8 +18,9 @@ export class ModelQueryUtil {
|
|
|
18
18
|
* @param val
|
|
19
19
|
* @returns
|
|
20
20
|
*/
|
|
21
|
-
static resolveComparator(val: unknown) {
|
|
21
|
+
static resolveComparator(val: unknown): unknown {
|
|
22
22
|
if (typeof val === 'string') {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
23
24
|
return Util.timeFromNow(val as '1m');
|
|
24
25
|
} else {
|
|
25
26
|
return val;
|
|
@@ -29,10 +30,10 @@ export class ModelQueryUtil {
|
|
|
29
30
|
/**
|
|
30
31
|
* Verify result set is singular, and decide if failing on many should happen
|
|
31
32
|
*/
|
|
32
|
-
static verifyGetSingleCounts<T>(cls: Class<T>, res?: T[], failOnMany = true) {
|
|
33
|
+
static verifyGetSingleCounts<T>(cls: Class<T>, res?: T[], failOnMany = true): T {
|
|
33
34
|
res = res ?? [];
|
|
34
35
|
if (res.length === 1 || res.length > 1 && !failOnMany) {
|
|
35
|
-
return res[0]
|
|
36
|
+
return res[0]!;
|
|
36
37
|
}
|
|
37
38
|
throw res.length === 0 ? new NotFoundError(cls, 'none') : new AppError(`Invalid number of results for find by id: ${res.length}`, 'data');
|
|
38
39
|
}
|
|
@@ -41,14 +42,16 @@ export class ModelQueryUtil {
|
|
|
41
42
|
* Get a where clause with type
|
|
42
43
|
*/
|
|
43
44
|
static getWhereClause<T extends ModelType>(cls: Class<T>, o: WhereClause<T> | string | undefined, checkExpiry = true): WhereClause<T> {
|
|
44
|
-
let q = o ? (typeof o === 'string' ? QueryLanguageParser.parseToQuery(o)
|
|
45
|
-
const clauses = (q ? [q] : [])
|
|
45
|
+
let q: WhereClause<T> | undefined = o ? (typeof o === 'string' ? QueryLanguageParser.parseToQuery(o) : o) : undefined;
|
|
46
|
+
const clauses: WhereClauseRaw<T>[] = (q ? [q] : []);
|
|
46
47
|
|
|
47
48
|
const conf = ModelRegistry.get(cls);
|
|
48
49
|
if (conf.subType) {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
49
51
|
clauses.push({ type: SchemaRegistry.getSubTypeName(cls) } as WhereClauseRaw<T>);
|
|
50
52
|
}
|
|
51
53
|
if (checkExpiry && conf.expiresAt) {
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
52
55
|
clauses.push({
|
|
53
56
|
$or: [
|
|
54
57
|
{ [conf.expiresAt]: { $exists: false } },
|
|
@@ -67,9 +70,12 @@ export class ModelQueryUtil {
|
|
|
67
70
|
/**
|
|
68
71
|
* Enrich query where clause, and verify query is correct
|
|
69
72
|
*/
|
|
70
|
-
static getQueryAndVerify<T extends ModelType, U extends Query<T> | ModelQuery<T>>(
|
|
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> } {
|
|
71
76
|
query.where = this.getWhereClause(cls, query.where, checkExpiry);
|
|
72
77
|
QueryVerifier.verify(cls, query);
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
73
79
|
return query as U & { where: WhereClause<T> };
|
|
74
80
|
}
|
|
75
81
|
}
|
|
@@ -13,14 +13,14 @@ export class ModelQuerySuggestUtil {
|
|
|
13
13
|
/**
|
|
14
14
|
* Build regex for suggesting
|
|
15
15
|
*/
|
|
16
|
-
static getSuggestRegex(prefix?: string) {
|
|
16
|
+
static getSuggestRegex(prefix?: string): RegExp {
|
|
17
17
|
return prefix ? new RegExp(`\\b${prefix}.*`, 'i') : /./;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Build suggest query on top of query language
|
|
22
22
|
*/
|
|
23
|
-
static getSuggestQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: Query<T>) {
|
|
23
|
+
static getSuggestQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: Query<T>): Query<T> {
|
|
24
24
|
const config = ModelRegistry.get(cls);
|
|
25
25
|
const limit = query?.limit ?? 10;
|
|
26
26
|
const clauses: WhereClauseRaw<ModelType>[] = prefix ? [{ [field]: { $regex: this.getSuggestRegex(prefix) } }] : [];
|
|
@@ -34,6 +34,7 @@ export class ModelQuerySuggestUtil {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
if (query?.where) {
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
37
38
|
clauses.push(query.where! as WhereClauseRaw<ModelType>);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -41,15 +42,17 @@ export class ModelQuerySuggestUtil {
|
|
|
41
42
|
where: clauses.length ? (clauses.length > 1 ? { $and: clauses } : clauses[0]) : {},
|
|
42
43
|
limit,
|
|
43
44
|
select: query?.select
|
|
44
|
-
}
|
|
45
|
+
};
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Join suggestion results
|
|
49
50
|
*/
|
|
50
51
|
static combineSuggestResults<T extends ModelType, U>(
|
|
51
|
-
cls: Class<T>,
|
|
52
|
-
|
|
52
|
+
cls: Class<T>,
|
|
53
|
+
field: ValidStringFields<T>,
|
|
54
|
+
prefix: string = '',
|
|
55
|
+
results: T[],
|
|
53
56
|
transform: (value: string, entity: T) => U,
|
|
54
57
|
limit?: number
|
|
55
58
|
): U[] {
|
|
@@ -59,7 +62,8 @@ export class ModelQuerySuggestUtil {
|
|
|
59
62
|
for (const r of results) {
|
|
60
63
|
const val = r[field];
|
|
61
64
|
if (Array.isArray(val)) {
|
|
62
|
-
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
66
|
+
out.push(...val.filter(f => pattern.test(f)).map((f: string) => [f, transform(f, r)] as [string, U]));
|
|
63
67
|
} else if (typeof val === 'string') {
|
|
64
68
|
out.push([val, transform(val, r)]);
|
|
65
69
|
}
|
|
@@ -74,11 +78,12 @@ export class ModelQuerySuggestUtil {
|
|
|
74
78
|
/**
|
|
75
79
|
* Build suggestion query
|
|
76
80
|
*/
|
|
77
|
-
static getSuggestFieldQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>) {
|
|
81
|
+
static getSuggestFieldQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Query<T> {
|
|
78
82
|
const config = ModelRegistry.get(cls);
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
79
84
|
return this.getSuggestQuery<ModelType>(cls, field as ValidStringFields<ModelType>, prefix, {
|
|
80
85
|
...(query ?? {}),
|
|
81
86
|
select: { [field]: true, ...(config.subType ? { type: true } : {}) }
|
|
82
|
-
})
|
|
87
|
+
}) as Query<T>;
|
|
83
88
|
}
|
|
84
89
|
}
|
|
@@ -3,14 +3,14 @@ import { Class } from '@travetto/base';
|
|
|
3
3
|
|
|
4
4
|
import { PointImpl } from '../model/point';
|
|
5
5
|
|
|
6
|
-
const st = (t: string | string[], arr: boolean = false) =>
|
|
6
|
+
const st = (t: string | string[], arr: boolean = false): Set<string> =>
|
|
7
7
|
new Set((Array.isArray(t) ? t : [t]).map(v => arr ? `${v}[]` : v));
|
|
8
8
|
|
|
9
|
-
const basic = (types: Set<string>) => ({ $ne: types, $eq: types, $exists: st('boolean') });
|
|
10
|
-
const scalar = (types: Set<string>) => ({ $in: types, $nin: types });
|
|
11
|
-
const str = () => ({ $regex: st(['RegExp', 'string']) });
|
|
12
|
-
const comp = (types: Set<string>) => ({ $lt: types, $lte: types, $gt: types, $gte: types });
|
|
13
|
-
const geo = (type: string) => ({
|
|
9
|
+
const basic = (types: Set<string>): Record<string, Set<string>> => ({ $ne: types, $eq: types, $exists: st('boolean') });
|
|
10
|
+
const scalar = (types: Set<string>): Record<string, Set<string>> => ({ $in: types, $nin: types });
|
|
11
|
+
const str = (): Record<string, Set<string>> => ({ $regex: st(['RegExp', 'string']) });
|
|
12
|
+
const comp = (types: Set<string>): Record<string, Set<string>> => ({ $lt: types, $lte: types, $gt: types, $gte: types });
|
|
13
|
+
const geo = (type: string): Record<string, Set<string>> => ({
|
|
14
14
|
$near: st(type),
|
|
15
15
|
$maxDistance: st('number'),
|
|
16
16
|
$unit: st('string'),
|
|
@@ -26,11 +26,11 @@ export class TypeUtil {
|
|
|
26
26
|
* Mapping types to various operators
|
|
27
27
|
*/
|
|
28
28
|
static OPERATORS = {
|
|
29
|
-
string: { ...basic(st('string')), ...scalar(st('string', true)), ...str() }
|
|
30
|
-
number: { ...basic(st('number')), ...scalar(st('number', true)), ...comp(st('number')) }
|
|
31
|
-
boolean: { ...basic(st('boolean')), ...scalar(st('boolean', true)) }
|
|
32
|
-
Date: { ...basic(st('Date')), ...scalar(st('Date', true)), ...comp(st(['string', 'Date'])) }
|
|
33
|
-
Point: { ...basic(st('Point')), ...geo('Point') }
|
|
29
|
+
string: { ...basic(st('string')), ...scalar(st('string', true)), ...str() },
|
|
30
|
+
number: { ...basic(st('number')), ...scalar(st('number', true)), ...comp(st('number')) },
|
|
31
|
+
boolean: { ...basic(st('boolean')), ...scalar(st('boolean', true)) },
|
|
32
|
+
Date: { ...basic(st('Date')), ...scalar(st('Date', true)), ...comp(st(['string', 'Date'])) },
|
|
33
|
+
Point: { ...basic(st('Point')), ...geo('Point') }
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -86,7 +86,7 @@ export type WhereClause<T> = WhereClauseRaw<RetainFields<T>>;
|
|
|
86
86
|
* Provides all the valid string type fields from a given type T
|
|
87
87
|
*/
|
|
88
88
|
export type ValidStringFields<T> = {
|
|
89
|
-
[K in keyof T]:
|
|
89
|
+
[K in Extract<keyof T, string>]:
|
|
90
90
|
(T[K] extends (String | string | string[] | String[] | undefined) ? K : never) // eslint-disable-line @typescript-eslint/ban-types
|
|
91
|
-
}[keyof T];
|
|
91
|
+
}[Extract<keyof T, string>];
|
|
92
92
|
|