@travetto/model-query-language 7.0.0-rc.1 → 7.0.0-rc.3
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/package.json +5 -5
- package/src/model-query.ts +2 -1
- package/src/parser.ts +45 -45
- package/src/tokenizer.ts +25 -26
- package/src/types.ts +7 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-query-language",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.3",
|
|
4
4
|
"description": "Datastore query language.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"datastore",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"directory": "module/model-query"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/model": "^7.0.0-rc.
|
|
30
|
-
"@travetto/model-query": "^7.0.0-rc.
|
|
31
|
-
"@travetto/schema": "^7.0.0-rc.
|
|
29
|
+
"@travetto/model": "^7.0.0-rc.3",
|
|
30
|
+
"@travetto/model-query": "^7.0.0-rc.3",
|
|
31
|
+
"@travetto/schema": "^7.0.0-rc.3"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/test": "^7.0.0-rc.
|
|
34
|
+
"@travetto/test": "^7.0.0-rc.3"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/test": {
|
package/src/model-query.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Schema } from '@travetto/schema';
|
|
2
2
|
import { PageableModelQuery } from '@travetto/model-query';
|
|
3
|
+
import { JSONUtil } from '@travetto/runtime';
|
|
3
4
|
|
|
4
5
|
import { QueryLanguageParser } from './parser.ts';
|
|
5
6
|
|
|
6
|
-
const parse = <T>(
|
|
7
|
+
const parse = <T>(key: string): T | undefined => !key || typeof key !== 'string' || !/^[\{\[]/.test(key) ? undefined : JSONUtil.parseSafe(key);
|
|
7
8
|
|
|
8
9
|
@Schema()
|
|
9
10
|
export class QueryLanguageModelQuery {
|
package/src/parser.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { castTo } from '@travetto/runtime';
|
|
|
2
2
|
import { WhereClauseRaw } from '@travetto/model-query';
|
|
3
3
|
|
|
4
4
|
import { QueryLanguageTokenizer } from './tokenizer.ts';
|
|
5
|
-
import { Token, Literal, GroupNode,
|
|
5
|
+
import { Token, Literal, GroupNode, OPERATOR_TRANSLATION, ArrayNode, AllNode } from './types.ts';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Determine if a token is boolean
|
|
9
9
|
*/
|
|
10
|
-
function isBoolean(
|
|
11
|
-
return !!
|
|
10
|
+
function isBoolean(value: unknown): value is Token & { type: 'boolean' } {
|
|
11
|
+
return !!value && typeof value === 'object' && 'type' in value && value.type === 'boolean';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -20,32 +20,32 @@ export class QueryLanguageParser {
|
|
|
20
20
|
* Handle all clauses
|
|
21
21
|
*/
|
|
22
22
|
static handleClause(nodes: (AllNode | Token)[]): void {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
23
|
+
const value: Token | ArrayNode = castTo(nodes.pop());
|
|
24
|
+
const operator: Token & { value: string } = castTo(nodes.pop());
|
|
25
|
+
const identifier: Token & { value: string } = castTo(nodes.pop());
|
|
26
26
|
|
|
27
27
|
// value isn't a literal or a list, bail
|
|
28
|
-
if (
|
|
29
|
-
throw new Error(`Unexpected token: ${
|
|
28
|
+
if (value.type !== 'literal' && value.type !== 'list') {
|
|
29
|
+
throw new Error(`Unexpected token: ${value.value}`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// If operator is not an operator, bail
|
|
33
|
-
if (
|
|
34
|
-
throw new Error(`Unexpected token: ${
|
|
33
|
+
if (operator.type !== 'operator') {
|
|
34
|
+
throw new Error(`Unexpected token: ${operator.value}`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// If operator is not known, bail
|
|
38
|
-
const
|
|
38
|
+
const finalOperation = OPERATOR_TRANSLATION[operator.value];
|
|
39
39
|
|
|
40
|
-
if (!
|
|
41
|
-
throw new Error(`Unexpected operator: ${
|
|
40
|
+
if (!finalOperation) {
|
|
41
|
+
throw new Error(`Unexpected operator: ${operator.value}`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
nodes.push({
|
|
45
45
|
type: 'clause',
|
|
46
|
-
field:
|
|
47
|
-
|
|
48
|
-
value:
|
|
46
|
+
field: identifier.value,
|
|
47
|
+
operator: finalOperation,
|
|
48
|
+
value: value.value
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
// Handle unary support
|
|
@@ -58,21 +58,21 @@ export class QueryLanguageParser {
|
|
|
58
58
|
* Condense nodes to remove unnecessary groupings
|
|
59
59
|
* (a AND (b AND (c AND d))) => (a AND b AND c)
|
|
60
60
|
*/
|
|
61
|
-
static condense(nodes: (AllNode | Token)[],
|
|
61
|
+
static condense(nodes: (AllNode | Token)[], operator: 'and' | 'or'): void {
|
|
62
62
|
let second = nodes[nodes.length - 2];
|
|
63
63
|
|
|
64
|
-
while (isBoolean(second) && second.value ===
|
|
64
|
+
while (isBoolean(second) && second.value === operator) {
|
|
65
65
|
const right: AllNode = castTo(nodes.pop());
|
|
66
66
|
nodes.pop()!;
|
|
67
67
|
const left: AllNode = castTo(nodes.pop());
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
nodes.push(
|
|
68
|
+
const rightGroup: GroupNode = castTo(right);
|
|
69
|
+
if (rightGroup.type === 'group' && rightGroup.operator === operator) {
|
|
70
|
+
rightGroup.value.unshift(left);
|
|
71
|
+
nodes.push(rightGroup);
|
|
72
72
|
} else {
|
|
73
73
|
nodes.push({
|
|
74
74
|
type: 'group',
|
|
75
|
-
|
|
75
|
+
operator,
|
|
76
76
|
value: [left, right]
|
|
77
77
|
});
|
|
78
78
|
}
|
|
@@ -91,7 +91,7 @@ export class QueryLanguageParser {
|
|
|
91
91
|
nodes.pop(); // This is second
|
|
92
92
|
nodes.push({
|
|
93
93
|
type: 'unary',
|
|
94
|
-
|
|
94
|
+
operator: 'not',
|
|
95
95
|
value: castTo<AllNode>(node)
|
|
96
96
|
});
|
|
97
97
|
}
|
|
@@ -100,13 +100,13 @@ export class QueryLanguageParser {
|
|
|
100
100
|
/**
|
|
101
101
|
* Parse all tokens
|
|
102
102
|
*/
|
|
103
|
-
static parse(tokens: Token[],
|
|
103
|
+
static parse(tokens: Token[], position: number = 0): AllNode {
|
|
104
104
|
|
|
105
105
|
let top: (AllNode | Token)[] = [];
|
|
106
106
|
const stack: (typeof top)[] = [top];
|
|
107
|
-
let
|
|
107
|
+
let list: Literal[] | undefined;
|
|
108
108
|
|
|
109
|
-
let token = tokens[
|
|
109
|
+
let token = tokens[position];
|
|
110
110
|
while (token) {
|
|
111
111
|
switch (token.type) {
|
|
112
112
|
case 'grouping':
|
|
@@ -123,31 +123,31 @@ export class QueryLanguageParser {
|
|
|
123
123
|
break;
|
|
124
124
|
case 'array':
|
|
125
125
|
if (token.value === 'start') {
|
|
126
|
-
|
|
126
|
+
list = [];
|
|
127
127
|
} else {
|
|
128
|
-
const arrNode: ArrayNode = { type: 'list', value:
|
|
128
|
+
const arrNode: ArrayNode = { type: 'list', value: list! };
|
|
129
129
|
top.push(arrNode);
|
|
130
|
-
|
|
130
|
+
list = undefined;
|
|
131
131
|
this.handleClause(top);
|
|
132
132
|
}
|
|
133
133
|
break;
|
|
134
134
|
case 'literal':
|
|
135
|
-
if (
|
|
136
|
-
|
|
135
|
+
if (list !== undefined) {
|
|
136
|
+
list.push(token.value);
|
|
137
137
|
} else {
|
|
138
138
|
top.push(token);
|
|
139
139
|
this.handleClause(top);
|
|
140
140
|
}
|
|
141
141
|
break;
|
|
142
142
|
case 'punctuation':
|
|
143
|
-
if (!
|
|
143
|
+
if (!list) {
|
|
144
144
|
throw new Error(`Invalid token: ${token.value}`);
|
|
145
145
|
}
|
|
146
146
|
break;
|
|
147
147
|
default:
|
|
148
148
|
top.push(token);
|
|
149
149
|
}
|
|
150
|
-
token = tokens[++
|
|
150
|
+
token = tokens[++position];
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
this.condense(top, 'or');
|
|
@@ -161,26 +161,26 @@ export class QueryLanguageParser {
|
|
|
161
161
|
static convert<T = unknown>(node: AllNode): WhereClauseRaw<T> {
|
|
162
162
|
switch (node.type) {
|
|
163
163
|
case 'unary': {
|
|
164
|
-
return castTo({ [`$${node.
|
|
164
|
+
return castTo({ [`$${node.operator!}`]: this.convert(node.value) });
|
|
165
165
|
}
|
|
166
166
|
case 'group': {
|
|
167
|
-
return castTo({ [`$${node.
|
|
167
|
+
return castTo({ [`$${node.operator!}`]: node.value.map(value => this.convert(value)) });
|
|
168
168
|
}
|
|
169
169
|
case 'clause': {
|
|
170
170
|
const parts = node.field!.split('.');
|
|
171
171
|
const top: WhereClauseRaw<T> = {};
|
|
172
172
|
let sub: Record<string, unknown> = top;
|
|
173
|
-
for (const
|
|
174
|
-
sub = sub[
|
|
173
|
+
for (const part of parts) {
|
|
174
|
+
sub = sub[part] = {};
|
|
175
175
|
}
|
|
176
|
-
if (node.
|
|
177
|
-
sub[node.
|
|
178
|
-
} else if ((node.
|
|
179
|
-
sub.$exists = node.
|
|
180
|
-
} else if ((node.
|
|
181
|
-
throw new Error(`Expected array literal for ${node.
|
|
176
|
+
if (node.operator === '$regex' && typeof node.value === 'string') {
|
|
177
|
+
sub[node.operator!] = new RegExp(`^${node.value}`);
|
|
178
|
+
} else if ((node.operator === '$eq' || node.operator === '$ne') && node.value === null) {
|
|
179
|
+
sub.$exists = node.operator !== '$eq';
|
|
180
|
+
} else if ((node.operator === '$in' || node.operator === '$nin') && !Array.isArray(node.value)) {
|
|
181
|
+
throw new Error(`Expected array literal for ${node.operator}`);
|
|
182
182
|
} else {
|
|
183
|
-
sub[node.
|
|
183
|
+
sub[node.operator!] = node.value;
|
|
184
184
|
}
|
|
185
185
|
return top;
|
|
186
186
|
}
|
package/src/tokenizer.ts
CHANGED
|
@@ -46,10 +46,10 @@ export class QueryLanguageTokenizer {
|
|
|
46
46
|
* Process the next token. Can specify expected type as needed
|
|
47
47
|
*/
|
|
48
48
|
static #processToken(state: TokenizeState, mode?: TokenType): Token {
|
|
49
|
-
const text = state.text.substring(state.start, state.
|
|
50
|
-
const
|
|
49
|
+
const text = state.text.substring(state.start, state.position);
|
|
50
|
+
const result = TOKEN_MAPPING[text.toLowerCase()];
|
|
51
51
|
let value: unknown = text;
|
|
52
|
-
if (!
|
|
52
|
+
if (!result && state.mode === 'literal') {
|
|
53
53
|
if (/^["']/.test(text)) {
|
|
54
54
|
value = text.substring(1, text.length - 1)
|
|
55
55
|
.replace(/\\[.]/g, (a, b) => ESCAPE[a] || b);
|
|
@@ -67,18 +67,18 @@ export class QueryLanguageTokenizer {
|
|
|
67
67
|
state.mode = 'identifier';
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
return
|
|
70
|
+
return result ?? { value, type: state.mode || mode };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Flush state to output
|
|
75
75
|
*/
|
|
76
76
|
static #flush(state: TokenizeState, mode?: TokenType): void {
|
|
77
|
-
if ((!mode || !state.mode || mode !== state.mode) && state.start !== state.
|
|
77
|
+
if ((!mode || !state.mode || mode !== state.mode) && state.start !== state.position) {
|
|
78
78
|
if (state.mode !== 'whitespace') {
|
|
79
79
|
state.out.push(this.#processToken(state, mode));
|
|
80
80
|
}
|
|
81
|
-
state.start = state.
|
|
81
|
+
state.start = state.position;
|
|
82
82
|
}
|
|
83
83
|
state.mode = mode || state.mode;
|
|
84
84
|
}
|
|
@@ -106,23 +106,22 @@ export class QueryLanguageTokenizer {
|
|
|
106
106
|
/**
|
|
107
107
|
* Read string until quote
|
|
108
108
|
*/
|
|
109
|
-
static readString(text: string,
|
|
110
|
-
const
|
|
111
|
-
const ch = text.charCodeAt(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (text.charCodeAt(pos) === q) {
|
|
109
|
+
static readString(text: string, position: number): number {
|
|
110
|
+
const length = text.length;
|
|
111
|
+
const ch = text.charCodeAt(position);
|
|
112
|
+
position += 1;
|
|
113
|
+
while (position < length) {
|
|
114
|
+
if (text.charCodeAt(position) === ch) {
|
|
116
115
|
break;
|
|
117
|
-
} else if (text.charCodeAt(
|
|
118
|
-
|
|
116
|
+
} else if (text.charCodeAt(position) === BACKSLASH) {
|
|
117
|
+
position += 1;
|
|
119
118
|
}
|
|
120
|
-
|
|
119
|
+
position += 1;
|
|
121
120
|
}
|
|
122
|
-
if (
|
|
121
|
+
if (position === length && text.charCodeAt(position) !== ch) {
|
|
123
122
|
throw new Error('Unterminated string literal');
|
|
124
123
|
}
|
|
125
|
-
return
|
|
124
|
+
return position;
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
/**
|
|
@@ -131,16 +130,16 @@ export class QueryLanguageTokenizer {
|
|
|
131
130
|
static tokenize(text: string): Token[] {
|
|
132
131
|
const state: TokenizeState = {
|
|
133
132
|
out: [],
|
|
134
|
-
|
|
133
|
+
position: 0,
|
|
135
134
|
start: 0,
|
|
136
135
|
text,
|
|
137
136
|
mode: undefined!
|
|
138
137
|
};
|
|
139
138
|
const len = text.length;
|
|
140
139
|
// Loop through each char
|
|
141
|
-
while (state.
|
|
140
|
+
while (state.position < len) {
|
|
142
141
|
// Read code as a number, more efficient
|
|
143
|
-
const ch = text.charCodeAt(state.
|
|
142
|
+
const ch = text.charCodeAt(state.position);
|
|
144
143
|
switch (ch) {
|
|
145
144
|
// Handle punctuation
|
|
146
145
|
case OPEN_PARENS: case CLOSE_PARENS: case OPEN_BRACKET: case CLOSE_BRACKET: case COMMA:
|
|
@@ -160,10 +159,10 @@ export class QueryLanguageTokenizer {
|
|
|
160
159
|
case DBL_QUOTE: case SGL_QUOTE: case FORWARD_SLASH:
|
|
161
160
|
this.#flush(state);
|
|
162
161
|
state.mode = 'literal';
|
|
163
|
-
state.
|
|
162
|
+
state.position = this.readString(text, state.position) + 1;
|
|
164
163
|
if (ch === FORWARD_SLASH) { // Read modifiers, not used by all, but useful in general
|
|
165
|
-
while (this.#isValidRegexFlag(text.charCodeAt(state.
|
|
166
|
-
state.
|
|
164
|
+
while (this.#isValidRegexFlag(text.charCodeAt(state.position))) {
|
|
165
|
+
state.position += 1;
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
168
|
this.#flush(state);
|
|
@@ -173,10 +172,10 @@ export class QueryLanguageTokenizer {
|
|
|
173
172
|
if (this.#isValidIdentToken(ch)) {
|
|
174
173
|
this.#flush(state, 'literal');
|
|
175
174
|
} else {
|
|
176
|
-
throw new Error(`Invalid character: ${text.substring(Math.max(0, state.
|
|
175
|
+
throw new Error(`Invalid character: ${text.substring(Math.max(0, state.position - 10), state.position + 1)}`);
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
|
-
state.
|
|
178
|
+
state.position += 1;
|
|
180
179
|
}
|
|
181
180
|
|
|
182
181
|
this.#flush(state);
|
package/src/types.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type TokenType =
|
|
|
11
11
|
*/
|
|
12
12
|
export interface TokenizeState {
|
|
13
13
|
out: Token[];
|
|
14
|
-
|
|
14
|
+
position: number;
|
|
15
15
|
start: number;
|
|
16
16
|
text: string;
|
|
17
17
|
mode: TokenType;
|
|
@@ -42,7 +42,7 @@ export interface Node<T extends string = string> {
|
|
|
42
42
|
*/
|
|
43
43
|
export interface ClauseNode extends Node<'clause'> {
|
|
44
44
|
field?: string;
|
|
45
|
-
|
|
45
|
+
operator?: string;
|
|
46
46
|
value?: Literal | Literal[];
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -50,7 +50,7 @@ export interface ClauseNode extends Node<'clause'> {
|
|
|
50
50
|
* Grouping
|
|
51
51
|
*/
|
|
52
52
|
export interface GroupNode extends Node<'group'> {
|
|
53
|
-
|
|
53
|
+
operator?: 'and' | 'or';
|
|
54
54
|
value: AllNode[];
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -58,7 +58,7 @@ export interface GroupNode extends Node<'group'> {
|
|
|
58
58
|
* Unary node
|
|
59
59
|
*/
|
|
60
60
|
export interface UnaryNode extends Node<'unary'> {
|
|
61
|
-
|
|
61
|
+
operator?: 'not';
|
|
62
62
|
value: AllNode;
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -66,7 +66,7 @@ export interface UnaryNode extends Node<'unary'> {
|
|
|
66
66
|
* Array node
|
|
67
67
|
*/
|
|
68
68
|
export interface ArrayNode extends Node<'list'> {
|
|
69
|
-
|
|
69
|
+
operator?: 'not';
|
|
70
70
|
value: Literal[];
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -75,7 +75,7 @@ export type AllNode = ArrayNode | UnaryNode | GroupNode | ClauseNode;
|
|
|
75
75
|
/**
|
|
76
76
|
* Translation of operators to model query keys
|
|
77
77
|
*/
|
|
78
|
-
export const
|
|
78
|
+
export const OPERATOR_TRANSLATION: Record<string, string> = {
|
|
79
79
|
'<': '$lt', '<=': '$lte',
|
|
80
80
|
'>': '$gt', '>=': '$gte',
|
|
81
81
|
'!=': '$ne', '==': '$eq',
|
|
@@ -83,4 +83,4 @@ export const OP_TRANSLATION: Record<string, string> = {
|
|
|
83
83
|
in: '$in', 'not-in': '$nin'
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
export const
|
|
86
|
+
export const VALID_OPERATORS = new Set(Object.keys(OPERATOR_TRANSLATION));
|