@tellaro/tql 1.5.0 → 2.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/dist/index.js +67 -64
- package/dist/src/builder.d.ts +6 -0
- package/dist/src/completionProvider.js +276 -195
- package/dist/src/complexityAnalyzer.js +41 -32
- package/dist/src/index.d.ts +2 -2
- package/dist/src/parser.d.ts +193 -1
- package/dist/src/parser.js +614 -127
- package/dist/src/semanticPreview.d.ts +23 -0
- package/dist/src/semanticPreview.js +111 -41
- package/dist/src/tokenizer.d.ts +134 -5
- package/dist/src/tokenizer.js +47 -30
- package/dist/src/types.d.ts +86 -4
- package/dist/src/validator.d.ts +101 -1
- package/dist/src/validator.js +391 -152
- package/dist/tql.css +1 -1
- package/dist/vue/TqlInput.vue.js +1 -1
- package/dist/vue/TqlInput.vue2.js +273 -254
- package/package.json +1 -1
package/dist/src/validator.js
CHANGED
|
@@ -1,22 +1,110 @@
|
|
|
1
|
-
import { parseTql as
|
|
2
|
-
const c =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
1
|
+
import { parseTql as w } from "./parser.js";
|
|
2
|
+
const c = 1e3, i = [
|
|
3
|
+
"text",
|
|
4
|
+
"keyword",
|
|
5
|
+
"long",
|
|
6
|
+
"integer",
|
|
7
|
+
"short",
|
|
8
|
+
"byte",
|
|
9
|
+
"double",
|
|
10
|
+
"float",
|
|
11
|
+
"half_float",
|
|
12
|
+
"scaled_float",
|
|
13
|
+
"unsigned_long",
|
|
14
|
+
"date",
|
|
15
|
+
"boolean",
|
|
16
|
+
"ip",
|
|
17
|
+
"nested",
|
|
18
|
+
"object",
|
|
19
|
+
"geo_point"
|
|
20
|
+
], u = [
|
|
21
|
+
"long",
|
|
22
|
+
"integer",
|
|
23
|
+
"short",
|
|
24
|
+
"byte",
|
|
25
|
+
"double",
|
|
26
|
+
"float",
|
|
27
|
+
"half_float",
|
|
28
|
+
"scaled_float",
|
|
29
|
+
"unsigned_long"
|
|
30
|
+
], a = [...u, "date", "keyword"], g = [...u, "date"], f = {
|
|
31
|
+
// Equality (all types) — and now that is what it says.
|
|
32
|
+
//
|
|
33
|
+
// The transcribed twelve-element list this replaces claimed "all types" while omitting
|
|
34
|
+
// five: `short`, `byte`, `half_float`, `scaled_float` and `unsigned_long` are all in the
|
|
35
|
+
// `TqlFieldType` union and were all absent, so `port eq 443` on a `short` field warned
|
|
36
|
+
// that equality "may not work well" with it.
|
|
37
|
+
eq: i,
|
|
38
|
+
ne: i,
|
|
39
|
+
"=": i,
|
|
40
|
+
"!=": i,
|
|
9
41
|
eq_ci: ["text", "keyword"],
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
// Ordered comparison — numeric, date, AND keyword.
|
|
43
|
+
//
|
|
44
|
+
// `keyword` is not a courtesy. `compare_ordered` in `tql/src/comparator.rs` tries numbers
|
|
45
|
+
// first and falls back to a LEXICOGRAPHIC string comparison when both sides are strings,
|
|
46
|
+
// Python does the same, and Python's own validator lists all four in `keyword_ops`
|
|
47
|
+
// (`_validate_simple_type_compatibility`). Warning here was a false positive against every
|
|
48
|
+
// layer at once — `host.name gte "m"` is a query the editor flagged, the validator
|
|
49
|
+
// accepts, the evaluator answers and the cluster serves.
|
|
50
|
+
//
|
|
51
|
+
// `text` stays out, and that is the same source's judgement rather than an omission:
|
|
52
|
+
// `text_ops` excludes the four, because a range over an ANALYZED field compares tokens
|
|
53
|
+
// rather than the value the user typed. `between` also stays out — see the note on it.
|
|
54
|
+
//
|
|
55
|
+
// Every numeric width is listed via ALL_NUMERIC_FIELD_TYPES so the five narrow ones cannot
|
|
56
|
+
// fall out again the way they did from `eq`.
|
|
57
|
+
gt: a,
|
|
58
|
+
gte: a,
|
|
59
|
+
lt: a,
|
|
60
|
+
lte: a,
|
|
61
|
+
">": a,
|
|
62
|
+
">=": a,
|
|
63
|
+
"<": a,
|
|
64
|
+
"<=": a,
|
|
19
65
|
// String operations
|
|
66
|
+
//
|
|
67
|
+
// ## The `_cs` operators list `text`, and that is deliberate — see below
|
|
68
|
+
//
|
|
69
|
+
// Both engines now REFUSE every `_cs` operator when the field cannot answer it: an
|
|
70
|
+
// ANALYZED field with no case-preserving subfield. The analyzer lowercased every token it
|
|
71
|
+
// indexed, so the case the query asks about is not in the index at all; before the refusal
|
|
72
|
+
// both engines fell back to the analyzed field and returned zero hits with no error.
|
|
73
|
+
//
|
|
74
|
+
// ALL EIGHT are covered, and the set arrived in two commits rather than one, which is why
|
|
75
|
+
// it is stated as a rule and not as a list of six:
|
|
76
|
+
//
|
|
77
|
+
// * `a3971bd` — `contains_cs`, `not_contains_cs`, `startswith_cs`, `not_startswith_cs`,
|
|
78
|
+
// `endswith_cs`, `not_endswith_cs`, as `CASE_SENSITIVE_WHOLE_VALUE_OPERATORS`. Named
|
|
79
|
+
// one by one rather than as "and their negations": the shorthand is how the eighth
|
|
80
|
+
// operator went missing from this paragraph in the first place.
|
|
81
|
+
// * `c8e3e8a` — `in_cs`, `not_in_cs`, as `CASE_SENSITIVE_MEMBERSHIP_OPERATORS`. These
|
|
82
|
+
// escaped the first round for a classification reason, not a semantic one: `in_cs` is
|
|
83
|
+
// a keyword operator (an exact-match `terms` query), so it routed through a different
|
|
84
|
+
// branch of `get_query_field` whose fallback still landed on the analyzed field.
|
|
85
|
+
//
|
|
86
|
+
// Both constants live in `tql/src/opensearch/field_mappings.rs` and
|
|
87
|
+
// `src/tql/opensearch_components/field_mapping.py`. A note here naming only the first six
|
|
88
|
+
// would leave a reader correct about `contains_cs` and wrong about `in_cs`, silently —
|
|
89
|
+
// which is the exact failure the engines' own interim pin existed to prevent.
|
|
90
|
+
//
|
|
91
|
+
// That rule is NOT expressible in this package, and the reason is specific rather than a
|
|
92
|
+
// shrug: it keys on the MAPPING, not on the field type. The engines refuse only when there
|
|
93
|
+
// is no case-preserving form — a `text` field WITH a `.keyword` subfield translates fine,
|
|
94
|
+
// and that is the most common shape in ECS. This package's field model
|
|
95
|
+
// (`TqlValidationOptions.fieldTypes`, `TqlFieldSchema`) carries one `TqlFieldType` per
|
|
96
|
+
// name and nothing about analysis or multifields: `TqlFieldSchema.nested` is object
|
|
97
|
+
// children, not subfields. So `type === "text"` does not decide the question, and a check
|
|
98
|
+
// keyed on it would be a false positive on exactly the mapping shape that works.
|
|
99
|
+
//
|
|
100
|
+
// Dropping `text` from the eight `_cs` rows — the six below and `in_cs` / `not_in_cs`
|
|
101
|
+
// further down — would be that check by another name. It is left in on all eight.
|
|
102
|
+
// Closing this properly means teaching the schema about multifields, at which point the
|
|
103
|
+
// rule belongs in `OPERATOR_REQUIRED_FIELD_TYPES`'s caller rather than in the table.
|
|
104
|
+
//
|
|
105
|
+
// `tests/caseSensitiveOperatorAbsence.spec.ts` derives the engines' refusal set from their
|
|
106
|
+
// own sources and asserts this note names all of it — so a third `_cs` round cannot leave
|
|
107
|
+
// this paragraph quietly under-claiming the way the six-name version did.
|
|
20
108
|
contains: ["text", "keyword"],
|
|
21
109
|
not_contains: ["text", "keyword"],
|
|
22
110
|
"!contains": ["text", "keyword"],
|
|
@@ -44,47 +132,93 @@ const c = {
|
|
|
44
132
|
matches: ["text", "keyword"],
|
|
45
133
|
not_matches: ["text", "keyword"],
|
|
46
134
|
"!matches": ["text", "keyword"],
|
|
47
|
-
// Range
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
135
|
+
// Range. Numeric and date, via the named constant — the transcribed list this replaces
|
|
136
|
+
// dropped the same five narrow widths `eq` did (`short`, `byte`, `half_float`,
|
|
137
|
+
// `scaled_float`, `unsigned_long`), so `port between [1, 1024]` on a `short` field warned.
|
|
138
|
+
// Python's `numeric_ops` carries `between` and covers `short` and `byte`; the other three
|
|
139
|
+
// widths have no arm at all in `_validate_simple_type_compatibility` and fall through its
|
|
140
|
+
// `else: return`, which allows every operator.
|
|
141
|
+
//
|
|
142
|
+
// `keyword` and `text` stay out, and that is a decision rather than an omission: Python's
|
|
143
|
+
// `keyword_ops` excludes `between` while including `gte`, which is the divergence pinned
|
|
144
|
+
// in `tests/collectionAndNullSemantics.spec.ts`.
|
|
145
|
+
between: g,
|
|
146
|
+
not_between: g,
|
|
147
|
+
"!between": g,
|
|
148
|
+
// Membership — the same set as `eq`, because `in` IS a disjunction of equalities and both
|
|
149
|
+
// evaluators implement it that way. The transcribed list this replaces omitted the five
|
|
150
|
+
// narrow numeric widths plus `nested`, `object` and `geo_point`.
|
|
151
|
+
//
|
|
152
|
+
// This is deliberately WIDER than Python's validator, which is already the standing stance
|
|
153
|
+
// here rather than a new one: `date_ops` and `boolean_ops` both omit `in`, so
|
|
154
|
+
// `_validate_simple_type_compatibility` refuses `f in ['2024-01-01','2024-01-02']` on a
|
|
155
|
+
// date field — while its own evaluator answers it and the cluster serves the DSL. That is
|
|
156
|
+
// the same validator-versus-evaluator disagreement already pinned for `keyword between`
|
|
157
|
+
// and `keyword is null`, and this table has always sided with the evaluator on it (`date`
|
|
158
|
+
// and `boolean` were in the transcribed list too). Widening to the remaining types applies
|
|
159
|
+
// the existing stance consistently; it does not take a new position.
|
|
160
|
+
in: i,
|
|
161
|
+
not_in: i,
|
|
162
|
+
"!in": i,
|
|
54
163
|
in_cs: ["text", "keyword"],
|
|
55
164
|
not_in_cs: ["text", "keyword"],
|
|
56
165
|
"!in_cs": ["text", "keyword"],
|
|
57
|
-
// Existence
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
is
|
|
63
|
-
|
|
64
|
-
|
|
166
|
+
// Existence and null checks — ONE predicate, so ONE list.
|
|
167
|
+
//
|
|
168
|
+
// `field is null` now means the field HAS NO VALUE (absent OR explicitly null), which is
|
|
169
|
+
// exactly `field not exists`; `is not null` is its complement and exactly `exists`. A
|
|
170
|
+
// product-owner decision landed in both engines as a breaking change. An empty array `[]`
|
|
171
|
+
// is a PRESENT value and satisfies neither `is null` nor `not exists` — complementarity is
|
|
172
|
+
// the reason, since `exists` answers true for `[]` and no record may satisfy both sides.
|
|
173
|
+
//
|
|
174
|
+
// Sharing the constant is what keeps the four honest. As four separate transcriptions they
|
|
175
|
+
// had already drifted from the union by five types apiece, and nothing would have caught
|
|
176
|
+
// two of them drifting from each other.
|
|
177
|
+
exists: i,
|
|
178
|
+
not_exists: i,
|
|
179
|
+
"!exists": i,
|
|
180
|
+
is: i,
|
|
181
|
+
is_not: i,
|
|
182
|
+
"!is": i,
|
|
65
183
|
// IP-specific operators
|
|
66
184
|
cidr: ["ip"],
|
|
67
185
|
not_cidr: ["ip"],
|
|
68
186
|
"!cidr": ["ip"],
|
|
69
|
-
// Collection operators
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
187
|
+
// Collection operators — every type, and that is the engines' rule rather than a shrug.
|
|
188
|
+
//
|
|
189
|
+
// NOTE these rows are not reachable from this file: the parser builds a `collection` node
|
|
190
|
+
// whose `condition` carries the default inner operator `eq`, so `any`/`all`/`none` never
|
|
191
|
+
// arrive at `validateComparison`. They are corrected rather than left wrong because a
|
|
192
|
+
// stale rule gets copied -- the identical restriction WAS live in
|
|
193
|
+
// `completionProvider.ts`, where it withheld `any` from every `ip` and `long` field, and
|
|
194
|
+
// that is where `tests/collectionAndNullSemantics.spec.ts` measures it.
|
|
195
|
+
//
|
|
196
|
+
// Two things changed under this table. Both evaluators now treat a SCALAR field as a
|
|
197
|
+
// one-element collection (Rust returned false for every scalar before `1fe0a9f`), and both
|
|
198
|
+
// iterate array-valued fields across the comparison surface. Neither is conditional on the
|
|
199
|
+
// mapping type: OpenSearch has no array type, so `related.ip`, `host.ip`, `tags` and
|
|
200
|
+
// `dns.answers` are `ip` / `keyword` / `object` fields that ECS guarantees hold arrays.
|
|
201
|
+
//
|
|
202
|
+
// Restricting these six to `nested | object | keyword | text` therefore described neither
|
|
203
|
+
// engine. `related.ip any ["10.0.0.1"]` is the ordinary case for an IP field, not an
|
|
204
|
+
// abuse of one.
|
|
205
|
+
any: i,
|
|
206
|
+
all: i,
|
|
207
|
+
none: i,
|
|
208
|
+
not_any: i,
|
|
209
|
+
not_all: i,
|
|
210
|
+
not_none: i,
|
|
211
|
+
"!any": i,
|
|
212
|
+
"!all": i,
|
|
213
|
+
"!none": i
|
|
214
|
+
}, h = {
|
|
80
215
|
cidr: ["ip"],
|
|
81
216
|
not_cidr: ["ip"],
|
|
82
217
|
"!cidr": ["ip"]
|
|
83
|
-
},
|
|
218
|
+
}, y = {
|
|
84
219
|
eq: ["string", "number", "boolean"],
|
|
85
220
|
ne: ["string", "number", "boolean"],
|
|
86
221
|
"=": ["string", "number", "boolean"],
|
|
87
|
-
"==": ["string", "number", "boolean"],
|
|
88
222
|
"!=": ["string", "number", "boolean"],
|
|
89
223
|
eq_ci: ["string"],
|
|
90
224
|
gt: ["number", "string"],
|
|
@@ -154,17 +288,17 @@ class b {
|
|
|
154
288
|
options;
|
|
155
289
|
errors;
|
|
156
290
|
warnings;
|
|
157
|
-
constructor(
|
|
158
|
-
this.options =
|
|
291
|
+
constructor(t = {}) {
|
|
292
|
+
this.options = t, this.errors = [], this.warnings = [];
|
|
159
293
|
}
|
|
160
294
|
/**
|
|
161
295
|
* Validate a TQL query
|
|
162
296
|
*/
|
|
163
|
-
validate(
|
|
164
|
-
if (this.errors = [], this.warnings = [], !
|
|
297
|
+
validate(t) {
|
|
298
|
+
if (this.errors = [], this.warnings = [], !t)
|
|
165
299
|
return { valid: !0, errors: [], warnings: [] };
|
|
166
|
-
const
|
|
167
|
-
return this.errors.push(...
|
|
300
|
+
const e = w(t);
|
|
301
|
+
return this.errors.push(...e.errors), this.warnings.push(...e.warnings), e.ast && this.validateExpression(e.ast), {
|
|
168
302
|
valid: this.errors.length === 0,
|
|
169
303
|
errors: this.errors,
|
|
170
304
|
warnings: this.warnings
|
|
@@ -173,171 +307,276 @@ class b {
|
|
|
173
307
|
/**
|
|
174
308
|
* Validate an expression recursively
|
|
175
309
|
*/
|
|
176
|
-
validateExpression(
|
|
177
|
-
switch (
|
|
310
|
+
validateExpression(t) {
|
|
311
|
+
switch (t.type) {
|
|
178
312
|
case "binary":
|
|
179
|
-
this.validateExpression(
|
|
313
|
+
this.validateExpression(t.left), this.validateExpression(t.right);
|
|
180
314
|
break;
|
|
181
315
|
case "unary":
|
|
182
|
-
this.validateExpression(
|
|
316
|
+
this.validateExpression(t.operand);
|
|
183
317
|
break;
|
|
184
318
|
case "group":
|
|
185
|
-
this.validateExpression(
|
|
319
|
+
this.validateExpression(t.expression);
|
|
186
320
|
break;
|
|
187
321
|
case "comparison":
|
|
188
|
-
this.validateComparison(
|
|
322
|
+
this.validateComparison(t);
|
|
323
|
+
break;
|
|
324
|
+
// `field ANY value`. The collection wrapper carries no validatable content of its
|
|
325
|
+
// own -- field, inner operator and value all live in `condition` -- but it must be
|
|
326
|
+
// walked explicitly. Falling through the switch would have made every collection
|
|
327
|
+
// query validate vacuously clean, which is the failure mode this node type was added
|
|
328
|
+
// out of, not into.
|
|
329
|
+
//
|
|
330
|
+
// The COLLECTION OPERATOR itself is deliberately not checked here. Every field type
|
|
331
|
+
// is compatible with it (see the note on `any` in OPERATOR_TYPE_COMPATIBILITY), so a
|
|
332
|
+
// check would be a code path that can never produce an answer -- and a mechanism with
|
|
333
|
+
// no reachable outcome reads as enforcement while enforcing nothing. The completion
|
|
334
|
+
// provider's copy of the table IS live, and is where the widened rule is observable.
|
|
335
|
+
//
|
|
336
|
+
// The OPERAND SHAPE is checked, though — see validateCollectionOperand.
|
|
337
|
+
case "collection":
|
|
338
|
+
this.validateCollectionOperand(t), this.validateExpression(t.condition);
|
|
189
339
|
break;
|
|
190
340
|
}
|
|
191
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* `f any ['a','b']` is ill-typed. Say so, and name `in`.
|
|
344
|
+
*
|
|
345
|
+
* ## The rule, as both engines implement it
|
|
346
|
+
*
|
|
347
|
+
* `any` / `none` apply a comparison to each ELEMENT of an array-valued field. The operand
|
|
348
|
+
* is therefore a SCALAR: `f any 'admin'` asks whether some element equals `'admin'`. A
|
|
349
|
+
* LIST operand asks whether some single element equals the whole two-element list, which
|
|
350
|
+
* no scalar element can. `in` is already the membership operator, and is what the author
|
|
351
|
+
* meant.
|
|
352
|
+
*
|
|
353
|
+
* Both engines now refuse it -- `ab55587` in `tql/src/opensearch/query_builder.rs`
|
|
354
|
+
* (`build_collection_op`) and `_refuse_list_operand` in
|
|
355
|
+
* `src/tql/opensearch_components/query_converter.py` -- raising a type error whose
|
|
356
|
+
* `valid_operators` are `in` / `not in`. Before that it emitted `{"term": {"f": [..]}}`,
|
|
357
|
+
* which OpenSearch answers with HTTP 400 `[term] query does not support array of values`,
|
|
358
|
+
* failing the WHOLE search including every unrelated clause; the intermediate fix emitted
|
|
359
|
+
* `match_none`, which executed cleanly and excluded nothing, silently.
|
|
360
|
+
*
|
|
361
|
+
* ## Three arities, and only the middle one is new
|
|
362
|
+
*
|
|
363
|
+
* The engines do NOT test `is_array`. They unwrap a single-element list FIRST
|
|
364
|
+
* (`unwrap_single` in Rust, `len(value) == 1` in Python) and refuse only what is still a
|
|
365
|
+
* list. Measured against the live Python translator at `ab55587`:
|
|
366
|
+
*
|
|
367
|
+
* | Query | Python translator |
|
|
368
|
+
* |--------------------|------------------------------------------------------|
|
|
369
|
+
* | `f any 'admin'` | `{"term": {"f": "admin"}}` |
|
|
370
|
+
* | `f any ['a']` | `{"term": {"f": "a"}}` -- unwrapped, NOT refused |
|
|
371
|
+
* | `f any ['a','b']` | `TQLTypeError`, valid operators `in`, `not in` |
|
|
372
|
+
*
|
|
373
|
+
* A naive `Array.isArray(value)` check would flag `f any ['a']`, which both engines
|
|
374
|
+
* translate perfectly well -- a red squiggle over a working query, which is the defect
|
|
375
|
+
* this parser keeps being fixed for. Hence `length >= 2`, and hence the two arities are
|
|
376
|
+
* tested separately in `tests/collectionListOperand.spec.ts`.
|
|
377
|
+
*
|
|
378
|
+
* The empty list is not handled here: `f any []` is already an `INVALID_VALUE` error from
|
|
379
|
+
* the parser (`tests/emptyValueList.spec.ts`), and Python does not even parse it
|
|
380
|
+
* (`TQLSyntaxError`). Reporting it twice would be noise.
|
|
381
|
+
*
|
|
382
|
+
* ## Why `ALL` is excluded, and why that is not an oversight
|
|
383
|
+
*
|
|
384
|
+
* `all` / `not_all` are answered by a Painless script that compares each element to
|
|
385
|
+
* `params.value`. A list operand there emits valid DSL that matches nothing -- which is
|
|
386
|
+
* what both in-memory evaluators also answer -- so both engines dispatch `all` BEFORE the
|
|
387
|
+
* refusal and leave it untouched. `f all ['a','b']` translating cleanly is a pinned
|
|
388
|
+
* negative case here, not a gap.
|
|
389
|
+
*
|
|
390
|
+
* Negation is irrelevant: in both engines the array check precedes the quantifier wrapper,
|
|
391
|
+
* so `not any` / `not none` refuse on the same footing as the positive spellings.
|
|
392
|
+
*/
|
|
393
|
+
validateCollectionOperand(t) {
|
|
394
|
+
if (t.operator === "ALL")
|
|
395
|
+
return;
|
|
396
|
+
const e = t.condition;
|
|
397
|
+
if (e.type !== "comparison")
|
|
398
|
+
return;
|
|
399
|
+
const n = e.value;
|
|
400
|
+
if (!Array.isArray(n) || n.length < 2)
|
|
401
|
+
return;
|
|
402
|
+
const s = t.operator.toLowerCase(), r = t.negated ? `not ${s}` : s, l = t.field.name;
|
|
403
|
+
this.addError(
|
|
404
|
+
`'${r}' takes a single value and tests it against each element of '${l}'; it does not take a list.`,
|
|
405
|
+
n[0].start,
|
|
406
|
+
n[n.length - 1].end,
|
|
407
|
+
1,
|
|
408
|
+
1,
|
|
409
|
+
"TYPE_MISMATCH",
|
|
410
|
+
`Use \`in\` for membership: \`${l} in [...]\` (or \`${l} not in [...]\` to exclude).`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
192
413
|
/**
|
|
193
414
|
* Validate a comparison expression
|
|
194
415
|
*/
|
|
195
|
-
validateComparison(
|
|
196
|
-
const
|
|
197
|
-
if (this.options.availableFields && this.options.strictMode && !this.options.availableFields.includes(
|
|
198
|
-
`Unknown field '${
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
(
|
|
416
|
+
validateComparison(t) {
|
|
417
|
+
const e = t.field.type === "field" ? t.field.name : "computed";
|
|
418
|
+
if (this.options.availableFields && this.options.strictMode && !this.options.availableFields.includes(e) && t.field.type === "field" && this.addWarning(
|
|
419
|
+
`Unknown field '${e}'`,
|
|
420
|
+
t.field.start,
|
|
421
|
+
t.field.end,
|
|
422
|
+
(t.field.type === "field", 1),
|
|
202
423
|
1,
|
|
203
424
|
"UNKNOWN_FIELD",
|
|
204
|
-
`Did you mean: ${this.suggestField(
|
|
205
|
-
), this.options.fieldTypes &&
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
`Operator '${
|
|
211
|
-
|
|
212
|
-
|
|
425
|
+
`Did you mean: ${this.suggestField(e)}`
|
|
426
|
+
), this.options.fieldTypes && t.field.type === "field") {
|
|
427
|
+
const n = this.options.fieldTypes[e];
|
|
428
|
+
if (n) {
|
|
429
|
+
const s = h[t.operator];
|
|
430
|
+
s && !s.includes(n) && this.addError(
|
|
431
|
+
`Operator '${t.operator}' requires field type ${s.join(" or ")}, but '${e}' is type '${n}'`,
|
|
432
|
+
t.start,
|
|
433
|
+
t.end,
|
|
213
434
|
1,
|
|
214
435
|
1,
|
|
215
436
|
"TYPE_MISMATCH",
|
|
216
|
-
`Use '${
|
|
437
|
+
`Use '${t.operator}' only with ${s.join(" or ")} fields`
|
|
217
438
|
);
|
|
218
439
|
}
|
|
219
440
|
}
|
|
220
|
-
if (this.options.fieldTypes &&
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
const
|
|
224
|
-
!
|
|
225
|
-
`Operator '${
|
|
226
|
-
|
|
227
|
-
|
|
441
|
+
if (this.options.fieldTypes && t.field.type === "field") {
|
|
442
|
+
const n = this.options.fieldTypes[e];
|
|
443
|
+
if (n) {
|
|
444
|
+
const s = f[t.operator];
|
|
445
|
+
!h[t.operator] && s && !s.includes(n) && this.addWarning(
|
|
446
|
+
`Operator '${t.operator}' may not work well with field type '${n}'`,
|
|
447
|
+
t.start,
|
|
448
|
+
t.end,
|
|
228
449
|
1,
|
|
229
450
|
1,
|
|
230
451
|
"TYPE_MISMATCH",
|
|
231
|
-
`Consider using a different operator for ${
|
|
452
|
+
`Consider using a different operator for ${n} fields`
|
|
232
453
|
);
|
|
233
454
|
}
|
|
234
455
|
}
|
|
235
|
-
if (!Array.isArray(
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
`Operator '${
|
|
239
|
-
|
|
240
|
-
|
|
456
|
+
if (!Array.isArray(t.value) && t.value.type === "literal") {
|
|
457
|
+
const n = y[t.operator];
|
|
458
|
+
n && !n.includes(t.value.valueType) && this.addWarning(
|
|
459
|
+
`Operator '${t.operator}' expects ${n.join(" or ")} value`,
|
|
460
|
+
t.value.start,
|
|
461
|
+
t.value.end,
|
|
241
462
|
1,
|
|
242
463
|
1,
|
|
243
464
|
"TYPE_MISMATCH"
|
|
244
465
|
);
|
|
245
466
|
}
|
|
246
|
-
if ((
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
} catch {
|
|
467
|
+
if ((t.operator === "regexp" || t.operator === "regex" || t.operator === "not_regexp" || t.operator === "not_regex" || t.operator === "matches" || t.operator === "not_matches") && !Array.isArray(t.value) && t.value.valueType === "string" && typeof t.value.value == "string") {
|
|
468
|
+
const n = Array.from(t.value.value).length;
|
|
469
|
+
if (n > c)
|
|
250
470
|
this.addError(
|
|
251
|
-
`
|
|
252
|
-
|
|
253
|
-
|
|
471
|
+
`Regex pattern is ${n} characters, which exceeds the maximum supported length of ${c}. The query cannot run as written.`,
|
|
472
|
+
t.value.start,
|
|
473
|
+
t.value.end,
|
|
254
474
|
1,
|
|
255
475
|
1,
|
|
256
476
|
"INVALID_VALUE",
|
|
257
|
-
"
|
|
477
|
+
"Split the pattern into several shorter alternations combined with OR, or use an `in [...]` list"
|
|
258
478
|
);
|
|
259
|
-
|
|
479
|
+
else
|
|
480
|
+
try {
|
|
481
|
+
new RegExp(t.value.value);
|
|
482
|
+
} catch {
|
|
483
|
+
this.addError(
|
|
484
|
+
`Invalid regular expression: ${t.value.value}`,
|
|
485
|
+
t.value.start,
|
|
486
|
+
t.value.end,
|
|
487
|
+
1,
|
|
488
|
+
1,
|
|
489
|
+
"INVALID_VALUE",
|
|
490
|
+
"Check the regex syntax"
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
260
494
|
}
|
|
261
495
|
/**
|
|
262
496
|
* Suggest a similar field name using Levenshtein distance
|
|
263
497
|
*/
|
|
264
|
-
suggestField(
|
|
498
|
+
suggestField(t) {
|
|
265
499
|
if (!this.options.availableFields || this.options.availableFields.length === 0)
|
|
266
500
|
return "check available fields";
|
|
267
|
-
let
|
|
268
|
-
for (const
|
|
269
|
-
const
|
|
270
|
-
|
|
501
|
+
let e = this.options.availableFields[0], n = this.levenshteinDistance(t, e);
|
|
502
|
+
for (const s of this.options.availableFields) {
|
|
503
|
+
const r = this.levenshteinDistance(t, s);
|
|
504
|
+
r < n && (n = r, e = s);
|
|
271
505
|
}
|
|
272
|
-
return
|
|
506
|
+
return e;
|
|
273
507
|
}
|
|
274
508
|
/**
|
|
275
509
|
* Calculate Levenshtein distance between two strings
|
|
276
510
|
*/
|
|
277
|
-
levenshteinDistance(
|
|
278
|
-
const
|
|
279
|
-
for (let
|
|
280
|
-
|
|
281
|
-
for (let
|
|
282
|
-
|
|
283
|
-
for (let
|
|
284
|
-
for (let
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
511
|
+
levenshteinDistance(t, e) {
|
|
512
|
+
const n = [];
|
|
513
|
+
for (let s = 0; s <= e.length; s++)
|
|
514
|
+
n[s] = [s];
|
|
515
|
+
for (let s = 0; s <= t.length; s++)
|
|
516
|
+
n[0][s] = s;
|
|
517
|
+
for (let s = 1; s <= e.length; s++)
|
|
518
|
+
for (let r = 1; r <= t.length; r++)
|
|
519
|
+
e.charAt(s - 1) === t.charAt(r - 1) ? n[s][r] = n[s - 1][r - 1] : n[s][r] = Math.min(
|
|
520
|
+
n[s - 1][r - 1] + 1,
|
|
521
|
+
n[s][r - 1] + 1,
|
|
522
|
+
n[s - 1][r] + 1
|
|
289
523
|
);
|
|
290
|
-
return
|
|
524
|
+
return n[e.length][t.length];
|
|
291
525
|
}
|
|
292
|
-
addError(
|
|
526
|
+
addError(t, e, n, s, r, l, d) {
|
|
293
527
|
this.errors.push({
|
|
294
|
-
message:
|
|
295
|
-
start:
|
|
296
|
-
end:
|
|
297
|
-
line:
|
|
298
|
-
column:
|
|
528
|
+
message: t,
|
|
529
|
+
start: e,
|
|
530
|
+
end: n,
|
|
531
|
+
line: s,
|
|
532
|
+
column: r,
|
|
299
533
|
severity: "error",
|
|
300
|
-
code:
|
|
301
|
-
suggestion:
|
|
534
|
+
code: l,
|
|
535
|
+
suggestion: d
|
|
302
536
|
});
|
|
303
537
|
}
|
|
304
|
-
addWarning(
|
|
538
|
+
addWarning(t, e, n, s, r, l, d) {
|
|
305
539
|
this.warnings.push({
|
|
306
|
-
message:
|
|
307
|
-
start:
|
|
308
|
-
end:
|
|
309
|
-
line:
|
|
310
|
-
column:
|
|
540
|
+
message: t,
|
|
541
|
+
start: e,
|
|
542
|
+
end: n,
|
|
543
|
+
line: s,
|
|
544
|
+
column: r,
|
|
311
545
|
severity: "warning",
|
|
312
|
-
code:
|
|
313
|
-
suggestion:
|
|
546
|
+
code: l,
|
|
547
|
+
suggestion: d
|
|
314
548
|
});
|
|
315
549
|
}
|
|
316
550
|
}
|
|
317
|
-
function
|
|
318
|
-
return new b(
|
|
551
|
+
function _(o, t) {
|
|
552
|
+
return new b(t).validate(o);
|
|
319
553
|
}
|
|
320
|
-
function
|
|
321
|
-
return
|
|
554
|
+
function m(o) {
|
|
555
|
+
return _(o).valid;
|
|
322
556
|
}
|
|
323
|
-
function
|
|
324
|
-
const
|
|
325
|
-
return [...
|
|
557
|
+
function v(o, t) {
|
|
558
|
+
const e = _(o, t);
|
|
559
|
+
return [...e.errors, ...e.warnings];
|
|
326
560
|
}
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
for (const
|
|
331
|
-
|
|
332
|
-
for (const
|
|
333
|
-
|
|
334
|
-
return
|
|
561
|
+
function E(o) {
|
|
562
|
+
const t = [];
|
|
563
|
+
o.valid ? t.push("Query is valid") : t.push("Query has errors:");
|
|
564
|
+
for (const e of o.errors)
|
|
565
|
+
t.push(` Error: ${e.message}`), e.suggestion && t.push(` Suggestion: ${e.suggestion}`);
|
|
566
|
+
for (const e of o.warnings)
|
|
567
|
+
t.push(` Warning: ${e.message}`), e.suggestion && t.push(` Suggestion: ${e.suggestion}`);
|
|
568
|
+
return t.join(`
|
|
335
569
|
`);
|
|
336
570
|
}
|
|
337
571
|
export {
|
|
572
|
+
i as ALL_FIELD_TYPES,
|
|
573
|
+
g as BETWEEN_FIELD_TYPES,
|
|
574
|
+
c as MAX_REGEX_PATTERN_LENGTH,
|
|
575
|
+
u as NUMERIC_FIELD_TYPES,
|
|
576
|
+
a as ORDERED_FIELD_TYPES,
|
|
338
577
|
b as TqlValidator,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
578
|
+
E as formatValidationResult,
|
|
579
|
+
v as getTqlIssues,
|
|
580
|
+
m as isValidTqlSyntax,
|
|
581
|
+
_ as validateTql
|
|
343
582
|
};
|