autotel-devtools 21.0.1 → 23.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/README.md +183 -28
- package/dist/cli.cjs +80 -13
- package/dist/cli.js +80 -13
- package/dist/compile-JDFHUCo5.d.cts +177 -0
- package/dist/compile-JDFHUCo5.d.ts +177 -0
- package/dist/{error-aggregator-D8VKciLY.d.ts → error-aggregator-B52JI8jL.d.ts} +1 -1
- package/dist/{error-aggregator-DCEKMOm3.d.cts → error-aggregator-DSwUvgFF.d.cts} +1 -1
- package/dist/exporter-BrEcnzoT.d.ts +546 -0
- package/dist/exporter-C4uKgo7l.d.cts +546 -0
- package/dist/fullpage.global.js +39 -0
- package/dist/grpc-CSWjMRiB.cjs +82 -0
- package/dist/grpc-CZEDUYCM.js +77 -0
- package/dist/http-Cqm_MAtc.cjs +3942 -0
- package/dist/http-Dbd9TIYU.js +3776 -0
- package/dist/index.cjs +25 -7
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +25 -7
- package/dist/{listen-DBfsfcdd.js → listen-D-lLgfro.js} +5 -2
- package/dist/{listen-CEJ3nYJf.cjs → listen-l09RRHht.cjs} +5 -2
- package/dist/parse-BRlosZft.cjs +642 -0
- package/dist/parse-D_RmPPQs.js +612 -0
- package/dist/query/index.cjs +8 -0
- package/dist/query/index.d.cts +16 -0
- package/dist/query/index.d.ts +16 -0
- package/dist/query/index.js +3 -0
- package/dist/server/exporter.d.cts +1 -1
- package/dist/server/exporter.d.ts +1 -1
- package/dist/server/index.cjs +6 -2
- package/dist/server/index.d.cts +19 -6
- package/dist/server/index.d.ts +18 -5
- package/dist/server/index.js +3 -2
- package/dist/types-B0tjwFqj.d.cts +107 -0
- package/dist/types-DM8y4A9Z.d.ts +107 -0
- package/dist/widget.global.js +15 -23
- package/dist/wire/index.cjs +7 -0
- package/dist/wire/index.d.cts +26 -0
- package/dist/wire/index.d.ts +26 -0
- package/dist/wire/index.js +3 -0
- package/dist/wire-2Rmfg6IT.js +51 -0
- package/dist/wire-CHU1PkMo.cjs +75 -0
- package/package.json +22 -6
- package/dist/exporter-Dt4kx128.d.cts +0 -207
- package/dist/exporter-Due9Rd4s.d.ts +0 -207
- package/dist/http-CNZMrnzv.js +0 -1453
- package/dist/http-CXSzX4ee.cjs +0 -1607
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/query/compile.ts
|
|
3
|
+
/** The escape character for LIKE patterns; also escaped within them. */
|
|
4
|
+
const LIKE_ESCAPE = "\\";
|
|
5
|
+
function compileWhere(node, schema) {
|
|
6
|
+
const params = [];
|
|
7
|
+
return {
|
|
8
|
+
sql: emit(node, schema, params),
|
|
9
|
+
params
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function emit(node, schema, params) {
|
|
13
|
+
switch (node.type) {
|
|
14
|
+
case "all": return "1";
|
|
15
|
+
case "and":
|
|
16
|
+
case "or": {
|
|
17
|
+
const left = emit(node.left, schema, params);
|
|
18
|
+
const right = emit(node.right, schema, params);
|
|
19
|
+
return `(${left} ${node.type === "and" ? "AND" : "OR"} ${right})`;
|
|
20
|
+
}
|
|
21
|
+
case "freeText": {
|
|
22
|
+
const pattern = likePattern(node.text, "contains");
|
|
23
|
+
const clauses = schema.freeTextColumns.map((field) => {
|
|
24
|
+
params.push(pattern);
|
|
25
|
+
return `${targetSql(field, schema, params, { pathAlreadyPushed: false })} LIKE ? ESCAPE '${LIKE_ESCAPE}'`;
|
|
26
|
+
});
|
|
27
|
+
if (clauses.length === 0) return "1";
|
|
28
|
+
if (clauses.length === 1) return clauses[0];
|
|
29
|
+
return `(${clauses.join(" OR ")})`;
|
|
30
|
+
}
|
|
31
|
+
case "comparison": return emitComparison(node.field, node.op, node.value, schema, params);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* SQL for the left-hand side of a comparison.
|
|
36
|
+
*
|
|
37
|
+
* A known field becomes a quoted column identifier taken from the schema. An
|
|
38
|
+
* unknown one becomes `json_extract(<attrs>, ?)` with the JSON path bound as a
|
|
39
|
+
* parameter — so an arbitrary attribute key is queryable without ever being
|
|
40
|
+
* concatenated into SQL.
|
|
41
|
+
*
|
|
42
|
+
* The parameter ordering is fiddly and deliberate: the path parameter must be
|
|
43
|
+
* pushed *before* the value parameter, because it appears earlier in the
|
|
44
|
+
* emitted SQL.
|
|
45
|
+
*/
|
|
46
|
+
function targetSql(field, schema, params, opts) {
|
|
47
|
+
const known = schema.columns[field];
|
|
48
|
+
if (known) return quoteIdent(known.column);
|
|
49
|
+
if (!opts.pathAlreadyPushed) params.push(jsonPath(field));
|
|
50
|
+
return `json_extract(${quoteIdent(schema.attributesColumn)}, ?)`;
|
|
51
|
+
}
|
|
52
|
+
function emitComparison(field, op, value, schema, params) {
|
|
53
|
+
const related = schema.related?.[field];
|
|
54
|
+
if (related) {
|
|
55
|
+
const predicate = applyOperator(`rel.${quoteIdent(related.column)}`, op, value, params);
|
|
56
|
+
return `EXISTS (SELECT 1 FROM ${quoteIdent(related.table)} rel WHERE ${related.joinSql} AND ${predicate})`;
|
|
57
|
+
}
|
|
58
|
+
const isAttribute = !schema.columns[field];
|
|
59
|
+
if (isAttribute && op === "=" && value.type !== "null" && schema.attributeIndex) {
|
|
60
|
+
params.push(schema.attributeIndex.signal, field, JSON.stringify(jsonScalar(value)));
|
|
61
|
+
return `EXISTS (SELECT 1 FROM ${quoteIdent(schema.attributeIndex.table)} ai WHERE ai.signal = ? AND ai.entity_id = ${schema.attributeIndex.entitySql} AND ai.key = ? AND ai.value_json = ?)`;
|
|
62
|
+
}
|
|
63
|
+
if (isAttribute) params.push(jsonPath(field));
|
|
64
|
+
return applyOperator(targetSql(field, schema, params, { pathAlreadyPushed: isAttribute }), op, value, params);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The operator half of a comparison, against an already-built target
|
|
68
|
+
* expression. Split out so a child-table field can reuse it inside an EXISTS.
|
|
69
|
+
*/
|
|
70
|
+
function applyOperator(target, op, value, params) {
|
|
71
|
+
if (value.type === "null") {
|
|
72
|
+
if (op === "=") return `${target} IS NULL`;
|
|
73
|
+
if (op === "!=") return `${target} IS NOT NULL`;
|
|
74
|
+
}
|
|
75
|
+
switch (op) {
|
|
76
|
+
case "=":
|
|
77
|
+
case "!=":
|
|
78
|
+
case ">":
|
|
79
|
+
case "<":
|
|
80
|
+
case ">=":
|
|
81
|
+
case "<=":
|
|
82
|
+
params.push(scalar(value));
|
|
83
|
+
return `${target} ${op} ?`;
|
|
84
|
+
case "CONTAINS":
|
|
85
|
+
case "NOT CONTAINS":
|
|
86
|
+
case "^":
|
|
87
|
+
case "$": {
|
|
88
|
+
const mode = op === "^" ? "prefix" : op === "$" ? "suffix" : "contains";
|
|
89
|
+
params.push(likePattern(String(scalar(value) ?? ""), mode));
|
|
90
|
+
return `${target} ${op === "NOT CONTAINS" ? "NOT LIKE" : "LIKE"} ? ESCAPE '${LIKE_ESCAPE}'`;
|
|
91
|
+
}
|
|
92
|
+
case "REGEXP":
|
|
93
|
+
case "NOT REGEXP":
|
|
94
|
+
params.push(String(scalar(value) ?? ""));
|
|
95
|
+
return `${target} ${op} ?`;
|
|
96
|
+
case "IN":
|
|
97
|
+
case "NOT IN": {
|
|
98
|
+
const elements = value.type === "array" ? value.values : [value];
|
|
99
|
+
if (elements.length === 0) return op === "IN" ? "0" : "1";
|
|
100
|
+
for (const element of elements) params.push(scalar(element));
|
|
101
|
+
return `${target} ${op} (${elements.map(() => "?").join(", ")})`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function jsonScalar(value) {
|
|
106
|
+
if (value.type === "array") return value.values.map(jsonScalar);
|
|
107
|
+
if (value.type === "null") return null;
|
|
108
|
+
if (value.type === "boolean") return value.value;
|
|
109
|
+
return value.value;
|
|
110
|
+
}
|
|
111
|
+
/** The bindable value for a scalar node. Arrays never reach here. */
|
|
112
|
+
function scalar(value) {
|
|
113
|
+
switch (value.type) {
|
|
114
|
+
case "string": return value.value;
|
|
115
|
+
case "number": return value.value;
|
|
116
|
+
case "boolean": return value.value ? 1 : 0;
|
|
117
|
+
case "null": return null;
|
|
118
|
+
case "array": return JSON.stringify(value.values);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A JSON path for an attribute key.
|
|
123
|
+
*
|
|
124
|
+
* The key is wrapped in double quotes so dots inside it (`http.status_code`)
|
|
125
|
+
* are one key rather than a nested path, and embedded quotes and backslashes
|
|
126
|
+
* are escaped so the path itself stays well-formed. This string is *bound as a
|
|
127
|
+
* parameter*, never concatenated into SQL — the escaping here protects the JSON
|
|
128
|
+
* path grammar, not the SQL.
|
|
129
|
+
*/
|
|
130
|
+
function jsonPath(key) {
|
|
131
|
+
return `$."${key.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build a LIKE pattern, escaping the wildcards LIKE would otherwise honour.
|
|
135
|
+
*
|
|
136
|
+
* Someone searching for `100%` or `user_id` means those characters literally;
|
|
137
|
+
* without escaping, `%` matches anything and `_` matches any single character,
|
|
138
|
+
* and the search quietly returns far too much.
|
|
139
|
+
*/
|
|
140
|
+
function likePattern(text, mode) {
|
|
141
|
+
const escaped = text.replace(/\\/g, `${LIKE_ESCAPE}\\`).replace(/%/g, `${LIKE_ESCAPE}%`).replace(/_/g, `${LIKE_ESCAPE}_`);
|
|
142
|
+
if (mode === "prefix") return `${escaped}%`;
|
|
143
|
+
if (mode === "suffix") return `%${escaped}`;
|
|
144
|
+
return `%${escaped}%`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Quote a SQL identifier.
|
|
148
|
+
*
|
|
149
|
+
* Identifiers only ever come from a `SignalSchema` the caller authored, so this
|
|
150
|
+
* is defence in depth rather than the primary control — but a schema built from
|
|
151
|
+
* config one day should not become an injection vector.
|
|
152
|
+
*/
|
|
153
|
+
function quoteIdent(name) {
|
|
154
|
+
return `"${name.replace(/"/g, "\"\"")}"`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/query/ast.ts
|
|
159
|
+
/**
|
|
160
|
+
* Every operator the language accepts.
|
|
161
|
+
*
|
|
162
|
+
* Sigils and keywords are two spellings of one set — `=~` and `REGEXP` produce
|
|
163
|
+
* the same node — so downstream code branches on one canonical value.
|
|
164
|
+
*/
|
|
165
|
+
const OPERATORS = {
|
|
166
|
+
"=": {
|
|
167
|
+
label: "equals",
|
|
168
|
+
kind: "comparison"
|
|
169
|
+
},
|
|
170
|
+
"!=": {
|
|
171
|
+
label: "does not equal",
|
|
172
|
+
kind: "comparison"
|
|
173
|
+
},
|
|
174
|
+
">": {
|
|
175
|
+
label: "greater than",
|
|
176
|
+
kind: "ordered"
|
|
177
|
+
},
|
|
178
|
+
"<": {
|
|
179
|
+
label: "less than",
|
|
180
|
+
kind: "ordered"
|
|
181
|
+
},
|
|
182
|
+
">=": {
|
|
183
|
+
label: "greater than or equal",
|
|
184
|
+
kind: "ordered"
|
|
185
|
+
},
|
|
186
|
+
"<=": {
|
|
187
|
+
label: "less than or equal",
|
|
188
|
+
kind: "ordered"
|
|
189
|
+
},
|
|
190
|
+
"^": {
|
|
191
|
+
label: "starts with",
|
|
192
|
+
kind: "text"
|
|
193
|
+
},
|
|
194
|
+
$: {
|
|
195
|
+
label: "ends with",
|
|
196
|
+
kind: "text"
|
|
197
|
+
},
|
|
198
|
+
CONTAINS: {
|
|
199
|
+
label: "contains",
|
|
200
|
+
kind: "text"
|
|
201
|
+
},
|
|
202
|
+
"NOT CONTAINS": {
|
|
203
|
+
label: "does not contain",
|
|
204
|
+
kind: "text"
|
|
205
|
+
},
|
|
206
|
+
REGEXP: {
|
|
207
|
+
label: "matches regex",
|
|
208
|
+
kind: "text"
|
|
209
|
+
},
|
|
210
|
+
"NOT REGEXP": {
|
|
211
|
+
label: "does not match regex",
|
|
212
|
+
kind: "text"
|
|
213
|
+
},
|
|
214
|
+
IN: {
|
|
215
|
+
label: "is one of",
|
|
216
|
+
kind: "set"
|
|
217
|
+
},
|
|
218
|
+
"NOT IN": {
|
|
219
|
+
label: "is not one of",
|
|
220
|
+
kind: "set"
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
/** Sigil spellings that map onto a keyword operator. */
|
|
224
|
+
const SIGIL_ALIASES = {
|
|
225
|
+
"=~": "REGEXP",
|
|
226
|
+
"!~": "NOT REGEXP"
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/query/tokenize.ts
|
|
231
|
+
/**
|
|
232
|
+
* Characters a bare word may contain.
|
|
233
|
+
*
|
|
234
|
+
* Deliberately wide: attribute keys in the wild carry dots, slashes, colons and
|
|
235
|
+
* dashes (`http.status_code`, `GET /users`, `db.system`), and requiring quotes
|
|
236
|
+
* around every one of them would make the common query the awkward one. The
|
|
237
|
+
* exclusions are the characters the grammar itself needs.
|
|
238
|
+
*/
|
|
239
|
+
const WORD_RE = /[^\s()[\],=!<>^$"']/;
|
|
240
|
+
/** Multi-character sigils are tested before single-character ones. */
|
|
241
|
+
const SIGILS = [
|
|
242
|
+
">=",
|
|
243
|
+
"<=",
|
|
244
|
+
"!=",
|
|
245
|
+
"=~",
|
|
246
|
+
"!~",
|
|
247
|
+
"=",
|
|
248
|
+
">",
|
|
249
|
+
"<",
|
|
250
|
+
"^",
|
|
251
|
+
"$"
|
|
252
|
+
];
|
|
253
|
+
function tokenize(input) {
|
|
254
|
+
const tokens = [];
|
|
255
|
+
let i = 0;
|
|
256
|
+
const push = (type, value, from, to, terminated = true) => tokens.push({
|
|
257
|
+
type,
|
|
258
|
+
value,
|
|
259
|
+
from,
|
|
260
|
+
to,
|
|
261
|
+
terminated
|
|
262
|
+
});
|
|
263
|
+
while (i < input.length) {
|
|
264
|
+
const ch = input[i];
|
|
265
|
+
if (/\s/.test(ch)) {
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const punctuation = {
|
|
270
|
+
"(": "lparen",
|
|
271
|
+
")": "rparen",
|
|
272
|
+
"[": "lbracket",
|
|
273
|
+
"]": "rbracket",
|
|
274
|
+
",": "comma"
|
|
275
|
+
};
|
|
276
|
+
if (punctuation[ch]) {
|
|
277
|
+
push(punctuation[ch], ch, i, i + 1);
|
|
278
|
+
i++;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (ch === "\"" || ch === "'") {
|
|
282
|
+
const start = i;
|
|
283
|
+
const quote = ch;
|
|
284
|
+
i++;
|
|
285
|
+
let value = "";
|
|
286
|
+
let terminated = false;
|
|
287
|
+
while (i < input.length) {
|
|
288
|
+
if (input[i] === "\\" && i + 1 < input.length) {
|
|
289
|
+
value += input[i + 1];
|
|
290
|
+
i += 2;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (input[i] === quote) {
|
|
294
|
+
i++;
|
|
295
|
+
terminated = true;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
value += input[i];
|
|
299
|
+
i++;
|
|
300
|
+
}
|
|
301
|
+
push("string", value, start, i, terminated);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const sigil = SIGILS.find((s) => input.startsWith(s, i));
|
|
305
|
+
if (sigil) {
|
|
306
|
+
push("sigil", sigil, i, i + sigil.length);
|
|
307
|
+
i += sigil.length;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const start = i;
|
|
311
|
+
while (i < input.length && WORD_RE.test(input[i])) i++;
|
|
312
|
+
if (i === start) i++;
|
|
313
|
+
push("word", input.slice(start, i), start, i);
|
|
314
|
+
}
|
|
315
|
+
return tokens;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/query/parse.ts
|
|
320
|
+
/**
|
|
321
|
+
* Recursive-descent parser for the telemetry query language.
|
|
322
|
+
*
|
|
323
|
+
* Grammar, loosest binding first:
|
|
324
|
+
*
|
|
325
|
+
* query := or?
|
|
326
|
+
* or := and (OR and)*
|
|
327
|
+
* and := condition ((AND)? condition)* -- juxtaposition means AND
|
|
328
|
+
* condition := "(" or ")" | comparison | freeText
|
|
329
|
+
* comparison := field operator value
|
|
330
|
+
* value := string | number | boolean | null | array
|
|
331
|
+
* array := "[" (value ("," value)*)? "]"
|
|
332
|
+
*
|
|
333
|
+
* Juxtaposition binding as AND is deliberate: `service = api duration > 100` is
|
|
334
|
+
* what people type, and rejecting it to demand the keyword buys nothing.
|
|
335
|
+
*
|
|
336
|
+
* Errors are collected with source ranges rather than thrown on the first
|
|
337
|
+
* problem, so a half-typed query can still be linted in the editor.
|
|
338
|
+
*/
|
|
339
|
+
const BOOLEAN_KEYWORDS = /* @__PURE__ */ new Set(["and", "or"]);
|
|
340
|
+
const NULL_LITERALS = /* @__PURE__ */ new Set(["null", "nil"]);
|
|
341
|
+
/** Keyword operators, lower-cased, mapped to their canonical spelling. */
|
|
342
|
+
const KEYWORD_OPERATORS = /* @__PURE__ */ new Map([
|
|
343
|
+
["contains", "CONTAINS"],
|
|
344
|
+
["regexp", "REGEXP"],
|
|
345
|
+
["in", "IN"]
|
|
346
|
+
]);
|
|
347
|
+
function parse(input) {
|
|
348
|
+
const tokens = tokenize(input);
|
|
349
|
+
const errors = [];
|
|
350
|
+
/** Range used when a problem is found past the last token. */
|
|
351
|
+
const endRange = () => ({
|
|
352
|
+
from: tokens.length ? tokens[tokens.length - 1].to : 0,
|
|
353
|
+
to: input.length
|
|
354
|
+
});
|
|
355
|
+
let pos = 0;
|
|
356
|
+
const peek = (offset = 0) => tokens[pos + offset];
|
|
357
|
+
const next = () => tokens[pos++];
|
|
358
|
+
const fail = (message, range) => errors.push({
|
|
359
|
+
message,
|
|
360
|
+
range
|
|
361
|
+
});
|
|
362
|
+
/** Lower-cased text of a `word` token, or undefined for any other token. */
|
|
363
|
+
const wordAt = (offset = 0) => {
|
|
364
|
+
const token = peek(offset);
|
|
365
|
+
return token?.type === "word" ? token.value.toLowerCase() : void 0;
|
|
366
|
+
};
|
|
367
|
+
const isBooleanKeyword = (offset = 0) => {
|
|
368
|
+
const word = wordAt(offset);
|
|
369
|
+
return word !== void 0 && BOOLEAN_KEYWORDS.has(word);
|
|
370
|
+
};
|
|
371
|
+
function span(from, to) {
|
|
372
|
+
return {
|
|
373
|
+
from: from.from,
|
|
374
|
+
to: to.to
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function parseValue() {
|
|
378
|
+
const token = next();
|
|
379
|
+
if (!token) {
|
|
380
|
+
fail("Expected a value", endRange());
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (token.type === "string") {
|
|
384
|
+
if (!token.terminated) fail("Unterminated quoted string", {
|
|
385
|
+
from: token.from,
|
|
386
|
+
to: token.to
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
type: "string",
|
|
390
|
+
value: token.value
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (token.type === "lbracket") {
|
|
394
|
+
const values = [];
|
|
395
|
+
if (peek()?.type === "rbracket") {
|
|
396
|
+
next();
|
|
397
|
+
return {
|
|
398
|
+
type: "array",
|
|
399
|
+
values
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
for (;;) {
|
|
403
|
+
const element = parseValue();
|
|
404
|
+
if (!element) return {
|
|
405
|
+
type: "array",
|
|
406
|
+
values
|
|
407
|
+
};
|
|
408
|
+
values.push(element);
|
|
409
|
+
const separator = peek();
|
|
410
|
+
if (separator?.type === "comma") {
|
|
411
|
+
next();
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (separator?.type === "rbracket") {
|
|
415
|
+
next();
|
|
416
|
+
return {
|
|
417
|
+
type: "array",
|
|
418
|
+
values
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
fail("Expected \",\" or \"]\" in array", separator ?? endRange());
|
|
422
|
+
return {
|
|
423
|
+
type: "array",
|
|
424
|
+
values
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (token.type === "word") {
|
|
429
|
+
const lower = token.value.toLowerCase();
|
|
430
|
+
if (NULL_LITERALS.has(lower)) return { type: "null" };
|
|
431
|
+
if (lower === "true") return {
|
|
432
|
+
type: "boolean",
|
|
433
|
+
value: true
|
|
434
|
+
};
|
|
435
|
+
if (lower === "false") return {
|
|
436
|
+
type: "boolean",
|
|
437
|
+
value: false
|
|
438
|
+
};
|
|
439
|
+
const numeric = Number(token.value);
|
|
440
|
+
if (Number.isFinite(numeric)) return {
|
|
441
|
+
type: "number",
|
|
442
|
+
value: numeric
|
|
443
|
+
};
|
|
444
|
+
return {
|
|
445
|
+
type: "string",
|
|
446
|
+
value: token.value
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
fail(`Expected a value, found "${token.value}"`, token);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Read an operator at the cursor, or undefined if there isn't one.
|
|
453
|
+
*
|
|
454
|
+
* Handles all three spellings: a sigil (`>=`), a keyword (`CONTAINS`), and a
|
|
455
|
+
* two-word negation (`NOT IN`).
|
|
456
|
+
*/
|
|
457
|
+
function parseOperator() {
|
|
458
|
+
const token = peek();
|
|
459
|
+
if (!token) return void 0;
|
|
460
|
+
if (token.type === "sigil") {
|
|
461
|
+
next();
|
|
462
|
+
return SIGIL_ALIASES[token.value] ?? token.value;
|
|
463
|
+
}
|
|
464
|
+
if (token.type !== "word") return void 0;
|
|
465
|
+
const lower = token.value.toLowerCase();
|
|
466
|
+
if (lower === "not") {
|
|
467
|
+
const following = wordAt(1);
|
|
468
|
+
const base = following ? KEYWORD_OPERATORS.get(following) : void 0;
|
|
469
|
+
if (base) {
|
|
470
|
+
next();
|
|
471
|
+
next();
|
|
472
|
+
return `NOT ${base}`;
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const keyword = KEYWORD_OPERATORS.get(lower);
|
|
477
|
+
if (keyword) {
|
|
478
|
+
next();
|
|
479
|
+
return keyword;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function parseCondition() {
|
|
483
|
+
const token = peek();
|
|
484
|
+
if (!token) {
|
|
485
|
+
fail("Expected a condition", endRange());
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (token.type === "lparen") {
|
|
489
|
+
next();
|
|
490
|
+
const inner = parseOr();
|
|
491
|
+
const closing = peek();
|
|
492
|
+
if (closing?.type === "rparen") next();
|
|
493
|
+
else fail("Unclosed parenthesis — expected \")\"", closing ?? endRange());
|
|
494
|
+
return inner;
|
|
495
|
+
}
|
|
496
|
+
if (token.type === "word" || token.type === "string") {
|
|
497
|
+
const savedPos = pos;
|
|
498
|
+
next();
|
|
499
|
+
const op = parseOperator();
|
|
500
|
+
if (op) {
|
|
501
|
+
const value = parseValue();
|
|
502
|
+
if (!value) return void 0;
|
|
503
|
+
const previous = tokens[pos - 1];
|
|
504
|
+
return {
|
|
505
|
+
type: "comparison",
|
|
506
|
+
field: token.value,
|
|
507
|
+
op,
|
|
508
|
+
value,
|
|
509
|
+
range: span(token, previous ?? token)
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
pos = savedPos;
|
|
513
|
+
next();
|
|
514
|
+
if (token.type === "string" && !token.terminated) fail("Unterminated quoted string", {
|
|
515
|
+
from: token.from,
|
|
516
|
+
to: token.to
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
type: "freeText",
|
|
520
|
+
text: token.value,
|
|
521
|
+
range: {
|
|
522
|
+
from: token.from,
|
|
523
|
+
to: token.to
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
fail(`Unexpected "${token.value}"`, token);
|
|
528
|
+
next();
|
|
529
|
+
}
|
|
530
|
+
/** True when the cursor sits on something that could begin a condition. */
|
|
531
|
+
function atConditionStart() {
|
|
532
|
+
const token = peek();
|
|
533
|
+
if (!token) return false;
|
|
534
|
+
if (token.type === "rparen" || token.type === "rbracket") return false;
|
|
535
|
+
if (token.type === "comma") return false;
|
|
536
|
+
return !isBooleanKeyword();
|
|
537
|
+
}
|
|
538
|
+
function parseAnd() {
|
|
539
|
+
let left = parseCondition();
|
|
540
|
+
if (!left) return void 0;
|
|
541
|
+
for (;;) {
|
|
542
|
+
if (wordAt() === "and") {
|
|
543
|
+
next();
|
|
544
|
+
const right = parseCondition();
|
|
545
|
+
if (!right) return left;
|
|
546
|
+
left = {
|
|
547
|
+
type: "and",
|
|
548
|
+
left,
|
|
549
|
+
right,
|
|
550
|
+
range: nodeSpan(left, right)
|
|
551
|
+
};
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (atConditionStart()) {
|
|
555
|
+
const right = parseCondition();
|
|
556
|
+
if (!right) return left;
|
|
557
|
+
left = {
|
|
558
|
+
type: "and",
|
|
559
|
+
left,
|
|
560
|
+
right,
|
|
561
|
+
range: nodeSpan(left, right)
|
|
562
|
+
};
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
return left;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function parseOr() {
|
|
569
|
+
let left = parseAnd();
|
|
570
|
+
if (!left) return void 0;
|
|
571
|
+
while (wordAt() === "or") {
|
|
572
|
+
next();
|
|
573
|
+
const right = parseAnd();
|
|
574
|
+
if (!right) return left;
|
|
575
|
+
left = {
|
|
576
|
+
type: "or",
|
|
577
|
+
left,
|
|
578
|
+
right,
|
|
579
|
+
range: nodeSpan(left, right)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return left;
|
|
583
|
+
}
|
|
584
|
+
if (tokens.length === 0) return {
|
|
585
|
+
ok: true,
|
|
586
|
+
node: { type: "all" }
|
|
587
|
+
};
|
|
588
|
+
const node = parseOr();
|
|
589
|
+
const leftover = peek();
|
|
590
|
+
if (leftover) fail(`Unexpected "${leftover.value}"`, leftover);
|
|
591
|
+
if (errors.length > 0 || !node) return {
|
|
592
|
+
ok: false,
|
|
593
|
+
errors: errors.length > 0 ? errors : [{
|
|
594
|
+
message: "Invalid query",
|
|
595
|
+
range: endRange()
|
|
596
|
+
}]
|
|
597
|
+
};
|
|
598
|
+
return {
|
|
599
|
+
ok: true,
|
|
600
|
+
node
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/** Source range covering two nodes, tolerating the range-less `all` node. */
|
|
604
|
+
function nodeSpan(left, right) {
|
|
605
|
+
const from = "range" in left ? left.range.from : 0;
|
|
606
|
+
return {
|
|
607
|
+
from,
|
|
608
|
+
to: "range" in right ? right.range.to : from
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
//#endregion
|
|
613
|
+
Object.defineProperty(exports, 'OPERATORS', {
|
|
614
|
+
enumerable: true,
|
|
615
|
+
get: function () {
|
|
616
|
+
return OPERATORS;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
Object.defineProperty(exports, 'SIGIL_ALIASES', {
|
|
620
|
+
enumerable: true,
|
|
621
|
+
get: function () {
|
|
622
|
+
return SIGIL_ALIASES;
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
Object.defineProperty(exports, 'compileWhere', {
|
|
626
|
+
enumerable: true,
|
|
627
|
+
get: function () {
|
|
628
|
+
return compileWhere;
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
Object.defineProperty(exports, 'parse', {
|
|
632
|
+
enumerable: true,
|
|
633
|
+
get: function () {
|
|
634
|
+
return parse;
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
Object.defineProperty(exports, 'tokenize', {
|
|
638
|
+
enumerable: true,
|
|
639
|
+
get: function () {
|
|
640
|
+
return tokenize;
|
|
641
|
+
}
|
|
642
|
+
});
|