wizard-ql 1.0.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/LICENSE +201 -0
- package/README.md +412 -0
- package/dist/dominput.d.ts +16 -0
- package/dist/errors.d.ts +51 -0
- package/dist/execute.d.ts +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1083 -0
- package/dist/parse.d.ts +51 -0
- package/dist/regex.d.ts +11 -0
- package/dist/spec.d.ts +169 -0
- package/dist/stringify.d.ts +42 -0
- package/dist/summarize.d.ts +15 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
// src/spec.ts
|
|
2
|
+
var OPERATION_ALIAS_DICTIONARY = {
|
|
3
|
+
AND: "AND",
|
|
4
|
+
"&&": "AND",
|
|
5
|
+
"&": "AND",
|
|
6
|
+
"^": "AND",
|
|
7
|
+
OR: "OR",
|
|
8
|
+
"||": "OR",
|
|
9
|
+
"|": "OR",
|
|
10
|
+
V: "OR",
|
|
11
|
+
GEQ: "GEQ",
|
|
12
|
+
">=": "GEQ",
|
|
13
|
+
"=>": "GEQ",
|
|
14
|
+
LEQ: "LEQ",
|
|
15
|
+
"<=": "LEQ",
|
|
16
|
+
"=<": "LEQ",
|
|
17
|
+
NOTEQUALS: "NOTEQUAL",
|
|
18
|
+
NOTEQUAL: "NOTEQUAL",
|
|
19
|
+
NEQ: "NOTEQUAL",
|
|
20
|
+
ISNT: "NOTEQUAL",
|
|
21
|
+
"!==": "NOTEQUAL",
|
|
22
|
+
"!=": "NOTEQUAL",
|
|
23
|
+
EQUALS: "EQUAL",
|
|
24
|
+
EQUAL: "EQUAL",
|
|
25
|
+
EQ: "EQUAL",
|
|
26
|
+
IS: "EQUAL",
|
|
27
|
+
"==": "EQUAL",
|
|
28
|
+
"=": "EQUAL",
|
|
29
|
+
LESS: "LESS",
|
|
30
|
+
"<": "LESS",
|
|
31
|
+
GREATER: "GREATER",
|
|
32
|
+
">": "GREATER",
|
|
33
|
+
MORE: "GREATER",
|
|
34
|
+
NOTIN: "NOTIN",
|
|
35
|
+
"!:": "NOTIN",
|
|
36
|
+
IN: "IN",
|
|
37
|
+
":": "IN",
|
|
38
|
+
NOTMATCHES: "NOTMATCH",
|
|
39
|
+
NOTMATCH: "NOTMATCH",
|
|
40
|
+
"!~": "NOTMATCH",
|
|
41
|
+
MATCHES: "MATCH",
|
|
42
|
+
MATCH: "MATCH",
|
|
43
|
+
"~": "MATCH"
|
|
44
|
+
};
|
|
45
|
+
var ALIASES = Object.keys(OPERATION_ALIAS_DICTIONARY);
|
|
46
|
+
var OPERATION_PURPOSE_DICTIONARY = {
|
|
47
|
+
AND: "junction",
|
|
48
|
+
OR: "junction",
|
|
49
|
+
EQUAL: "comparison",
|
|
50
|
+
NOTEQUAL: "comparison",
|
|
51
|
+
LESS: "comparison",
|
|
52
|
+
GREATER: "comparison",
|
|
53
|
+
GEQ: "comparison",
|
|
54
|
+
LEQ: "comparison",
|
|
55
|
+
IN: "comparison",
|
|
56
|
+
NOTIN: "comparison",
|
|
57
|
+
MATCH: "comparison",
|
|
58
|
+
NOTMATCH: "comparison"
|
|
59
|
+
};
|
|
60
|
+
var COMPARISON_TYPE_DICTIONARY = {
|
|
61
|
+
EQUAL: "primitive",
|
|
62
|
+
NOTEQUAL: "primitive",
|
|
63
|
+
GEQ: "number",
|
|
64
|
+
GREATER: "number",
|
|
65
|
+
LEQ: "number",
|
|
66
|
+
LESS: "number",
|
|
67
|
+
IN: "array",
|
|
68
|
+
NOTIN: "array",
|
|
69
|
+
MATCH: "string",
|
|
70
|
+
NOTMATCH: "string"
|
|
71
|
+
};
|
|
72
|
+
// src/regex.ts
|
|
73
|
+
var ESCAPE_REGEX = "(?<=(?<!\\\\)(?:\\\\\\\\)*)";
|
|
74
|
+
var QUOTES = ["'", '"', "`"];
|
|
75
|
+
var QUOTE_REGEX_STR = `${ESCAPE_REGEX}(?<quote>${QUOTES.map((q) => RegExp.escape(q)).join("|")})(?<quotecontent>.*?)${ESCAPE_REGEX}\\k<quote>`;
|
|
76
|
+
|
|
77
|
+
// src/errors.ts
|
|
78
|
+
function constructHeader(startToken, startIndex, endToken, endIndex) {
|
|
79
|
+
let header = "Token #";
|
|
80
|
+
header += startIndex === undefined ? "??" : startIndex;
|
|
81
|
+
if (endIndex !== undefined && endIndex !== startIndex)
|
|
82
|
+
header += " -> #" + endIndex;
|
|
83
|
+
if (startToken) {
|
|
84
|
+
header += " (char ";
|
|
85
|
+
header += startToken.index;
|
|
86
|
+
if (endToken && endToken !== startToken) {
|
|
87
|
+
header += " -> " + endToken.index;
|
|
88
|
+
}
|
|
89
|
+
header += ' "';
|
|
90
|
+
header += startToken.content.replaceAll('"', "\\\"");
|
|
91
|
+
if (endToken && endToken !== startToken) {
|
|
92
|
+
header += '" -> "' + endToken.content.replaceAll('"', "\\\"");
|
|
93
|
+
}
|
|
94
|
+
header += '"';
|
|
95
|
+
header += ")";
|
|
96
|
+
}
|
|
97
|
+
header += ": ";
|
|
98
|
+
return header;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class ParseError extends Error {
|
|
102
|
+
name = "ParseError";
|
|
103
|
+
rawMessage;
|
|
104
|
+
startToken;
|
|
105
|
+
endToken;
|
|
106
|
+
startIndex;
|
|
107
|
+
endIndex;
|
|
108
|
+
constructor(message, startToken, startIndex, endToken = startToken, endIndex = startIndex) {
|
|
109
|
+
const header = constructHeader(startToken, startIndex, endToken, endIndex);
|
|
110
|
+
super(header + message);
|
|
111
|
+
this.rawMessage = message;
|
|
112
|
+
this.startToken = startToken;
|
|
113
|
+
this.endToken = endToken;
|
|
114
|
+
this.startIndex = startIndex;
|
|
115
|
+
this.endIndex = endIndex;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class ConstraintError extends Error {
|
|
120
|
+
name = "ConstraintError";
|
|
121
|
+
rawMessage;
|
|
122
|
+
startToken;
|
|
123
|
+
endToken;
|
|
124
|
+
startIndex;
|
|
125
|
+
endIndex;
|
|
126
|
+
constructor(message, startToken, startIndex, endToken = startToken, endIndex = startIndex) {
|
|
127
|
+
const header = constructHeader(startToken, startIndex, endToken, endIndex);
|
|
128
|
+
super(header + message);
|
|
129
|
+
this.rawMessage = message;
|
|
130
|
+
this.startToken = startToken;
|
|
131
|
+
this.endToken = endToken;
|
|
132
|
+
this.startIndex = startIndex;
|
|
133
|
+
this.endIndex = endIndex;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/parse.ts
|
|
138
|
+
var TOKEN_REGEX = new RegExp("(?<=(?<!\\\\)(?:\\\\\\\\)*)(?<quote>\\x27|\\x22|\\x60)(?<quotecontent>.*?)(?<=(?<!\\\\)(?:\\\\\\\\)*)\\k<quote>|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x41ND(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x26\\x26|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x26|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\^|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4fR(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\|\\||(?<=(?<!\\\\)(?:\\\\\\\\)*)\\||(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x56(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x47EQ(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3e\\x3d|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3d\\x3e|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4cEQ(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3c\\x3d|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3d\\x3c|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTEQUALS(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTEQUAL(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eEQ(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x49SNT(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21\\x3d\\x3d|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21\\x3d|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x45QUALS(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x45QUAL(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x45Q(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x49S(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3d\\x3d|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3d|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4cESS(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3c|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x47REATER(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3e|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4dORE(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTIN(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21\\x3a|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x49N(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x3a|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTMATCHES(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4eOTMATCH(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21\\x7e|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4dATCHES(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|^)\\x4dATCH(?=(?<=(?<!\\\\)(?:\\\\\\\\)*)\\s|$)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x7e|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\(|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\)|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\[|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\]|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\{|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\}|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x2c|(?<=(?<!\\\\)(?:\\\\\\\\)*)\\x21", "g");
|
|
139
|
+
var QUOTE_REGEX = new RegExp("(?<=(?<!\\\\)(?:\\\\\\\\)*)(?<quote>\\x27|\\x22|\\x60)(?<quotecontent>.*?)(?<=(?<!\\\\)(?:\\\\\\\\)*)\\k<quote>");
|
|
140
|
+
var QUOTE_EDGE_REGEX = new RegExp(`^${"(?<=(?<!\\\\)(?:\\\\\\\\)*)(?<quote>\\x27|\\x22|\\x60)(?<quotecontent>.*?)(?<=(?<!\\\\)(?:\\\\\\\\)*)\\k<quote>"}$`);
|
|
141
|
+
function pushSanitized(array, item, index) {
|
|
142
|
+
let trimmed = item.trimEnd();
|
|
143
|
+
const pretrimLength = trimmed.length;
|
|
144
|
+
trimmed = trimmed.trimStart();
|
|
145
|
+
const lengthDiff = trimmed.length - pretrimLength;
|
|
146
|
+
if (trimmed) {
|
|
147
|
+
const token = { content: trimmed, index: index - lengthDiff };
|
|
148
|
+
array.push(token);
|
|
149
|
+
return token;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function _tokenize(expression, pattern) {
|
|
153
|
+
const tokens = [];
|
|
154
|
+
const matches = expression.toUpperCase().matchAll(pattern);
|
|
155
|
+
let lastMatchEnd = null;
|
|
156
|
+
for (const match of matches) {
|
|
157
|
+
pushSanitized(tokens, expression.slice(lastMatchEnd ?? 0, match.index), lastMatchEnd === null ? 0 : lastMatchEnd);
|
|
158
|
+
if (["[", "{"].includes(match[0])) {
|
|
159
|
+
const startToken = {
|
|
160
|
+
content: match[0],
|
|
161
|
+
index: match.index
|
|
162
|
+
};
|
|
163
|
+
tokens.push(startToken);
|
|
164
|
+
let endToken;
|
|
165
|
+
for (const submatch of matches) {
|
|
166
|
+
if (match[0] === "[" && submatch[0] === "]" || match[0] === "{" && submatch[0] === "}") {
|
|
167
|
+
endToken = {
|
|
168
|
+
content: submatch[0],
|
|
169
|
+
index: submatch.index
|
|
170
|
+
};
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const subtokens = endToken ? _tokenize(expression.slice(startToken.index + startToken.content.length, endToken.index), new RegExp(`${"(?<=(?<!\\\\)(?:\\\\\\\\)*)(?<quote>\\x27|\\x22|\\x60)(?<quotecontent>.*?)(?<=(?<!\\\\)(?:\\\\\\\\)*)\\k<quote>"}|${ESCAPE_REGEX},`, "g")) : _tokenize(expression.slice(startToken.index + startToken.content.length), TOKEN_REGEX);
|
|
175
|
+
for (const subtoken of subtokens)
|
|
176
|
+
subtoken.index += match.index + 1;
|
|
177
|
+
tokens.push(...subtokens);
|
|
178
|
+
if (endToken) {
|
|
179
|
+
tokens.push(endToken);
|
|
180
|
+
lastMatchEnd = endToken.index + endToken.content.length;
|
|
181
|
+
} else {
|
|
182
|
+
lastMatchEnd = expression.length;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
} else {
|
|
187
|
+
pushSanitized(tokens, match.groups?.quotecontent !== undefined ? expression.slice(match.index, match.index + match[0].length) : match[0], match.index);
|
|
188
|
+
}
|
|
189
|
+
lastMatchEnd = match.index + match[0].length;
|
|
190
|
+
}
|
|
191
|
+
pushSanitized(tokens, expression.slice(lastMatchEnd ?? 0), lastMatchEnd ?? 0);
|
|
192
|
+
return tokens;
|
|
193
|
+
}
|
|
194
|
+
function tokenize(expression) {
|
|
195
|
+
return _tokenize(expression, TOKEN_REGEX);
|
|
196
|
+
}
|
|
197
|
+
function parseValue(token, isQuoted) {
|
|
198
|
+
const number = Number(token);
|
|
199
|
+
return isQuoted || isNaN(number) ? token === "true" ? true : token === "false" ? false : token : number;
|
|
200
|
+
}
|
|
201
|
+
function processToken(token) {
|
|
202
|
+
const unquoted = token.match(QUOTE_EDGE_REGEX)?.groups?.quotecontent;
|
|
203
|
+
const escaped = unquoted ?? token;
|
|
204
|
+
const unescaped = escaped.replaceAll(new RegExp(`(?<!${ESCAPE_REGEX}\\\\)\\\\`, "g"), "");
|
|
205
|
+
return {
|
|
206
|
+
unquoted,
|
|
207
|
+
escaped,
|
|
208
|
+
unescaped
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function getClosingIndex(tokens, start, opening, closing) {
|
|
212
|
+
let openingCount = 1;
|
|
213
|
+
for (let index = start + 1;index < tokens.length; ++index) {
|
|
214
|
+
switch (tokens[index].content) {
|
|
215
|
+
case opening:
|
|
216
|
+
++openingCount;
|
|
217
|
+
break;
|
|
218
|
+
case closing:
|
|
219
|
+
--openingCount;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
if (openingCount === 0)
|
|
223
|
+
return index;
|
|
224
|
+
}
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
function complementExpression(expression) {
|
|
228
|
+
switch (expression.type) {
|
|
229
|
+
case "group":
|
|
230
|
+
switch (expression.operation) {
|
|
231
|
+
case "AND":
|
|
232
|
+
expression.operation = "OR";
|
|
233
|
+
break;
|
|
234
|
+
case "OR":
|
|
235
|
+
expression.operation = "AND";
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
expression.constituents.forEach(complementExpression);
|
|
239
|
+
break;
|
|
240
|
+
case "condition":
|
|
241
|
+
switch (expression.operation) {
|
|
242
|
+
case "EQUAL":
|
|
243
|
+
expression.operation = "NOTEQUAL";
|
|
244
|
+
break;
|
|
245
|
+
case "NOTEQUAL":
|
|
246
|
+
expression.operation = "EQUAL";
|
|
247
|
+
break;
|
|
248
|
+
case "GEQ":
|
|
249
|
+
expression.operation = "LESS";
|
|
250
|
+
break;
|
|
251
|
+
case "LESS":
|
|
252
|
+
expression.operation = "GEQ";
|
|
253
|
+
break;
|
|
254
|
+
case "LEQ":
|
|
255
|
+
expression.operation = "GREATER";
|
|
256
|
+
break;
|
|
257
|
+
case "GREATER":
|
|
258
|
+
expression.operation = "LEQ";
|
|
259
|
+
break;
|
|
260
|
+
case "IN":
|
|
261
|
+
expression.operation = "NOTIN";
|
|
262
|
+
break;
|
|
263
|
+
case "NOTIN":
|
|
264
|
+
expression.operation = "IN";
|
|
265
|
+
break;
|
|
266
|
+
case "MATCH":
|
|
267
|
+
expression.operation = "NOTMATCH";
|
|
268
|
+
break;
|
|
269
|
+
case "NOTMATCH":
|
|
270
|
+
expression.operation = "MATCH";
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function validateCondition(condition, constraints, valueIsImplicit, ctx) {
|
|
277
|
+
let validated = false;
|
|
278
|
+
const field = constraints?.caseInsensitive ? [...Object.keys(constraints.types ?? {}), ...Object.keys(constraints.restricted ?? {})].find((k) => k.toLowerCase() === condition.field.toLowerCase()) ?? condition.field : condition.field;
|
|
279
|
+
const restriction = constraints?.restricted?.[field];
|
|
280
|
+
const type = constraints?.types?.[field];
|
|
281
|
+
if (constraints?.disallowUnvalidated && restriction === undefined && type === undefined)
|
|
282
|
+
throw new ConstraintError(`Unknown field "${condition.field}"`, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
283
|
+
const operationType = COMPARISON_TYPE_DICTIONARY[condition.operation];
|
|
284
|
+
const types = type && (Array.isArray(type) ? type : [type]);
|
|
285
|
+
if (!valueIsImplicit && constraints?.interpretDates && (!types || types.includes("number")) && ["primitive", "number"].includes(operationType)) {
|
|
286
|
+
const validator = constraints.interpretDates === true ? (v) => +new Date(v) : constraints.interpretDates;
|
|
287
|
+
if (Array.isArray(condition.value)) {
|
|
288
|
+
for (let v = 0;v < condition.value.length; ++v) {
|
|
289
|
+
const val = condition.value[v];
|
|
290
|
+
if (typeof val === "string") {
|
|
291
|
+
const numberized = validator(val);
|
|
292
|
+
if (!isNaN(numberized))
|
|
293
|
+
condition.value[v] = numberized;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
const val = condition.value;
|
|
298
|
+
if (typeof val === "string") {
|
|
299
|
+
const numberized = validator(val);
|
|
300
|
+
if (!isNaN(numberized))
|
|
301
|
+
condition.value = numberized;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const values = Array.isArray(condition.value) ? condition.value : [condition.value];
|
|
306
|
+
if (restriction === true)
|
|
307
|
+
throw new ConstraintError(`Field "${condition.field}" is restricted`, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
308
|
+
else if (Array.isArray(restriction)) {
|
|
309
|
+
const [philosophy, checks] = restriction;
|
|
310
|
+
if (philosophy === "deny") {
|
|
311
|
+
for (const check of checks) {
|
|
312
|
+
if (check instanceof RegExp) {
|
|
313
|
+
if (values.some((v) => check.test(v.toString()))) {
|
|
314
|
+
throw new ConstraintError(`Value for field "${condition.field}" violates prohibitive pattern constraint "${check.toString()}". Prohibited values/patterns: Allowed values/patterns: ${checks.join(", ")}`, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
if (values.includes(check)) {
|
|
318
|
+
throw new ConstraintError(`Forbidden value "${check}" for field "${condition.field}". Prohibited values/patterns: ${checks.join(", ")}`, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
for (const value of values) {
|
|
324
|
+
if (!checks.some((c) => c instanceof RegExp && c.test(value.toString()) || c === value)) {
|
|
325
|
+
throw new ConstraintError(`Value for field "${condition.field}" does not meet any allowed value/pattern. Allowed values/patterns: ${checks.join(", ")}`, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
validated = true;
|
|
330
|
+
}
|
|
331
|
+
if (!valueIsImplicit && operationType === "string" && (typeof condition.value === "number" || typeof condition.value === "boolean"))
|
|
332
|
+
condition.value = condition.value.toString();
|
|
333
|
+
let operationAllowed;
|
|
334
|
+
switch (operationType) {
|
|
335
|
+
case "primitive":
|
|
336
|
+
operationAllowed = !Array.isArray(condition.value);
|
|
337
|
+
break;
|
|
338
|
+
case "string":
|
|
339
|
+
operationAllowed = typeof condition.value === "string";
|
|
340
|
+
break;
|
|
341
|
+
case "number":
|
|
342
|
+
operationAllowed = typeof condition.value === "number";
|
|
343
|
+
break;
|
|
344
|
+
case "array":
|
|
345
|
+
operationAllowed = Array.isArray(condition.value);
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (!operationAllowed)
|
|
349
|
+
throw new ConstraintError(`Value "${condition.value.toString()}" not allowed for operation "${condition.operation}" which only allows for "${operationType}" type`, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
350
|
+
if (types) {
|
|
351
|
+
const meets = values.every((v, i) => {
|
|
352
|
+
if (!valueIsImplicit && (typeof v === "number" && !types.includes("number") || typeof v === "boolean" && !types.includes("boolean")) && types.includes("string")) {
|
|
353
|
+
v = v.toString();
|
|
354
|
+
values[i] = v;
|
|
355
|
+
if (!Array.isArray(condition.value))
|
|
356
|
+
condition.value = v;
|
|
357
|
+
}
|
|
358
|
+
return types.some((t) => {
|
|
359
|
+
switch (t) {
|
|
360
|
+
case "boolean":
|
|
361
|
+
return typeof v === "boolean";
|
|
362
|
+
case "number":
|
|
363
|
+
return typeof v === "number";
|
|
364
|
+
case "string":
|
|
365
|
+
return typeof v === "string";
|
|
366
|
+
default:
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
if (!meets)
|
|
372
|
+
throw new ConstraintError(`Value "${condition.value.toString()}" includes a type not permitted for field "${condition.field}". Allowed types: ${types.join(", ")}`, ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
373
|
+
validated = true;
|
|
374
|
+
}
|
|
375
|
+
const edit = condition;
|
|
376
|
+
edit.field = field;
|
|
377
|
+
edit.validated = validated;
|
|
378
|
+
return edit;
|
|
379
|
+
}
|
|
380
|
+
function _parse(tokens, _offset, constraints) {
|
|
381
|
+
let field;
|
|
382
|
+
let comparisonOperation;
|
|
383
|
+
let value;
|
|
384
|
+
let inConjunction = false;
|
|
385
|
+
let groupOperation;
|
|
386
|
+
let expectingExpression = true;
|
|
387
|
+
const expressions = [];
|
|
388
|
+
function getExpressionGroup(ctx) {
|
|
389
|
+
if (inConjunction) {
|
|
390
|
+
const prior = expressions.at(-1);
|
|
391
|
+
if (!prior)
|
|
392
|
+
throw new ParseError("Unexpected: Expression list empty when parser is meant to append to an AND group", ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
393
|
+
if (prior.type !== "group" || prior.operation !== "AND")
|
|
394
|
+
throw new ParseError("Unexpected: Last expression is not an AND group yet parser thinks it's appending to one", ctx?.startToken, ctx?.startIndex, ctx?.endToken, ctx?.endIndex);
|
|
395
|
+
return prior.constituents;
|
|
396
|
+
}
|
|
397
|
+
return expressions;
|
|
398
|
+
}
|
|
399
|
+
function resolveCondition(ctx) {
|
|
400
|
+
const baseCtx = {
|
|
401
|
+
startToken: field?.token ?? comparisonOperation?.token ?? value?.token,
|
|
402
|
+
startIndex: field?.index ?? comparisonOperation?.index ?? value?.index,
|
|
403
|
+
endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
|
|
404
|
+
endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
|
|
405
|
+
};
|
|
406
|
+
if (!ctx)
|
|
407
|
+
ctx = baseCtx;
|
|
408
|
+
const group = getExpressionGroup(ctx);
|
|
409
|
+
if (field && comparisonOperation && value) {
|
|
410
|
+
if (!expectingExpression)
|
|
411
|
+
throw new ParseError("Unexpected expression resolution before junctive operator", ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
|
|
412
|
+
group.push(validateCondition({
|
|
413
|
+
type: "condition",
|
|
414
|
+
field: field.content,
|
|
415
|
+
operation: comparisonOperation.content,
|
|
416
|
+
value: value.content
|
|
417
|
+
}, constraints, value.implicit, baseCtx));
|
|
418
|
+
inConjunction = false;
|
|
419
|
+
expectingExpression = false;
|
|
420
|
+
} else if (field && !comparisonOperation && !value) {
|
|
421
|
+
if (!expectingExpression)
|
|
422
|
+
throw new ParseError("Unexpected expression resolution before junctive operator", ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
|
|
423
|
+
group.push(validateCondition({
|
|
424
|
+
type: "condition",
|
|
425
|
+
field: field.content,
|
|
426
|
+
operation: "EQUAL",
|
|
427
|
+
value: true
|
|
428
|
+
}, constraints, true, baseCtx));
|
|
429
|
+
inConjunction = false;
|
|
430
|
+
expectingExpression = false;
|
|
431
|
+
} else if (field || comparisonOperation || value !== undefined)
|
|
432
|
+
throw new ParseError("Failed to resolve condition; missing operand or operator", ctx.startToken, ctx.startIndex, ctx.endToken, ctx.endIndex);
|
|
433
|
+
field = undefined;
|
|
434
|
+
comparisonOperation = undefined;
|
|
435
|
+
value = undefined;
|
|
436
|
+
}
|
|
437
|
+
for (let t = 0;t < tokens.length; ++t) {
|
|
438
|
+
const token = tokens[t];
|
|
439
|
+
const {
|
|
440
|
+
unquoted,
|
|
441
|
+
unescaped
|
|
442
|
+
} = processToken(token.content);
|
|
443
|
+
if (token.content === ")")
|
|
444
|
+
throw new ParseError("Unexpected closing parenthesis", token, _offset + t);
|
|
445
|
+
if (["]", "}"].includes(token.content))
|
|
446
|
+
throw new ParseError("Unexpected closing bracket/brace", token, _offset + t);
|
|
447
|
+
if (token.content === "(") {
|
|
448
|
+
if (field || comparisonOperation || value)
|
|
449
|
+
throw new ParseError("Tried to open a group during an operation", token, _offset + t);
|
|
450
|
+
const closingIndex = getClosingIndex(tokens, t, "(", ")");
|
|
451
|
+
if (closingIndex === -1)
|
|
452
|
+
throw new ParseError("Missing closing parenthesis for group", token, _offset + t);
|
|
453
|
+
++t;
|
|
454
|
+
const subExpression = _parse(tokens.slice(t, closingIndex), _offset + t, constraints);
|
|
455
|
+
if (subExpression) {
|
|
456
|
+
if (subExpression.type === "group" && subExpression.operation === groupOperation)
|
|
457
|
+
expressions.push(...subExpression.constituents);
|
|
458
|
+
else {
|
|
459
|
+
const group = getExpressionGroup({ startToken: token, startIndex: _offset + t });
|
|
460
|
+
group.push(subExpression);
|
|
461
|
+
inConjunction = false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
t = closingIndex;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const op = OPERATION_ALIAS_DICTIONARY[token.content];
|
|
468
|
+
if (op && OPERATION_PURPOSE_DICTIONARY[op] === "junction") {
|
|
469
|
+
resolveCondition({
|
|
470
|
+
startToken: field?.token ?? token,
|
|
471
|
+
startIndex: field?.index ?? _offset + t,
|
|
472
|
+
endToken: token,
|
|
473
|
+
endIndex: _offset + t
|
|
474
|
+
});
|
|
475
|
+
const prior = expressions.at(-1);
|
|
476
|
+
if (!prior)
|
|
477
|
+
throw new ParseError("Unexpected junction operator with no preceding expression", token, _offset + t);
|
|
478
|
+
expectingExpression = true;
|
|
479
|
+
if (groupOperation && groupOperation !== op) {
|
|
480
|
+
if (expressions.length < 2)
|
|
481
|
+
throw new ParseError("Unexpected junction operator with no preceding expression", token, _offset + t);
|
|
482
|
+
switch (groupOperation) {
|
|
483
|
+
case "AND": {
|
|
484
|
+
const futureSubgroup = _parse(tokens.slice(t + 1), _offset + t, constraints);
|
|
485
|
+
if (futureSubgroup === null)
|
|
486
|
+
throw new ParseError("Dangling junction operator", token, _offset + t);
|
|
487
|
+
return {
|
|
488
|
+
type: "group",
|
|
489
|
+
operation: "OR",
|
|
490
|
+
constituents: [
|
|
491
|
+
{
|
|
492
|
+
type: "group",
|
|
493
|
+
operation: "AND",
|
|
494
|
+
constituents: expressions
|
|
495
|
+
},
|
|
496
|
+
futureSubgroup
|
|
497
|
+
]
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
case "OR":
|
|
501
|
+
inConjunction = true;
|
|
502
|
+
if (prior.type === "group" && prior.operation === "AND")
|
|
503
|
+
continue;
|
|
504
|
+
expressions.splice(-1, 1);
|
|
505
|
+
expressions.push({
|
|
506
|
+
type: "group",
|
|
507
|
+
operation: "AND",
|
|
508
|
+
constituents: [
|
|
509
|
+
prior
|
|
510
|
+
]
|
|
511
|
+
});
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
groupOperation = op;
|
|
516
|
+
if (expressions.length === 1 && expressions[0]?.type === "group" && expressions[0].operation === groupOperation) {
|
|
517
|
+
const exp = expressions[0];
|
|
518
|
+
expressions.splice(0, 1);
|
|
519
|
+
expressions.push(...exp.constituents);
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (token.content === "!") {
|
|
524
|
+
const nextToken = tokens[t + 1];
|
|
525
|
+
if (nextToken?.content === "(") {
|
|
526
|
+
resolveCondition({
|
|
527
|
+
startToken: field?.token ?? token,
|
|
528
|
+
startIndex: field?.index ?? _offset + t,
|
|
529
|
+
endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
|
|
530
|
+
endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
|
|
531
|
+
});
|
|
532
|
+
++t;
|
|
533
|
+
const closingIndex = getClosingIndex(tokens, t, "(", ")");
|
|
534
|
+
if (closingIndex === -1)
|
|
535
|
+
throw new ParseError("Missing closing parenthesis for group", token, _offset + t);
|
|
536
|
+
++t;
|
|
537
|
+
const futureSubExpression = _parse(tokens.slice(t, closingIndex), _offset + t, constraints);
|
|
538
|
+
if (futureSubExpression) {
|
|
539
|
+
complementExpression(futureSubExpression);
|
|
540
|
+
if (futureSubExpression.type === "group" && futureSubExpression.operation === groupOperation)
|
|
541
|
+
expressions.push(...futureSubExpression.constituents);
|
|
542
|
+
else {
|
|
543
|
+
const group = getExpressionGroup({
|
|
544
|
+
startToken: token,
|
|
545
|
+
startIndex: _offset + t,
|
|
546
|
+
endToken: tokens[closingIndex],
|
|
547
|
+
endIndex: closingIndex
|
|
548
|
+
});
|
|
549
|
+
group.push(futureSubExpression);
|
|
550
|
+
inConjunction = false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
t = closingIndex;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!comparisonOperation || op && OPERATION_PURPOSE_DICTIONARY[op] === "comparison") {
|
|
558
|
+
if (op && OPERATION_PURPOSE_DICTIONARY[op] === "comparison") {
|
|
559
|
+
if (comparisonOperation || !field)
|
|
560
|
+
throw new ParseError("Unexpected comparison operator", field?.token ?? token, field?.index ?? _offset + t, token, _offset + t);
|
|
561
|
+
comparisonOperation = {
|
|
562
|
+
content: op,
|
|
563
|
+
token,
|
|
564
|
+
index: _offset + t
|
|
565
|
+
};
|
|
566
|
+
continue;
|
|
567
|
+
} else if (field)
|
|
568
|
+
throw new ParseError("Expected a comparison operator", field.token, field.index, token, _offset + t);
|
|
569
|
+
}
|
|
570
|
+
if (!field) {
|
|
571
|
+
if (token.content === "!") {
|
|
572
|
+
const nextToken = tokens[t + 1];
|
|
573
|
+
if (!nextToken)
|
|
574
|
+
throw new ParseError('Unexpected "!"', token, _offset + t);
|
|
575
|
+
resolveCondition({
|
|
576
|
+
startToken: token,
|
|
577
|
+
startIndex: _offset + t,
|
|
578
|
+
endToken: nextToken,
|
|
579
|
+
endIndex: _offset + t + 1
|
|
580
|
+
});
|
|
581
|
+
++t;
|
|
582
|
+
field = {
|
|
583
|
+
content: processToken(nextToken.content).unescaped,
|
|
584
|
+
token,
|
|
585
|
+
index: _offset + t
|
|
586
|
+
};
|
|
587
|
+
comparisonOperation = {
|
|
588
|
+
content: "EQUAL"
|
|
589
|
+
};
|
|
590
|
+
value = {
|
|
591
|
+
content: false,
|
|
592
|
+
implicit: true
|
|
593
|
+
};
|
|
594
|
+
resolveCondition({
|
|
595
|
+
startToken: field.token,
|
|
596
|
+
startIndex: field.index,
|
|
597
|
+
endToken: nextToken,
|
|
598
|
+
endIndex: _offset + t
|
|
599
|
+
});
|
|
600
|
+
} else {
|
|
601
|
+
field = {
|
|
602
|
+
content: unescaped,
|
|
603
|
+
token,
|
|
604
|
+
index: _offset + t
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (!value) {
|
|
610
|
+
if (["[", "{"].includes(token.content)) {
|
|
611
|
+
let resolveEntry = function(subtoken, subindex) {
|
|
612
|
+
if (workingEntry) {
|
|
613
|
+
const {
|
|
614
|
+
unquoted: unquotedWorkingEntry,
|
|
615
|
+
unescaped: unescapedWorkingEntry
|
|
616
|
+
} = processToken(workingEntry);
|
|
617
|
+
const subquotes = workingEntry.match(QUOTE_REGEX);
|
|
618
|
+
if (subquotes && (subquotes.index !== 0 || subquotes[0].length !== workingEntry.length))
|
|
619
|
+
throw new ParseError("Quotes must surround entire values in arrays", firstEntryToken ?? subtoken, firstEntryTokenIndex ?? subindex, lastEntryToken ?? subtoken, lastEntryTokenIndex ?? subindex);
|
|
620
|
+
if (!unquotedWorkingEntry && (token.content === "[" && new RegExp(`${ESCAPE_REGEX}(?:\\[|\\])`).test(workingEntry) || token.content === "{" && new RegExp(`${ESCAPE_REGEX}(?:\\{|\\})`).test(workingEntry)))
|
|
621
|
+
throw new ParseError("Unescaped bracket in an array value", firstEntryToken ?? subtoken, firstEntryTokenIndex ?? subindex, lastEntryToken ?? subtoken, lastEntryTokenIndex ?? subindex);
|
|
622
|
+
arr.push(parseValue(unescapedWorkingEntry, unquotedWorkingEntry !== undefined));
|
|
623
|
+
workingEntry = "";
|
|
624
|
+
firstEntryToken = undefined;
|
|
625
|
+
firstEntryTokenIndex = undefined;
|
|
626
|
+
lastEntryToken = undefined;
|
|
627
|
+
lastEntryTokenIndex = undefined;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const closingIndex = getClosingIndex(tokens, t, token.content, token.content === "[" ? "]" : "}");
|
|
631
|
+
if (closingIndex === -1)
|
|
632
|
+
throw new ParseError("Missing closing bracket/brace for array value", token, _offset + t);
|
|
633
|
+
++t;
|
|
634
|
+
const arr = [];
|
|
635
|
+
value = {
|
|
636
|
+
content: arr,
|
|
637
|
+
token: tokens[closingIndex],
|
|
638
|
+
index: closingIndex
|
|
639
|
+
};
|
|
640
|
+
const arrayContents = tokens.slice(t, closingIndex);
|
|
641
|
+
let workingEntry = "";
|
|
642
|
+
let firstEntryToken;
|
|
643
|
+
let firstEntryTokenIndex;
|
|
644
|
+
let lastEntryToken;
|
|
645
|
+
let lastEntryTokenIndex;
|
|
646
|
+
for (let ct = 0;ct < arrayContents.length; ++ct) {
|
|
647
|
+
const contentToken = arrayContents[ct];
|
|
648
|
+
if (contentToken.content === ",") {
|
|
649
|
+
if (!workingEntry)
|
|
650
|
+
throw new ParseError("Unexpected blank entry in array", contentToken, _offset + t + ct);
|
|
651
|
+
resolveEntry(contentToken, _offset + t + ct);
|
|
652
|
+
} else {
|
|
653
|
+
lastEntryToken = contentToken;
|
|
654
|
+
lastEntryTokenIndex = _offset + t + ct;
|
|
655
|
+
if (!firstEntryToken)
|
|
656
|
+
firstEntryToken = lastEntryToken;
|
|
657
|
+
if (!firstEntryTokenIndex)
|
|
658
|
+
firstEntryTokenIndex = lastEntryTokenIndex;
|
|
659
|
+
workingEntry += contentToken.content;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
resolveEntry(arrayContents.at(-1), _offset + t + arrayContents.length - 1);
|
|
663
|
+
if (!arr.length) {
|
|
664
|
+
throw new ParseError("Empty array provided as value", token, _offset + t - 1, tokens[closingIndex], _offset + closingIndex);
|
|
665
|
+
}
|
|
666
|
+
t = closingIndex;
|
|
667
|
+
} else {
|
|
668
|
+
value = {
|
|
669
|
+
content: parseValue(unescaped, unquoted !== undefined),
|
|
670
|
+
token,
|
|
671
|
+
index: _offset + t
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
resolveCondition({
|
|
675
|
+
startToken: field.token,
|
|
676
|
+
startIndex: field.index,
|
|
677
|
+
endToken: value.token ?? comparisonOperation?.token ?? field.token,
|
|
678
|
+
endIndex: value.index ?? comparisonOperation?.index ?? field.index
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
resolveCondition({
|
|
683
|
+
startToken: field?.token,
|
|
684
|
+
startIndex: field?.index,
|
|
685
|
+
endToken: value?.token ?? comparisonOperation?.token ?? field?.token,
|
|
686
|
+
endIndex: value?.index ?? comparisonOperation?.index ?? field?.index
|
|
687
|
+
});
|
|
688
|
+
if (inConjunction)
|
|
689
|
+
throw new ParseError("Dangling junction operator", tokens.at(-1), _offset + tokens.length - 1);
|
|
690
|
+
if (groupOperation) {
|
|
691
|
+
if (expressions.length === 1)
|
|
692
|
+
throw new ParseError("Dangling junction operator", tokens.at(-1), _offset + tokens.length - 1);
|
|
693
|
+
return {
|
|
694
|
+
type: "group",
|
|
695
|
+
operation: groupOperation,
|
|
696
|
+
constituents: expressions
|
|
697
|
+
};
|
|
698
|
+
} else if (expressions.length > 1)
|
|
699
|
+
throw new ParseError("Group possesses multiple conditions without disjunctive operators", tokens[0], _offset);
|
|
700
|
+
else
|
|
701
|
+
return expressions[0] ?? null;
|
|
702
|
+
}
|
|
703
|
+
function parse(expression, constraints) {
|
|
704
|
+
let tokens;
|
|
705
|
+
if (Array.isArray(expression)) {
|
|
706
|
+
tokens = [];
|
|
707
|
+
let type;
|
|
708
|
+
for (let t = 0;t < expression.length; ++t) {
|
|
709
|
+
const token = expression[t];
|
|
710
|
+
if (!type)
|
|
711
|
+
type = typeof token;
|
|
712
|
+
if (typeof token !== type)
|
|
713
|
+
console.warn("WizardQL: parse was called with a mixed array of string tokens and token objects");
|
|
714
|
+
tokens.push(typeof token === "string" ? { content: token, index: type === "string" ? t : -1 } : token);
|
|
715
|
+
}
|
|
716
|
+
} else
|
|
717
|
+
tokens = tokenize(expression);
|
|
718
|
+
return _parse(tokens, 0, constraints);
|
|
719
|
+
}
|
|
720
|
+
// src/execute.ts
|
|
721
|
+
function executeAsKnex(query, expression) {
|
|
722
|
+
switch (expression.type) {
|
|
723
|
+
case "group": {
|
|
724
|
+
let firstHappened = false;
|
|
725
|
+
for (const constituent of expression.constituents) {
|
|
726
|
+
query[firstHappened ? expression.operation === "AND" ? "andWhere" : "orWhere" : "where"]((clause) => executeAsKnex(clause, constituent));
|
|
727
|
+
firstHappened = true;
|
|
728
|
+
}
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case "condition":
|
|
732
|
+
switch (expression.operation) {
|
|
733
|
+
case "EQUAL":
|
|
734
|
+
query.where(expression.field, "=", expression.value);
|
|
735
|
+
break;
|
|
736
|
+
case "NOTEQUAL":
|
|
737
|
+
query.where(expression.field, "!=", expression.value);
|
|
738
|
+
break;
|
|
739
|
+
case "GEQ":
|
|
740
|
+
query.where(expression.field, ">=", expression.value);
|
|
741
|
+
break;
|
|
742
|
+
case "LEQ":
|
|
743
|
+
query.where(expression.field, "<=", expression.value);
|
|
744
|
+
break;
|
|
745
|
+
case "GREATER":
|
|
746
|
+
query.where(expression.field, ">", expression.value);
|
|
747
|
+
break;
|
|
748
|
+
case "LESS":
|
|
749
|
+
query.where(expression.field, "<", expression.value);
|
|
750
|
+
break;
|
|
751
|
+
case "IN":
|
|
752
|
+
query.whereIn(expression.field, expression.value);
|
|
753
|
+
break;
|
|
754
|
+
case "NOTIN":
|
|
755
|
+
query.whereNotIn(expression.field, expression.value);
|
|
756
|
+
break;
|
|
757
|
+
case "MATCH":
|
|
758
|
+
query.whereRaw("?? ~* ?", [expression.field, expression.value]);
|
|
759
|
+
break;
|
|
760
|
+
case "NOTMATCH":
|
|
761
|
+
query.whereRaw("?? !~* ?", [expression.field, expression.value]);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// src/summarize.ts
|
|
767
|
+
function getOrPutArrayInMap(map, key) {
|
|
768
|
+
const existingEntry = map.get(key);
|
|
769
|
+
if (existingEntry)
|
|
770
|
+
return existingEntry;
|
|
771
|
+
else {
|
|
772
|
+
const arr = [];
|
|
773
|
+
map.set(key, arr);
|
|
774
|
+
return arr;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function summarize(expressions) {
|
|
778
|
+
const array = Array.isArray(expressions) ? expressions : [expressions];
|
|
779
|
+
const summary = new Map;
|
|
780
|
+
for (const expression of array) {
|
|
781
|
+
if (expression.type === "group") {
|
|
782
|
+
const constituents = summarize(expression.constituents);
|
|
783
|
+
for (const [field, values] of constituents.entries()) {
|
|
784
|
+
const collection = getOrPutArrayInMap(summary, field);
|
|
785
|
+
collection.push(...values);
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
const collection = getOrPutArrayInMap(summary, expression.field);
|
|
789
|
+
collection.push({
|
|
790
|
+
operation: expression.operation,
|
|
791
|
+
value: expression.value,
|
|
792
|
+
exclusionary: ["NOTEQUAL", "LESS", "GREATER", "NOTIN"].includes(expression.operation)
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return summary;
|
|
797
|
+
}
|
|
798
|
+
// src/stringify.ts
|
|
799
|
+
var formats = {
|
|
800
|
+
programmatic: {
|
|
801
|
+
AND: "&",
|
|
802
|
+
OR: "|",
|
|
803
|
+
EQUAL: "=",
|
|
804
|
+
NOTEQUAL: "!=",
|
|
805
|
+
GEQ: ">=",
|
|
806
|
+
GREATER: ">",
|
|
807
|
+
LEQ: "<=",
|
|
808
|
+
LESS: "<",
|
|
809
|
+
IN: ":",
|
|
810
|
+
NOTIN: "!:",
|
|
811
|
+
MATCH: "~",
|
|
812
|
+
NOTMATCH: "!~"
|
|
813
|
+
},
|
|
814
|
+
linguistic: {
|
|
815
|
+
AND: "AND",
|
|
816
|
+
OR: "OR",
|
|
817
|
+
EQUAL: "EQUALS",
|
|
818
|
+
NOTEQUAL: "NOTEQUALS",
|
|
819
|
+
GEQ: "GEQ",
|
|
820
|
+
GREATER: "GREATER",
|
|
821
|
+
LEQ: "LEQ",
|
|
822
|
+
LESS: "LESS",
|
|
823
|
+
IN: "IN",
|
|
824
|
+
NOTIN: "NOTIN",
|
|
825
|
+
MATCH: "MATCHES",
|
|
826
|
+
NOTMATCH: "NOTMATCHES"
|
|
827
|
+
},
|
|
828
|
+
formal: {
|
|
829
|
+
AND: "^",
|
|
830
|
+
OR: "V"
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
function addQuotesIfNecessary(value) {
|
|
834
|
+
if (typeof value !== "string")
|
|
835
|
+
return value.toString();
|
|
836
|
+
if (value === "true")
|
|
837
|
+
return '"true"';
|
|
838
|
+
if (value === "false")
|
|
839
|
+
return '"false"';
|
|
840
|
+
if (!isNaN(Number(value)))
|
|
841
|
+
return `"${value}"`;
|
|
842
|
+
const escaped = value.replaceAll("\\", "\\\\");
|
|
843
|
+
if (new RegExp(TOKEN_REGEX, "gi").test(escaped))
|
|
844
|
+
return `"${escaped.replaceAll('"', "\\\"")}"`;
|
|
845
|
+
return escaped;
|
|
846
|
+
}
|
|
847
|
+
function stringify(expression, opts = {}) {
|
|
848
|
+
const {
|
|
849
|
+
junctionNotation = "programmatic",
|
|
850
|
+
comparisonNotation = "programmatic",
|
|
851
|
+
alwaysParenthesize = false,
|
|
852
|
+
compact = false,
|
|
853
|
+
condenseBooleans = false
|
|
854
|
+
} = opts;
|
|
855
|
+
let string = "";
|
|
856
|
+
switch (expression.type) {
|
|
857
|
+
case "group":
|
|
858
|
+
for (const constituent of expression.constituents) {
|
|
859
|
+
if (string.length) {
|
|
860
|
+
if (!compact || junctionNotation === "linguistic")
|
|
861
|
+
string += " ";
|
|
862
|
+
string += formats[junctionNotation][expression.operation];
|
|
863
|
+
if (!compact || junctionNotation === "linguistic")
|
|
864
|
+
string += " ";
|
|
865
|
+
}
|
|
866
|
+
if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
|
|
867
|
+
string += "(";
|
|
868
|
+
string += stringify(constituent, opts);
|
|
869
|
+
if (alwaysParenthesize && constituent.type === "group" || expression.operation === "AND" && constituent.operation === "OR")
|
|
870
|
+
string += ")";
|
|
871
|
+
}
|
|
872
|
+
break;
|
|
873
|
+
case "condition":
|
|
874
|
+
if (condenseBooleans && ["EQUAL", "NOTEQUAL"].includes(expression.operation) && typeof expression.value === "boolean") {
|
|
875
|
+
const negative = expression.operation === "EQUAL" && !expression.value || expression.operation === "NOTEQUAL" && expression.value;
|
|
876
|
+
string += `${negative ? "!" : ""}${expression.field}`;
|
|
877
|
+
} else {
|
|
878
|
+
string += expression.field;
|
|
879
|
+
if (!compact || comparisonNotation === "linguistic")
|
|
880
|
+
string += " ";
|
|
881
|
+
string += formats[comparisonNotation][expression.operation];
|
|
882
|
+
if (!compact || comparisonNotation === "linguistic")
|
|
883
|
+
string += " ";
|
|
884
|
+
if (Array.isArray(expression.value)) {
|
|
885
|
+
const join = expression.value.map(addQuotesIfNecessary).join(compact ? "," : ", ");
|
|
886
|
+
string += `[${join}]`;
|
|
887
|
+
} else
|
|
888
|
+
string += addQuotesIfNecessary(expression.value);
|
|
889
|
+
}
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
return string;
|
|
893
|
+
}
|
|
894
|
+
// src/dominput.ts
|
|
895
|
+
function getCursorIndex(element) {
|
|
896
|
+
const selection = window.getSelection();
|
|
897
|
+
const cursorNode = selection?.anchorNode;
|
|
898
|
+
const cursorOffset = selection?.anchorOffset;
|
|
899
|
+
if (!cursorNode || !element.contains(cursorNode))
|
|
900
|
+
return -1;
|
|
901
|
+
let absoluteIndex = 0;
|
|
902
|
+
if (cursorOffset) {
|
|
903
|
+
for (const node of element.childNodes) {
|
|
904
|
+
if (node.contains(cursorNode)) {
|
|
905
|
+
absoluteIndex += (node.textContent?.length ?? 0) - (cursorNode.textContent?.length ?? 0);
|
|
906
|
+
absoluteIndex += cursorOffset;
|
|
907
|
+
break;
|
|
908
|
+
} else
|
|
909
|
+
absoluteIndex += node.textContent?.length ?? 0;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return absoluteIndex;
|
|
913
|
+
}
|
|
914
|
+
function setCursor(element, index) {
|
|
915
|
+
const selection = window.getSelection();
|
|
916
|
+
let accumulated = 0;
|
|
917
|
+
for (const node of element.childNodes) {
|
|
918
|
+
const length = node.textContent?.length ?? 0;
|
|
919
|
+
if (accumulated + length >= index) {
|
|
920
|
+
selection?.setPosition(index > 0 ? node.childNodes.item(0) : node, index - accumulated);
|
|
921
|
+
break;
|
|
922
|
+
} else
|
|
923
|
+
accumulated += length;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function createDOMInput({ input, constraints, onUpdate, parseOnInitialize }) {
|
|
927
|
+
const history = [];
|
|
928
|
+
let historyIndex = -1;
|
|
929
|
+
let savedCursor;
|
|
930
|
+
function update() {
|
|
931
|
+
const focused = input.contains(document.activeElement);
|
|
932
|
+
const text = input.textContent.replaceAll(`
|
|
933
|
+
`, "");
|
|
934
|
+
const endPadding = input.textContent?.match(/\s*$/)?.[0].length ?? 0;
|
|
935
|
+
const newTokens = tokenize(text);
|
|
936
|
+
const lastToken = newTokens.at(-1);
|
|
937
|
+
if (lastToken && lastToken.content in OPERATION_ALIAS_DICTIONARY && focused && (!endPadding && lastToken.content.match(/^[A-Za-z]+?$/)))
|
|
938
|
+
return;
|
|
939
|
+
const absoluteIndex = focused ? getCursorIndex(input) : 0;
|
|
940
|
+
observer.disconnect();
|
|
941
|
+
let inArray;
|
|
942
|
+
let offset = 0;
|
|
943
|
+
for (let t = 0;t < newTokens.length; ++t) {
|
|
944
|
+
const token = newTokens[t];
|
|
945
|
+
const prior = newTokens[t - 1];
|
|
946
|
+
const differenceFromLast = prior ? token.index - (prior.index + prior.content.length) : token.index;
|
|
947
|
+
if (differenceFromLast > 0) {
|
|
948
|
+
const spacer = document.createElement("span");
|
|
949
|
+
spacer.textContent = " ".repeat(differenceFromLast);
|
|
950
|
+
spacer.classList.add("whitespace-pre");
|
|
951
|
+
spacer.toggleAttribute("data-spacer", true);
|
|
952
|
+
const existing2 = input.childNodes.item(t + offset);
|
|
953
|
+
if (existing2)
|
|
954
|
+
input.replaceChild(spacer, existing2);
|
|
955
|
+
else
|
|
956
|
+
input.appendChild(spacer);
|
|
957
|
+
++offset;
|
|
958
|
+
}
|
|
959
|
+
const element = document.createElement("span");
|
|
960
|
+
element.textContent = token.content;
|
|
961
|
+
element.toggleAttribute("data-node", true);
|
|
962
|
+
if (token.content.match(QUOTE_EDGE_REGEX))
|
|
963
|
+
element.toggleAttribute("data-quoted", true);
|
|
964
|
+
if (!isNaN(Number(token.content)))
|
|
965
|
+
element.toggleAttribute("data-number", true);
|
|
966
|
+
if (["(", ")", "[", "]", "{", "}"].includes(token.content)) {
|
|
967
|
+
if (inArray === "[" && token.content === "]" || inArray === "{" && token.content === "}")
|
|
968
|
+
inArray = undefined;
|
|
969
|
+
if (!inArray)
|
|
970
|
+
element.setAttribute("data-bracket", token.content);
|
|
971
|
+
if (!inArray && ["[", "{"].includes(token.content))
|
|
972
|
+
inArray = token.content;
|
|
973
|
+
}
|
|
974
|
+
if (inArray && token.content === ",")
|
|
975
|
+
element.toggleAttribute("data-delimiter", true);
|
|
976
|
+
if (!inArray && token.content === "!")
|
|
977
|
+
element.toggleAttribute("data-negation", true);
|
|
978
|
+
if (!inArray && token.content in OPERATION_ALIAS_DICTIONARY) {
|
|
979
|
+
element.setAttribute("data-operation", OPERATION_PURPOSE_DICTIONARY[OPERATION_ALIAS_DICTIONARY[token.content]]);
|
|
980
|
+
}
|
|
981
|
+
const existing = input.childNodes.item(t + offset);
|
|
982
|
+
if (existing)
|
|
983
|
+
input.replaceChild(element, existing);
|
|
984
|
+
else
|
|
985
|
+
input.appendChild(element);
|
|
986
|
+
}
|
|
987
|
+
while (newTokens.length + offset < input.childNodes.length)
|
|
988
|
+
input.lastChild?.remove();
|
|
989
|
+
const last = input.lastElementChild;
|
|
990
|
+
if (last?.tagName === "BR")
|
|
991
|
+
last.remove();
|
|
992
|
+
if (endPadding) {
|
|
993
|
+
const spacer = document.createElement("span");
|
|
994
|
+
spacer.textContent = " ".repeat(endPadding);
|
|
995
|
+
spacer.classList.add("whitespace-pre");
|
|
996
|
+
spacer.toggleAttribute("data-spacer", true);
|
|
997
|
+
input.appendChild(spacer);
|
|
998
|
+
}
|
|
999
|
+
observer.observe(input, { characterData: true, childList: true, subtree: true });
|
|
1000
|
+
if (focused)
|
|
1001
|
+
setCursor(input, savedCursor ?? absoluteIndex);
|
|
1002
|
+
savedCursor = undefined;
|
|
1003
|
+
if (input.textContent && input.textContent !== history[historyIndex]?.text) {
|
|
1004
|
+
++historyIndex;
|
|
1005
|
+
history.splice(historyIndex, history.length - historyIndex, {
|
|
1006
|
+
text: input.textContent,
|
|
1007
|
+
cursor: absoluteIndex
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
let result;
|
|
1011
|
+
try {
|
|
1012
|
+
result = parse(newTokens, constraints);
|
|
1013
|
+
input.removeAttribute("data-error-message");
|
|
1014
|
+
input.removeAttribute("data-error-start");
|
|
1015
|
+
input.removeAttribute("data-error-end");
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
if (err instanceof ParseError || err instanceof ConstraintError) {
|
|
1018
|
+
result = err;
|
|
1019
|
+
input.setAttribute("data-error-message", err.rawMessage);
|
|
1020
|
+
input.setAttribute("data-error-start", err.startIndex.toString());
|
|
1021
|
+
input.setAttribute("data-error-end", err.endIndex.toString());
|
|
1022
|
+
const nodes = input.querySelectorAll("[data-node]");
|
|
1023
|
+
if (!nodes.length)
|
|
1024
|
+
return;
|
|
1025
|
+
for (let n = 0;n < nodes.length; ++n) {
|
|
1026
|
+
const node = nodes.item(n);
|
|
1027
|
+
node.toggleAttribute("data-error", n >= err.startIndex && n <= err.endIndex);
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
result = null;
|
|
1031
|
+
if (err instanceof Error)
|
|
1032
|
+
input.setAttribute("data-error-message", err.message);
|
|
1033
|
+
input.removeAttribute("data-error-start");
|
|
1034
|
+
input.removeAttribute("data-error-end");
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
onUpdate?.(result, newTokens, text);
|
|
1038
|
+
}
|
|
1039
|
+
const observer = new MutationObserver(update);
|
|
1040
|
+
function onKey(e) {
|
|
1041
|
+
if (e.key === "Enter" || e.key === "Escape") {
|
|
1042
|
+
e.stopPropagation();
|
|
1043
|
+
e.preventDefault();
|
|
1044
|
+
input.blur();
|
|
1045
|
+
} else if (e.key === "z" && (e.metaKey || e.ctrlKey)) {
|
|
1046
|
+
e.stopPropagation();
|
|
1047
|
+
e.preventDefault();
|
|
1048
|
+
historyIndex = Math.min(history.length - 1, Math.max(0, historyIndex + (e.shiftKey ? 1 : -1)));
|
|
1049
|
+
const prior = history[historyIndex];
|
|
1050
|
+
if (prior !== undefined) {
|
|
1051
|
+
savedCursor = prior.cursor;
|
|
1052
|
+
input.replaceChildren(prior.text);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
input.setAttribute("role", "textbox");
|
|
1057
|
+
input.setAttribute("contenteditable", "plaintext-only");
|
|
1058
|
+
input.setAttribute("spellcheck", "false");
|
|
1059
|
+
input.addEventListener("keydown", onKey);
|
|
1060
|
+
input.addEventListener("blur", update, { passive: true });
|
|
1061
|
+
observer.observe(input, { characterData: true, childList: true, subtree: true });
|
|
1062
|
+
if (parseOnInitialize)
|
|
1063
|
+
update();
|
|
1064
|
+
return () => {
|
|
1065
|
+
observer.disconnect();
|
|
1066
|
+
input.removeEventListener("keydown", onKey);
|
|
1067
|
+
input.removeEventListener("blur", update);
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
export {
|
|
1071
|
+
tokenize,
|
|
1072
|
+
summarize,
|
|
1073
|
+
stringify,
|
|
1074
|
+
parse,
|
|
1075
|
+
executeAsKnex,
|
|
1076
|
+
createDOMInput,
|
|
1077
|
+
QUOTE_EDGE_REGEX,
|
|
1078
|
+
ParseError,
|
|
1079
|
+
OPERATION_PURPOSE_DICTIONARY,
|
|
1080
|
+
OPERATION_ALIAS_DICTIONARY,
|
|
1081
|
+
ConstraintError,
|
|
1082
|
+
COMPARISON_TYPE_DICTIONARY
|
|
1083
|
+
};
|