@webda/ql 4.0.0-beta.5 → 4.0.0-beta.7
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 +72 -31
- package/lib/WebdaQLLexer.d.ts +44 -30
- package/lib/WebdaQLLexer.d.ts.map +1 -1
- package/lib/WebdaQLLexer.js +257 -152
- package/lib/WebdaQLLexer.js.map +1 -1
- package/lib/WebdaQLParserLexer.d.ts +44 -30
- package/lib/WebdaQLParserLexer.d.ts.map +1 -1
- package/lib/WebdaQLParserLexer.js +257 -152
- package/lib/WebdaQLParserLexer.js.map +1 -1
- package/lib/WebdaQLParserListener.d.ts +203 -30
- package/lib/WebdaQLParserListener.d.ts.map +1 -1
- package/lib/WebdaQLParserParser.d.ts +242 -44
- package/lib/WebdaQLParserParser.d.ts.map +1 -1
- package/lib/WebdaQLParserParser.js +2054 -715
- package/lib/WebdaQLParserParser.js.map +1 -1
- package/lib/WebdaQLParserVisitor.d.ts +126 -17
- package/lib/WebdaQLParserVisitor.d.ts.map +1 -1
- package/lib/aggregation-query.d.ts +95 -0
- package/lib/aggregation-query.d.ts.map +1 -0
- package/lib/aggregation-query.js +104 -0
- package/lib/aggregation-query.js.map +1 -0
- package/lib/aggregation-select.spec.d.ts +2 -0
- package/lib/aggregation-select.spec.d.ts.map +1 -0
- package/lib/aggregation-select.spec.js +165 -0
- package/lib/aggregation-select.spec.js.map +1 -0
- package/lib/aggregation.d.ts +97 -0
- package/lib/aggregation.d.ts.map +1 -0
- package/lib/aggregation.js +241 -0
- package/lib/aggregation.js.map +1 -0
- package/lib/aggregation.spec.d.ts +2 -0
- package/lib/aggregation.spec.d.ts.map +1 -0
- package/lib/aggregation.spec.js +360 -0
- package/lib/aggregation.spec.js.map +1 -0
- package/lib/bind.d.ts.map +1 -1
- package/lib/bind.js +29 -0
- package/lib/bind.js.map +1 -1
- package/lib/filter.d.ts +54 -0
- package/lib/filter.d.ts.map +1 -0
- package/lib/filter.js +135 -0
- package/lib/filter.js.map +1 -0
- package/lib/filter.spec.d.ts +2 -0
- package/lib/filter.spec.d.ts.map +1 -0
- package/lib/filter.spec.js +124 -0
- package/lib/filter.spec.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/query.d.ts +173 -12
- package/lib/query.d.ts.map +1 -1
- package/lib/query.js +529 -64
- package/lib/query.js.map +1 -1
- package/lib/query.spec.js +7 -4
- package/lib/query.spec.js.map +1 -1
- package/lib/statements.spec.d.ts +2 -0
- package/lib/statements.spec.d.ts.map +1 -0
- package/lib/statements.spec.js +423 -0
- package/lib/statements.spec.js.map +1 -0
- package/lib/webdaql-string.d.ts +9 -0
- package/lib/webdaql-string.d.ts.map +1 -1
- package/lib/webdaql-string.js +9 -8
- package/lib/webdaql-string.js.map +1 -1
- package/package.json +2 -2
package/lib/query.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { CharStreams, CommonTokenStream } from "antlr4ts";
|
|
2
2
|
import { AbstractParseTreeVisitor, TerminalNode } from "antlr4ts/tree/index.js";
|
|
3
3
|
import { WebdaQLLexer } from "./WebdaQLLexer.js";
|
|
4
|
-
import { OrderFieldExpressionContext, SubExpressionContext, WebdaQLParserParser } from "./WebdaQLParserParser.js";
|
|
5
|
-
import {
|
|
4
|
+
import { CountAllItemContext, CountDistinctItemContext, FieldItemContext, MetricItemContext, OrderFieldExpressionContext, SubExpressionContext, WebdaQLParserParser } from "./WebdaQLParserParser.js";
|
|
5
|
+
import { checkAlias, validateAggregation } from "./aggregation-query.js";
|
|
6
|
+
import { escapeValue, WebdaQLError } from "./webdaql-string.js";
|
|
6
7
|
/**
|
|
7
8
|
* Strip the quotes of a WebdaQL string literal and unescape its contents.
|
|
8
9
|
*
|
|
@@ -58,11 +59,14 @@ export function unescapeStringLiteral(literal) {
|
|
|
58
59
|
* Prepend a condition to an existing query string using AND logic
|
|
59
60
|
*
|
|
60
61
|
* Parses both strings, merges them, and reconstructs the combined query.
|
|
61
|
-
* ORDER BY, LIMIT, and OFFSET clauses
|
|
62
|
+
* The statement head (`SELECT fields`, `DELETE`, `UPDATE SET ...`) and the ORDER BY, LIMIT, and OFFSET clauses
|
|
63
|
+
* of the original query are preserved; the condition joins the WHERE part. Both sides keep their own grouping:
|
|
64
|
+
* `a OR b` merged with `c OR d` is `( a OR b ) AND ( c OR d )`.
|
|
62
65
|
*
|
|
63
|
-
* @param query - existing query string (may include ORDER BY / LIMIT / OFFSET)
|
|
64
|
-
* @param condition - condition to prepend (filter
|
|
66
|
+
* @param query - existing query string (a plain filter or a statement, may include ORDER BY / LIMIT / OFFSET)
|
|
67
|
+
* @param condition - condition to prepend (a plain filter: a statement is refused)
|
|
65
68
|
* @returns merged query string
|
|
69
|
+
* @throws {SyntaxError} if the condition is a statement, or adds ORDER BY / OFFSET to a DELETE or UPDATE
|
|
66
70
|
*
|
|
67
71
|
* @example
|
|
68
72
|
* ```ts
|
|
@@ -99,6 +103,7 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
99
103
|
defaultResult() {
|
|
100
104
|
// An empty AND return true
|
|
101
105
|
return {
|
|
106
|
+
type: "SELECT",
|
|
102
107
|
filter: new AndExpression([])
|
|
103
108
|
};
|
|
104
109
|
}
|
|
@@ -146,41 +151,167 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
146
151
|
* @param ctx - the order expression context
|
|
147
152
|
*/
|
|
148
153
|
visitOrderExpression(ctx) {
|
|
149
|
-
this.orderBy = ctx.children
|
|
150
|
-
|
|
154
|
+
this.orderBy = (ctx.children ?? [])
|
|
155
|
+
.filter(c => c instanceof OrderFieldExpressionContext)
|
|
151
156
|
.map((c) => this.visitOrderFieldExpression(c));
|
|
152
157
|
}
|
|
153
158
|
/**
|
|
154
|
-
* Visit the root `webdaql` rule and build the complete Query
|
|
159
|
+
* Visit the root `webdaql` rule and build the complete Query: a statement or a plain filter query
|
|
160
|
+
* @param ctx - the webdaql parse context
|
|
161
|
+
* @returns the built Query
|
|
162
|
+
*/
|
|
163
|
+
visitWebdaql(ctx) {
|
|
164
|
+
const statement = ctx.statement();
|
|
165
|
+
const filter = ctx.filterQuery();
|
|
166
|
+
return statement ? this.visitStatement(statement) : filter ? this.visitFilterQuery(filter) : this.defaultResult();
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Visit a DELETE, UPDATE or SELECT statement
|
|
170
|
+
* @param ctx - the statement context
|
|
171
|
+
* @returns the built Query
|
|
172
|
+
*/
|
|
173
|
+
visitStatement(ctx) {
|
|
174
|
+
return this.visit(ctx.getChild(0));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Visit a plain filter query: an implicit SELECT of every field
|
|
155
178
|
*
|
|
156
179
|
* Parses filter expression, ORDER BY, LIMIT, and OFFSET clauses.
|
|
157
180
|
* Returns an empty AND expression (always true) when no filter is present.
|
|
158
|
-
* @param ctx - the
|
|
181
|
+
* @param ctx - the filter query context
|
|
159
182
|
* @returns the built Query
|
|
160
183
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
visitFilterQuery(ctx) {
|
|
185
|
+
return this.buildQuery("SELECT", ctx.expression(), ctx);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Visit `DELETE [WHERE ...] [LIMIT n]`
|
|
189
|
+
* @param ctx - the DELETE statement context
|
|
190
|
+
* @returns the built Query
|
|
191
|
+
*/
|
|
192
|
+
visitDeleteStatement(ctx) {
|
|
193
|
+
return this.buildQuery("DELETE", ctx.whereClause()?.expression(), ctx);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Visit `UPDATE SET a = v, ... [WHERE ...] [LIMIT n]`
|
|
197
|
+
* @param ctx - the UPDATE statement context
|
|
198
|
+
* @returns the built Query
|
|
199
|
+
*/
|
|
200
|
+
visitUpdateStatement(ctx) {
|
|
201
|
+
return {
|
|
202
|
+
...this.buildQuery("UPDATE", ctx.whereClause()?.expression(), ctx),
|
|
203
|
+
assignments: ctx
|
|
204
|
+
.assignmentList()
|
|
205
|
+
.assignment()
|
|
206
|
+
.map(a => this.visitAssignment(a))
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Visit a SELECT statement: a field list, or an aggregation when it has metrics or a GROUP BY
|
|
211
|
+
* @param ctx - the select statement context
|
|
212
|
+
* @returns the query
|
|
213
|
+
*/
|
|
214
|
+
visitSelectStatement(ctx) {
|
|
215
|
+
const query = { ...this.buildQuery("SELECT", ctx.whereClause()?.expression(), ctx) };
|
|
216
|
+
const { fields, metrics } = this.visitFieldList(ctx.fieldList());
|
|
217
|
+
query.fields = fields;
|
|
218
|
+
const groupBy = ctx
|
|
219
|
+
.groupByExpression()
|
|
220
|
+
?.identifier()
|
|
221
|
+
.map(identifier => identifier.text);
|
|
222
|
+
if (!Object.keys(metrics).length && groupBy === undefined) {
|
|
223
|
+
return query;
|
|
224
|
+
}
|
|
225
|
+
if (query.continuationToken !== undefined) {
|
|
226
|
+
throw new WebdaQLError("OFFSET is not supported with GROUP BY or aggregate functions");
|
|
167
227
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
228
|
+
const by = groupBy ?? [];
|
|
229
|
+
if (fields.length !== by.length || fields.some(field => !by.includes(field))) {
|
|
230
|
+
throw new WebdaQLError("With aggregate functions, the selected fields must be exactly the GROUP BY fields");
|
|
231
|
+
}
|
|
232
|
+
query.aggregation = validateAggregation({
|
|
233
|
+
filter: query.filter,
|
|
234
|
+
groupBy: by,
|
|
235
|
+
metrics,
|
|
236
|
+
orderBy: query.orderBy?.map(order => ({ key: order.field, direction: order.direction })),
|
|
237
|
+
limit: query.limit
|
|
238
|
+
});
|
|
239
|
+
return query;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Read one `field = value` of an UPDATE SET
|
|
243
|
+
* @param ctx - the assignment context
|
|
244
|
+
* @returns the assignment
|
|
245
|
+
*/
|
|
246
|
+
visitAssignment(ctx) {
|
|
247
|
+
return {
|
|
248
|
+
field: ctx.identifier().text,
|
|
249
|
+
value: this.visit(ctx.getChild(2))
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Read the field list of a SELECT: plain fields and aggregate metrics
|
|
254
|
+
* @param ctx - the field list context
|
|
255
|
+
* @returns the field paths and the metrics by alias
|
|
256
|
+
*/
|
|
257
|
+
visitFieldList(ctx) {
|
|
258
|
+
const fields = [];
|
|
259
|
+
const metrics = {};
|
|
260
|
+
const add = (alias, metric) => {
|
|
261
|
+
// Before assigning: a `__proto__` alias would otherwise change the prototype of `metrics`
|
|
262
|
+
checkAlias(alias);
|
|
263
|
+
if (Object.prototype.hasOwnProperty.call(metrics, alias)) {
|
|
264
|
+
throw new WebdaQLError(`Duplicate metric alias '${alias}'`);
|
|
265
|
+
}
|
|
266
|
+
metrics[alias] = metric;
|
|
267
|
+
};
|
|
268
|
+
for (const item of ctx.selectItem()) {
|
|
269
|
+
if (item instanceof FieldItemContext) {
|
|
270
|
+
fields.push(item.identifier().text);
|
|
271
|
+
}
|
|
272
|
+
else if (item instanceof CountAllItemContext) {
|
|
273
|
+
add(item.identifier().text, { fn: "COUNT" });
|
|
274
|
+
}
|
|
275
|
+
else if (item instanceof CountDistinctItemContext) {
|
|
276
|
+
add(item.identifier(1).text, { fn: "COUNT_DISTINCT", field: item.identifier(0).text });
|
|
277
|
+
}
|
|
278
|
+
else if (item instanceof MetricItemContext) {
|
|
279
|
+
add(item.identifier(1).text, {
|
|
280
|
+
fn: item.getChild(0).text,
|
|
281
|
+
field: item.identifier(0).text
|
|
282
|
+
});
|
|
283
|
+
}
|
|
171
284
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
285
|
+
return { fields, metrics };
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Visit a parenthesised expression: the inner expression
|
|
289
|
+
* @param ctx - the sub expression context
|
|
290
|
+
* @returns the inner expression
|
|
291
|
+
*/
|
|
292
|
+
visitSubExpression(ctx) {
|
|
293
|
+
return this.visit(ctx.expression());
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Build the Query of a statement or filter query: its condition and its ORDER BY, LIMIT and OFFSET clauses
|
|
297
|
+
* @param type - the statement type
|
|
298
|
+
* @param expression - the condition, if any
|
|
299
|
+
* @param ctx - the statement context holding the optional clauses
|
|
300
|
+
* @returns the Query
|
|
301
|
+
*/
|
|
302
|
+
buildQuery(type, expression, ctx) {
|
|
303
|
+
for (const clause of [
|
|
304
|
+
"orderExpression" in ctx ? ctx.orderExpression() : undefined,
|
|
305
|
+
ctx.limitExpression(),
|
|
306
|
+
"offsetExpression" in ctx ? ctx.offsetExpression() : undefined
|
|
307
|
+
]) {
|
|
308
|
+
if (clause) {
|
|
309
|
+
this.visit(clause);
|
|
310
|
+
}
|
|
180
311
|
}
|
|
181
|
-
// Go down one level - if expression empty it means no expression were provided
|
|
182
312
|
return {
|
|
183
|
-
|
|
313
|
+
type,
|
|
314
|
+
filter: normalizeFilter((expression ? this.visit(expression) : undefined) || new AndExpression([])),
|
|
184
315
|
limit: this.limit,
|
|
185
316
|
continuationToken: this.offset,
|
|
186
317
|
orderBy: this.orderBy
|
|
@@ -199,7 +330,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
199
330
|
getComparison(ctx) {
|
|
200
331
|
const res = [];
|
|
201
332
|
// eslint-disable-next-line prefer-const
|
|
202
|
-
let
|
|
333
|
+
let left = ctx.getChild(0);
|
|
334
|
+
let right = ctx.getChild(2);
|
|
203
335
|
if (right instanceof SubExpressionContext) {
|
|
204
336
|
right = right.getChild(1);
|
|
205
337
|
}
|
|
@@ -237,7 +369,9 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
237
369
|
* @returns the comparison expression
|
|
238
370
|
*/
|
|
239
371
|
visitBinaryComparisonExpression(ctx) {
|
|
240
|
-
const
|
|
372
|
+
const left = ctx.getChild(0);
|
|
373
|
+
const op = ctx.getChild(1);
|
|
374
|
+
const right = ctx.getChild(2);
|
|
241
375
|
// @ts-ignore
|
|
242
376
|
return new ComparisonExpression(op.text, left.text, this.visit(right));
|
|
243
377
|
}
|
|
@@ -247,7 +381,7 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
247
381
|
* @returns the array of values
|
|
248
382
|
*/
|
|
249
383
|
visitSetExpression(ctx) {
|
|
250
|
-
return ctx.children.filter((_i, id) => id % 2).map(c => this.visit(c));
|
|
384
|
+
return (ctx.children ?? []).filter((_i, id) => id % 2).map(c => this.visit(c));
|
|
251
385
|
}
|
|
252
386
|
/**
|
|
253
387
|
* Visit a LIKE expression (e.g. `field LIKE '%pattern_'`)
|
|
@@ -255,7 +389,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
255
389
|
* @returns the comparison expression
|
|
256
390
|
*/
|
|
257
391
|
visitLikeExpression(ctx) {
|
|
258
|
-
const
|
|
392
|
+
const left = ctx.getChild(0);
|
|
393
|
+
const right = ctx.getChild(2);
|
|
259
394
|
const value = this.visit(right);
|
|
260
395
|
return new ComparisonExpression("LIKE", left.text, value);
|
|
261
396
|
}
|
|
@@ -265,7 +400,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
265
400
|
* @returns the comparison expression
|
|
266
401
|
*/
|
|
267
402
|
visitInExpression(ctx) {
|
|
268
|
-
const
|
|
403
|
+
const left = ctx.getChild(0);
|
|
404
|
+
const right = ctx.getChild(2);
|
|
269
405
|
const value = this.visit(right);
|
|
270
406
|
return new ComparisonExpression("IN", left.text, value);
|
|
271
407
|
}
|
|
@@ -275,7 +411,8 @@ export class ExpressionBuilder extends AbstractParseTreeVisitor {
|
|
|
275
411
|
* @returns the comparison expression
|
|
276
412
|
*/
|
|
277
413
|
visitContainsExpression(ctx) {
|
|
278
|
-
const
|
|
414
|
+
const left = ctx.getChild(0);
|
|
415
|
+
const right = ctx.getChild(2);
|
|
279
416
|
const value = this.visit(right);
|
|
280
417
|
return new ComparisonExpression("CONTAINS", left.text, value);
|
|
281
418
|
}
|
|
@@ -468,19 +605,27 @@ export class ComparisonExpression extends Expression {
|
|
|
468
605
|
* @override
|
|
469
606
|
*/
|
|
470
607
|
eval(target) {
|
|
471
|
-
|
|
608
|
+
let left = ComparisonExpression.getAttributeValue(target, this.attribute);
|
|
609
|
+
// WebdaQL has no date literal: a date compared with a string compares as its ISO 8601 form, the way it is
|
|
610
|
+
// serialized; comparisons with numbers keep the native Date behavior
|
|
611
|
+
if (left instanceof Date &&
|
|
612
|
+
!isNaN(left.getTime()) &&
|
|
613
|
+
this.operator !== "LIKE" &&
|
|
614
|
+
(typeof this.value === "string" || (Array.isArray(this.value) && this.value.some(v => typeof v === "string")))) {
|
|
615
|
+
left = left.toISOString();
|
|
616
|
+
}
|
|
472
617
|
switch (this.operator) {
|
|
473
618
|
case "=":
|
|
474
619
|
// ignore strong type on purpose
|
|
475
620
|
return left == this.value;
|
|
476
621
|
case "<=":
|
|
477
|
-
return left <= this.value;
|
|
622
|
+
return left <= (this.value ?? 0);
|
|
478
623
|
case ">=":
|
|
479
|
-
return left >= this.value;
|
|
624
|
+
return left >= (this.value ?? 0);
|
|
480
625
|
case "<":
|
|
481
|
-
return left < this.value;
|
|
626
|
+
return left < (this.value ?? 0);
|
|
482
627
|
case ">":
|
|
483
|
-
return left > this.value;
|
|
628
|
+
return left > (this.value ?? 0);
|
|
484
629
|
case "!=":
|
|
485
630
|
return left != this.value;
|
|
486
631
|
case "LIKE":
|
|
@@ -522,7 +667,7 @@ export class ComparisonExpression extends Expression {
|
|
|
522
667
|
case "boolean":
|
|
523
668
|
return value.toString().toUpperCase();
|
|
524
669
|
}
|
|
525
|
-
return value
|
|
670
|
+
return value === null ? "NULL" : value.toString();
|
|
526
671
|
}
|
|
527
672
|
/**
|
|
528
673
|
* Allow subclass to create different display
|
|
@@ -801,17 +946,7 @@ export class QueryValidator {
|
|
|
801
946
|
* @returns the reconstructed query string
|
|
802
947
|
*/
|
|
803
948
|
toString() {
|
|
804
|
-
|
|
805
|
-
if (this.query.orderBy) {
|
|
806
|
-
res += ` ORDER BY ${this.query.orderBy.map(o => `${o.field} ${o.direction}`).join(", ")}`;
|
|
807
|
-
}
|
|
808
|
-
if (this.query.limit) {
|
|
809
|
-
res += ` LIMIT ${this.query.limit}`;
|
|
810
|
-
}
|
|
811
|
-
if (this.query.continuationToken) {
|
|
812
|
-
res += ` OFFSET "${this.query.continuationToken}"`;
|
|
813
|
-
}
|
|
814
|
-
return res.trim();
|
|
949
|
+
return stringifyQuery(this.query);
|
|
815
950
|
}
|
|
816
951
|
/**
|
|
817
952
|
* Merge another query string into this query
|
|
@@ -823,8 +958,15 @@ export class QueryValidator {
|
|
|
823
958
|
* @param type - logical operator to combine filters (`"AND"` or `"OR"`)
|
|
824
959
|
* @returns `this` for chaining
|
|
825
960
|
*/
|
|
826
|
-
merge(query, type = "AND") {
|
|
961
|
+
merge(query = "", type = "AND") {
|
|
827
962
|
const adds = new QueryValidator(query);
|
|
963
|
+
if (adds.query.type !== "SELECT" || adds.query.fields) {
|
|
964
|
+
throw new SyntaxError(`Only a filter can be merged into a query, not a statement (Query: ${query})`);
|
|
965
|
+
}
|
|
966
|
+
if ((this.query.type === "DELETE" || this.query.type === "UPDATE") &&
|
|
967
|
+
(adds.query.orderBy || adds.query.continuationToken)) {
|
|
968
|
+
throw new SyntaxError(`${this.query.type} statements take no ORDER BY or OFFSET (Query: ${query})`);
|
|
969
|
+
}
|
|
828
970
|
// Add additional conditions
|
|
829
971
|
if (adds.hasCondition()) {
|
|
830
972
|
if ((this.query.filter instanceof AndExpression && type === "AND") ||
|
|
@@ -836,7 +978,7 @@ export class QueryValidator {
|
|
|
836
978
|
}
|
|
837
979
|
}
|
|
838
980
|
// Set the limit if overriden
|
|
839
|
-
if (adds.query.limit) {
|
|
981
|
+
if (adds.query.limit !== undefined) {
|
|
840
982
|
this.query.limit = adds.query.limit;
|
|
841
983
|
}
|
|
842
984
|
// Set the offset if overriden
|
|
@@ -871,11 +1013,10 @@ export class QueryValidator {
|
|
|
871
1013
|
* @returns the full query object
|
|
872
1014
|
*/
|
|
873
1015
|
getQuery() {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
};
|
|
1016
|
+
const query = { ...this.query };
|
|
1017
|
+
// Print the current state of the query, so a caller changing its LIMIT or OFFSET prints the new values
|
|
1018
|
+
query.toString = () => stringifyQuery(query);
|
|
1019
|
+
return query;
|
|
879
1020
|
}
|
|
880
1021
|
/**
|
|
881
1022
|
* Verify if a target fit the expression
|
|
@@ -1080,7 +1221,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1080
1221
|
* @returns the partial comparison expression
|
|
1081
1222
|
*/
|
|
1082
1223
|
visitLikeExpression(ctx) {
|
|
1083
|
-
const
|
|
1224
|
+
const left = ctx.getChild(0);
|
|
1225
|
+
const right = ctx.getChild(2);
|
|
1084
1226
|
const value = this.visit(right);
|
|
1085
1227
|
return new PartialComparisonExpression(this, "LIKE", left.text, value);
|
|
1086
1228
|
}
|
|
@@ -1090,7 +1232,9 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1090
1232
|
* @returns the partial comparison expression
|
|
1091
1233
|
*/
|
|
1092
1234
|
visitBinaryComparisonExpression(ctx) {
|
|
1093
|
-
const
|
|
1235
|
+
const left = ctx.getChild(0);
|
|
1236
|
+
const op = ctx.getChild(1);
|
|
1237
|
+
const right = ctx.getChild(2);
|
|
1094
1238
|
// @ts-ignore
|
|
1095
1239
|
return new PartialComparisonExpression(this, op.text, left.text, this.visit(right));
|
|
1096
1240
|
}
|
|
@@ -1100,7 +1244,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1100
1244
|
* @returns the partial comparison expression
|
|
1101
1245
|
*/
|
|
1102
1246
|
visitInExpression(ctx) {
|
|
1103
|
-
const
|
|
1247
|
+
const left = ctx.getChild(0);
|
|
1248
|
+
const right = ctx.getChild(2);
|
|
1104
1249
|
const value = this.visit(right);
|
|
1105
1250
|
return new PartialComparisonExpression(this, "IN", left.text, value);
|
|
1106
1251
|
}
|
|
@@ -1110,7 +1255,8 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1110
1255
|
* @returns the partial comparison expression
|
|
1111
1256
|
*/
|
|
1112
1257
|
visitContainsExpression(ctx) {
|
|
1113
|
-
const
|
|
1258
|
+
const left = ctx.getChild(0);
|
|
1259
|
+
const right = ctx.getChild(2);
|
|
1114
1260
|
const value = this.visit(right);
|
|
1115
1261
|
return new PartialComparisonExpression(this, "CONTAINS", left.text, value);
|
|
1116
1262
|
}
|
|
@@ -1139,12 +1285,331 @@ export class PartialExpressionBuilder extends ExpressionBuilder {
|
|
|
1139
1285
|
export function unsanitize(query) {
|
|
1140
1286
|
return query.replace(/</g, "<").replace(/>/g, ">");
|
|
1141
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Print a query in its canonical form, which parses back to the same query
|
|
1290
|
+
*
|
|
1291
|
+
* - plain filter: `<filter> [ORDER BY ...] [LIMIT n] [OFFSET "token"]`
|
|
1292
|
+
* - `SELECT f1, f2 [WHERE <filter>] [ORDER BY ...] [LIMIT n] [OFFSET "token"]`
|
|
1293
|
+
* - `DELETE [WHERE <filter>] [LIMIT n]`
|
|
1294
|
+
* - `UPDATE SET a = v, ... [WHERE <filter>] [LIMIT n]`
|
|
1295
|
+
*
|
|
1296
|
+
* @param query - the query
|
|
1297
|
+
* @returns the query string
|
|
1298
|
+
*/
|
|
1299
|
+
export function stringifyQuery(query) {
|
|
1300
|
+
const filter = query.filter.toString();
|
|
1301
|
+
let head = "";
|
|
1302
|
+
if (query.type === "DELETE") {
|
|
1303
|
+
head = "DELETE";
|
|
1304
|
+
}
|
|
1305
|
+
else if (query.type === "UPDATE") {
|
|
1306
|
+
head = `UPDATE SET ${(query.assignments ?? []).map(a => new ComparisonExpression("=", a.field, a.value).toString()).join(", ")}`;
|
|
1307
|
+
}
|
|
1308
|
+
else if (query.aggregation) {
|
|
1309
|
+
const metrics = Object.entries(query.aggregation.metrics).map(([alias, metric]) => {
|
|
1310
|
+
if (metric.fn === "COUNT_DISTINCT") {
|
|
1311
|
+
return `COUNT(DISTINCT ${metric.field}) AS ${alias}`;
|
|
1312
|
+
}
|
|
1313
|
+
return `${metric.fn}(${metric.field ?? "*"}) AS ${alias}`;
|
|
1314
|
+
});
|
|
1315
|
+
head = `SELECT ${[...query.aggregation.groupBy, ...metrics].join(", ")}`;
|
|
1316
|
+
}
|
|
1317
|
+
else if (query.fields) {
|
|
1318
|
+
head = `SELECT ${query.fields.join(", ")}`;
|
|
1319
|
+
}
|
|
1320
|
+
let res = head ? `${head}${filter ? ` WHERE ${filter}` : ""}` : filter;
|
|
1321
|
+
if (query.aggregation?.groupBy.length) {
|
|
1322
|
+
res += ` GROUP BY ${query.aggregation.groupBy.join(", ")}`;
|
|
1323
|
+
}
|
|
1324
|
+
if (query.orderBy?.length) {
|
|
1325
|
+
res += ` ORDER BY ${query.orderBy.map(o => `${o.field} ${o.direction}`).join(", ")}`;
|
|
1326
|
+
}
|
|
1327
|
+
if (query.limit !== undefined) {
|
|
1328
|
+
res += ` LIMIT ${query.limit}`;
|
|
1329
|
+
}
|
|
1330
|
+
if (query.continuationToken) {
|
|
1331
|
+
res += ` OFFSET ${ComparisonExpression.prototype.toStringValue(query.continuationToken)}`;
|
|
1332
|
+
}
|
|
1333
|
+
return res.trim();
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* One segment of a field path, as the grammar's identifiers allow it: letters, digits and `_`
|
|
1337
|
+
*/
|
|
1338
|
+
const FIELD_SEGMENT = /^[A-Za-z0-9_]+$/;
|
|
1339
|
+
/**
|
|
1340
|
+
* Check a field path of a query object
|
|
1341
|
+
* @param field - the path, as a string or its segments
|
|
1342
|
+
* @returns the dotted path
|
|
1343
|
+
* @throws {WebdaQLError} when a segment is empty or holds anything but letters, digits and `_`
|
|
1344
|
+
*/
|
|
1345
|
+
export function checkFieldPath(field) {
|
|
1346
|
+
// Copy first: what is checked is what is printed, whatever the object does on later reads
|
|
1347
|
+
const segments = Array.isArray(field) ? snapshot(field) : typeof field === "string" ? field.split(".") : undefined;
|
|
1348
|
+
if (!segments?.length || segments.some(segment => typeof segment !== "string" || !FIELD_SEGMENT.test(segment))) {
|
|
1349
|
+
throw new WebdaQLError("Invalid field path in WebdaQL query");
|
|
1350
|
+
}
|
|
1351
|
+
return segments.join(".");
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Copy an array of a query object into a plain array, reading each element once
|
|
1355
|
+
* @param value - the array
|
|
1356
|
+
* @returns the copy
|
|
1357
|
+
*/
|
|
1358
|
+
function snapshot(value) {
|
|
1359
|
+
const length = value.length;
|
|
1360
|
+
const copy = [];
|
|
1361
|
+
for (let i = 0; i < length; i++) {
|
|
1362
|
+
copy.push(value[i]);
|
|
1363
|
+
}
|
|
1364
|
+
return copy;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Print a single value of a query object
|
|
1368
|
+
* @param value - the value
|
|
1369
|
+
* @returns its WebdaQL literal
|
|
1370
|
+
* @throws {WebdaQLError} for anything but a string, a finite number or a boolean
|
|
1371
|
+
*/
|
|
1372
|
+
function checkScalar(value) {
|
|
1373
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
1374
|
+
throw new WebdaQLError(`Invalid value in WebdaQL query: ${typeof value}`);
|
|
1375
|
+
}
|
|
1376
|
+
return escapeValue(value);
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Print an expression of a query object from its data only (never its own toString); every property and array is
|
|
1380
|
+
* read once, so what is checked is what is printed
|
|
1381
|
+
* @param expression - the expression
|
|
1382
|
+
* @param nested - whether the expression is inside a logical expression
|
|
1383
|
+
* @returns the WebdaQL text
|
|
1384
|
+
* @throws {WebdaQLError} for an expression the grammar cannot produce
|
|
1385
|
+
*/
|
|
1386
|
+
function printExpression(expression, nested) {
|
|
1387
|
+
if (expression instanceof BooleanExpression) {
|
|
1388
|
+
const value = expression.value;
|
|
1389
|
+
if (typeof value === "boolean") {
|
|
1390
|
+
return value ? "TRUE" : "FALSE";
|
|
1391
|
+
}
|
|
1392
|
+
throw new WebdaQLError("Invalid expression in WebdaQL query");
|
|
1393
|
+
}
|
|
1394
|
+
if (expression instanceof AndExpression || expression instanceof OrExpression) {
|
|
1395
|
+
const operator = expression instanceof AndExpression ? "AND" : "OR";
|
|
1396
|
+
const children = expression.children;
|
|
1397
|
+
if (!Array.isArray(children)) {
|
|
1398
|
+
throw new WebdaQLError("Invalid expression in WebdaQL query");
|
|
1399
|
+
}
|
|
1400
|
+
const copy = snapshot(children);
|
|
1401
|
+
if (copy.length === 0) {
|
|
1402
|
+
// An empty AND or OR matches everything
|
|
1403
|
+
return nested ? "TRUE" : "";
|
|
1404
|
+
}
|
|
1405
|
+
const text = copy.map(child => printExpression(child, true)).join(` ${operator} `);
|
|
1406
|
+
return nested ? `( ${text} )` : text;
|
|
1407
|
+
}
|
|
1408
|
+
if (expression instanceof ComparisonExpression) {
|
|
1409
|
+
const attribute = checkFieldPath(expression.attribute);
|
|
1410
|
+
const operator = expression.operator;
|
|
1411
|
+
const value = expression.value;
|
|
1412
|
+
if (typeof operator !== "string") {
|
|
1413
|
+
throw new WebdaQLError("Invalid operator in WebdaQL query");
|
|
1414
|
+
}
|
|
1415
|
+
if (UNARY_COMPARISON_OPERATORS.includes(operator)) {
|
|
1416
|
+
return `${attribute} ${operator}`;
|
|
1417
|
+
}
|
|
1418
|
+
if (operator === "IN") {
|
|
1419
|
+
const values = Array.isArray(value) ? snapshot(value) : undefined;
|
|
1420
|
+
if (!values?.length) {
|
|
1421
|
+
throw new WebdaQLError("Invalid IN values in WebdaQL query");
|
|
1422
|
+
}
|
|
1423
|
+
return `${attribute} IN [${values.map(checkScalar).join(", ")}]`;
|
|
1424
|
+
}
|
|
1425
|
+
if (!["=", "!=", "<", "<=", ">", ">=", "LIKE", "CONTAINS"].includes(operator)) {
|
|
1426
|
+
throw new WebdaQLError(`Invalid operator in WebdaQL query: ${JSON.stringify(operator)}`);
|
|
1427
|
+
}
|
|
1428
|
+
if ((operator === "LIKE" || operator === "CONTAINS") && typeof value !== "string") {
|
|
1429
|
+
throw new WebdaQLError(`${operator} takes a string in WebdaQL query`);
|
|
1430
|
+
}
|
|
1431
|
+
return `${attribute} ${operator} ${checkScalar(value)}`;
|
|
1432
|
+
}
|
|
1433
|
+
throw new WebdaQLError("Invalid expression in WebdaQL query");
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Normalize a query object built or changed by code: every part is checked against what the grammar allows, the
|
|
1437
|
+
* query is printed from its data and parsed again
|
|
1438
|
+
*
|
|
1439
|
+
* Use it before handing a `Query` object received from a caller to a backend: field paths (letters, digits, `_`),
|
|
1440
|
+
* operators, values (strings, finite numbers, booleans), LIMIT (non-negative integer) and OFFSET (string) are
|
|
1441
|
+
* checked, and the result only holds expressions built by the parser.
|
|
1442
|
+
*
|
|
1443
|
+
* @param query - the query object
|
|
1444
|
+
* @returns the same query, as parsed from its canonical text
|
|
1445
|
+
* @throws {WebdaQLError} when a part of the query cannot be written in WebdaQL
|
|
1446
|
+
*/
|
|
1447
|
+
export function normalizeQuery(query) {
|
|
1448
|
+
if (!query || typeof query !== "object") {
|
|
1449
|
+
throw new WebdaQLError("Invalid WebdaQL query object");
|
|
1450
|
+
}
|
|
1451
|
+
const type = query.type ?? "SELECT";
|
|
1452
|
+
if (typeof type !== "string" || !["SELECT", "DELETE", "UPDATE"].includes(type)) {
|
|
1453
|
+
throw new WebdaQLError(`Invalid statement type in WebdaQL query: ${JSON.stringify(type)}`);
|
|
1454
|
+
}
|
|
1455
|
+
if (query.aggregation !== undefined) {
|
|
1456
|
+
throw new WebdaQLError("Aggregation queries cannot be normalized: use toAggregationQuery");
|
|
1457
|
+
}
|
|
1458
|
+
const filterObject = query.filter;
|
|
1459
|
+
const filter = printExpression(filterObject ?? new AndExpression([]), false);
|
|
1460
|
+
let head = "";
|
|
1461
|
+
if (type === "DELETE") {
|
|
1462
|
+
head = "DELETE";
|
|
1463
|
+
}
|
|
1464
|
+
else if (type === "UPDATE") {
|
|
1465
|
+
const assignments = Array.isArray(query.assignments) ? snapshot(query.assignments) : [];
|
|
1466
|
+
if (assignments.length === 0) {
|
|
1467
|
+
throw new WebdaQLError("UPDATE needs at least one SET assignment");
|
|
1468
|
+
}
|
|
1469
|
+
head = `UPDATE SET ${assignments
|
|
1470
|
+
.map(a => {
|
|
1471
|
+
const field = a?.field;
|
|
1472
|
+
const value = a?.value;
|
|
1473
|
+
return `${checkFieldPath(field)} = ${checkScalar(value)}`;
|
|
1474
|
+
})
|
|
1475
|
+
.join(", ")}`;
|
|
1476
|
+
}
|
|
1477
|
+
else if (query.fields !== undefined) {
|
|
1478
|
+
const fields = Array.isArray(query.fields) ? snapshot(query.fields) : [];
|
|
1479
|
+
if (fields.length === 0) {
|
|
1480
|
+
throw new WebdaQLError("Invalid SELECT field list in WebdaQL query");
|
|
1481
|
+
}
|
|
1482
|
+
head = `SELECT ${fields.map(checkFieldPath).join(", ")}`;
|
|
1483
|
+
}
|
|
1484
|
+
let res = head ? `${head}${filter ? ` WHERE ${filter}` : ""}` : filter;
|
|
1485
|
+
const orderBy = query.orderBy;
|
|
1486
|
+
if (orderBy !== undefined) {
|
|
1487
|
+
if (!Array.isArray(orderBy)) {
|
|
1488
|
+
throw new WebdaQLError("Invalid ORDER BY in WebdaQL query");
|
|
1489
|
+
}
|
|
1490
|
+
const orders = snapshot(orderBy);
|
|
1491
|
+
if (orders.length) {
|
|
1492
|
+
res += ` ORDER BY ${orders
|
|
1493
|
+
.map(o => {
|
|
1494
|
+
const field = o?.field;
|
|
1495
|
+
const direction = o?.direction;
|
|
1496
|
+
if (direction !== "ASC" && direction !== "DESC") {
|
|
1497
|
+
throw new WebdaQLError("Invalid ORDER BY direction in WebdaQL query");
|
|
1498
|
+
}
|
|
1499
|
+
return `${checkFieldPath(field)} ${direction}`;
|
|
1500
|
+
})
|
|
1501
|
+
.join(", ")}`;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
const limit = query.limit;
|
|
1505
|
+
if (limit !== undefined) {
|
|
1506
|
+
if (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 0) {
|
|
1507
|
+
throw new WebdaQLError("Invalid LIMIT in WebdaQL query");
|
|
1508
|
+
}
|
|
1509
|
+
res += ` LIMIT ${limit}`;
|
|
1510
|
+
}
|
|
1511
|
+
const token = query.continuationToken;
|
|
1512
|
+
if (token !== undefined && token !== null) {
|
|
1513
|
+
if (typeof token !== "string") {
|
|
1514
|
+
throw new WebdaQLError("Invalid OFFSET in WebdaQL query");
|
|
1515
|
+
}
|
|
1516
|
+
if (token) {
|
|
1517
|
+
res += ` OFFSET ${escapeValue(token)}`;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
try {
|
|
1521
|
+
return parse(res.trim());
|
|
1522
|
+
}
|
|
1523
|
+
catch (err) {
|
|
1524
|
+
throw new WebdaQLError(`Invalid WebdaQL query object: ${err.message}`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Whether a field path is allowed: listed itself, or nested in a listed field (`address.city` with `address`)
|
|
1529
|
+
* @param field - the field path
|
|
1530
|
+
* @param allowed - the allowed field paths
|
|
1531
|
+
* @returns true when allowed
|
|
1532
|
+
*/
|
|
1533
|
+
function isAllowedField(field, allowed) {
|
|
1534
|
+
const segments = field.split(".");
|
|
1535
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
1536
|
+
if (allowed.has(segments.slice(0, i).join("."))) {
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Validate the SELECT fields, the aggregation metric fields and the UPDATE SET targets of a query against the
|
|
1544
|
+
* allowed fields
|
|
1545
|
+
*
|
|
1546
|
+
* A dotted path is allowed when it is listed, or when one of its parents is (`profile.email` is allowed by
|
|
1547
|
+
* `profile` or by `profile.email`). GROUP BY paths are the SELECT fields; the field of every aggregation metric
|
|
1548
|
+
* (`SUM(points)`) is checked too, `COUNT(*)` has none. Filter attributes are not checked.
|
|
1549
|
+
*
|
|
1550
|
+
* @param query - parsed query to validate
|
|
1551
|
+
* @param allowedFields - allowed field paths (dot-notation)
|
|
1552
|
+
* @throws {SyntaxError} if a field, a metric field or an assignment target is not allowed
|
|
1553
|
+
*/
|
|
1554
|
+
export function validateQueryFields(query, allowedFields) {
|
|
1555
|
+
const allowed = new Set(allowedFields);
|
|
1556
|
+
for (const field of query.fields ?? []) {
|
|
1557
|
+
if (!isAllowedField(field, allowed)) {
|
|
1558
|
+
throw new SyntaxError(`Unknown field "${field}". Allowed fields: ${allowedFields.join(", ")}`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
for (const { field } of query.assignments ?? []) {
|
|
1562
|
+
if (!isAllowedField(field, allowed)) {
|
|
1563
|
+
throw new SyntaxError(`Unknown assignment field "${field}". Allowed fields: ${allowedFields.join(", ")}`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
for (const [alias, metric] of Object.entries(query.aggregation?.metrics ?? {})) {
|
|
1567
|
+
if (metric.field !== undefined && !isAllowedField(metric.field, allowed)) {
|
|
1568
|
+
throw new SyntaxError(`Unknown field "${metric.field}" in "${alias}". Allowed fields: ${allowedFields.join(", ")}`);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Refuse anything but a plain filter query: DELETE, UPDATE and SELECT with a field list
|
|
1574
|
+
*
|
|
1575
|
+
* Used where a query only selects objects, such as the Query operations exposed to clients. A query built by hand
|
|
1576
|
+
* without `type` is a filter.
|
|
1577
|
+
*
|
|
1578
|
+
* @param query - the parsed query
|
|
1579
|
+
* @throws {WebdaQLError} for a DELETE or UPDATE statement, or a SELECT field list
|
|
1580
|
+
*/
|
|
1581
|
+
export function assertFilterQuery(query) {
|
|
1582
|
+
if (query.type !== undefined && query.type !== "SELECT") {
|
|
1583
|
+
throw new WebdaQLError(`${query.type} statements are not accepted here: only a filter query is`);
|
|
1584
|
+
}
|
|
1585
|
+
if (query.fields !== undefined) {
|
|
1586
|
+
throw new WebdaQLError("SELECT field lists are not accepted here: only a filter query is");
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1142
1589
|
/**
|
|
1143
1590
|
* Parse a query string into a Query object
|
|
1591
|
+
*
|
|
1592
|
+
* Accepts a plain filter query (an implicit SELECT of every field) or a statement:
|
|
1593
|
+
* - `SELECT f1, f2 [WHERE <condition>] [ORDER BY ...] [LIMIT n] [OFFSET token]`
|
|
1594
|
+
* - `SELECT g1, COUNT(*) AS n, SUM(f) AS s [WHERE <condition>] [GROUP BY g1] [ORDER BY ...] [LIMIT n]`: an
|
|
1595
|
+
* aggregation (metrics COUNT, COUNT(DISTINCT f), SUM, AVG, MIN, MAX), returned in `aggregation`; the
|
|
1596
|
+
* selected fields must be the GROUP BY paths, every metric needs an alias, OFFSET is refused
|
|
1597
|
+
* - `DELETE [WHERE <condition>] [LIMIT n]`
|
|
1598
|
+
* - `UPDATE SET a = v, b = v [WHERE <condition>] [LIMIT n]`
|
|
1599
|
+
*
|
|
1600
|
+
* Keywords are uppercase only: `select`, `delete`, `set`, `count`... remain field names.
|
|
1601
|
+
*
|
|
1144
1602
|
* @param query - the query string to parse
|
|
1603
|
+
* @param allowedFields - when given, SELECT fields, aggregation metric fields and UPDATE SET targets must be in
|
|
1604
|
+
* this list (see {@link validateQueryFields})
|
|
1145
1605
|
* @returns the parsed Query object
|
|
1606
|
+
* @throws {SyntaxError} on a grammar error or a field outside `allowedFields`
|
|
1146
1607
|
*/
|
|
1147
|
-
export function parse(query) {
|
|
1148
|
-
|
|
1608
|
+
export function parse(query, allowedFields) {
|
|
1609
|
+
const result = new QueryValidator(query).getQuery();
|
|
1610
|
+
if (allowedFields) {
|
|
1611
|
+
validateQueryFields(result, allowedFields);
|
|
1612
|
+
}
|
|
1613
|
+
return result;
|
|
1149
1614
|
}
|
|
1150
1615
|
//# sourceMappingURL=query.js.map
|