UNPKG

@webda/core

Version:

Expose API with Lambda

709 lines 23.3 kB
import { CharStreams, CommonTokenStream } from "antlr4ts"; import { AbstractParseTreeVisitor, TerminalNode } from "antlr4ts/tree/index.js"; import { WebdaQLLexer } from "./WebdaQLLexer.js"; import { OrderFieldExpressionContext, SubExpressionContext, WebdaQLParserParser } from "./WebdaQLParserParser.js"; /** * Meta Query Language * * */ export var WebdaQL; (function (WebdaQL) { function PrependCondition(query = "", condition) { return new QueryValidator(query).merge(condition).toString(); } WebdaQL.PrependCondition = PrependCondition; /** * Create Expression based on the parsed token * * Expression allow to optimize and split between Query and Filter */ class ExpressionBuilder extends AbstractParseTreeVisitor { /** * Default result for the override * @returns */ defaultResult() { // An empty AND return true return { filter: new AndExpression([]) }; } /** * Get offset * @returns */ getOffset() { return this.offset; } /** * Get limit * @returns */ getLimit() { return this.limit; } /** * Read the limit * @param ctx */ visitLimitExpression(ctx) { this.limit = this.visitIntegerLiteral(ctx.getChild(1)); } /** * Read the offset if provided * @param ctx */ visitOffsetExpression(ctx) { this.offset = this.visitStringLiteral(ctx.getChild(1)); } /** * Visit a order field expression */ visitOrderFieldExpression(ctx) { return { field: ctx.getChild(0).text, direction: ctx.childCount > 1 ? ctx.getChild(1).text : "ASC" }; } /** * Read the order by values */ visitOrderExpression(ctx) { this.orderBy = ctx.children ?.filter(c => c instanceof OrderFieldExpressionContext) .map((c) => this.visitOrderFieldExpression(c)); } /** * Return only AndExpression * @param ctx * @returns */ visitWebdaql(ctx) { if (ctx.childCount === 1) { // An empty AND return true return { filter: new AndExpression([]) }; } // To parse offset and limit and order by for (let i = 1; i < ctx.childCount - 1; i++) { this.visit(ctx.getChild(i)); } // If the first element is a sub expression, it means we have a filter if (ctx.getChild(0) instanceof SubExpressionContext) { return { filter: this.visit(ctx.getChild(0).getChild(1)) || new AndExpression([]), limit: this.limit, continuationToken: this.offset, orderBy: this.orderBy }; } // Go down one level - if expression empty it means no expression were provided return { filter: this.visit(ctx.getChild(0)) || new AndExpression([]), limit: this.limit, continuationToken: this.offset, orderBy: this.orderBy }; } /** * Simplify Logic expression and regroup them * @param ctx * @returns */ getComparison(ctx) { const res = []; let [left, _, right] = ctx.children; if (right instanceof SubExpressionContext) { right = right.getChild(1); } if (left instanceof SubExpressionContext) { left = left.getChild(1); } if (left instanceof ctx.constructor) { res.push(...this.getComparison(left)); } else { res.push(left); } if (right instanceof ctx.constructor) { res.push(...this.getComparison(right)); } else { res.push(right); } return res; } /** * Get the AndExpression, regrouping all the parameters * * By default the parser is doing a AND (b AND (c AND d)) creating 3 depth expressions * This visitor simplify to a AND b AND c AND d with only one Expression */ visitAndLogicExpression(ctx) { return new AndExpression(this.getComparison(ctx).map(c => this.visit(c))); } /** * Implement the BinaryComparison with all methods managed */ visitBinaryComparisonExpression(ctx) { const [left, op, right] = ctx.children; // @ts-ignore return new ComparisonExpression(op.text, left.text, this.visit(right)); } /** * Visit each value of the [..., ..., ...] set */ visitSetExpression(ctx) { return ctx.children.filter((_i, id) => id % 2).map(c => this.visit(c)); } /** * a LIKE "%A?" * @param ctx * @returns */ visitLikeExpression(ctx) { const [left, _, right] = ctx.children; let value = this.visit(right); return new ComparisonExpression("LIKE", left.text, value); } /** * Map the a IN ['b','c'] */ visitInExpression(ctx) { const [left, _, right] = ctx.children; let value = this.visit(right); return new ComparisonExpression("IN", left.text, value); } /** * Map the a CONTAINS 'b' */ visitContainsExpression(ctx) { const [left, _, right] = ctx.children; let value = this.visit(right); return new ComparisonExpression("CONTAINS", left.text, value); } /** * Get the OrExpression, regrouping all the parameters * * By default the parser is doing a OR (b OR (c OR d)) creating 3 depth expressions * This visitor simplify to a OR b OR c OR d with only one Expression */ visitOrLogicExpression(ctx) { return new OrExpression(this.getComparison(ctx).map(c => this.visit(c))); } /** * Read the string literal (removing the simple or double bracket) */ visitStringLiteral(ctx) { let text = ctx.text.substring(1, ctx.text.length - 1); if (ctx.text[0] === '"') { text = text.replace(/\\"/g, '"'); } else { text = text.replace(/\\'/g, "'"); } return text; } /** * Read the boolean literal */ visitBooleanLiteral(ctx) { return "TRUE" === ctx.text; } /** * Read the number literal */ visitIntegerLiteral(ctx) { return parseInt(ctx.text); } } WebdaQL.ExpressionBuilder = ExpressionBuilder; /** * Represent the query expression or subset */ class Expression { constructor(operator) { this.operator = operator; } } WebdaQL.Expression = Expression; /** * Comparison expression */ class ComparisonExpression extends Expression { /** * * @param operator of the expression * @param attribute of the object to read * @param value */ constructor(operator, attribute, value) { super(operator); this.value = value; this.attribute = attribute.split("."); } static likeToRegex(like) { return new RegExp(like // Prevent common regexp chars .replace(/\?/g, "\\?") .replace(/\[/g, "\\[") .replace(/\{/g, "\\{") .replace(/\(/g, "\\(") // Update % and _ to match regex version .replace(/([^\\])_/g, "$1.{1}") .replace(/^_/g, ".{1}") .replace(/\\_/g, "_") .replace(/([^\\])%/g, "$1.*") .replace(/^%/g, ".*") .replace(/\\%/g, "%") // Replace backslash aswell .replace(/\\([^?[{(])/g, "\\\\")); } /** * Read the value from the object * * @param target * @returns */ static getAttributeValue(target, attribute) { let res = target; for (let i = 0; res && i < attribute.length; i++) { res = res[attribute[i]]; } return res; } /** * Set the value of the attribute based on the assignment * * If used as a Set expression * @param target */ setAttributeValue(target) { var _a; // Avoid alteration of prototype for security reason if (this.attribute.includes("__proto__")) { return; } if (this.operator === "=") { let res = target; for (let i = 0; res && i < this.attribute.length - 1; i++) { res[_a = this.attribute[i]] ?? (res[_a] = {}); res = res[this.attribute[i]]; } res[this.attribute[this.attribute.length - 1]] = this.value; } } /** * @override */ eval(target) { const left = ComparisonExpression.getAttributeValue(target, this.attribute); switch (this.operator) { case "=": // ignore strong type on purpose return left == this.value; case "<=": return left <= this.value; case ">=": return left >= this.value; case "<": return left < this.value; case ">": return left > this.value; case "!=": return left != this.value; case "LIKE": if (typeof left === "string") { // Grammar definie value as stringLiteral return left.match(ComparisonExpression.likeToRegex(this.value)) !== null; } return left.toString().match(ComparisonExpression.likeToRegex(this.value)) !== null; case "IN": return this.value.includes(left); case "CONTAINS": if (Array.isArray(left)) { return left.includes(this.value); } return false; } } /** * Return a string represantation of a value */ toStringValue(value) { if (Array.isArray(value)) { return `[${value.map(v => this.toStringValue(v)).join(", ")}]`; } switch (typeof value) { case "string": return `"${value}"`; case "boolean": return value.toString().toUpperCase(); } return value?.toString(); } /** * Allow subclass to create different display */ toStringAttribute() { return this.attribute.join("."); } /** * Allow subclass to create different display */ toStringOperator() { return this.operator; } /** * @override */ toString() { return `${this.toStringAttribute()} ${this.toStringOperator()} ${this.toStringValue(this.value)}`; } } WebdaQL.ComparisonExpression = ComparisonExpression; /** * Abstract logic expression (AND|OR) * * Could add XOR in the future */ class LogicalExpression extends Expression { /** * * @param operator * @param children */ constructor(operator, children) { super(operator); /** * Contains the members of the logical expression */ this.children = []; this.children = children; } /** * @override */ toString(depth = 0) { if (depth) { return "( " + this.children.map(c => c.toString(depth + 1)).join(` ${this.operator} `) + " )"; } return this.children.map(c => c.toString(depth + 1)).join(` ${this.operator} `); } } WebdaQL.LogicalExpression = LogicalExpression; /** * AND Expression implementation */ class AndExpression extends LogicalExpression { /** * @param children Expressions to use for AND */ constructor(children) { super("AND", children); } /** * @override */ eval(target) { for (let child of this.children) { if (!child.eval(target)) { return false; } } return true; } } WebdaQL.AndExpression = AndExpression; /** * OR Expression implementation */ class OrExpression extends LogicalExpression { /** * @param children Expressions to use for OR */ constructor(children) { super("OR", children); } /** * @override */ eval(target) { for (let child of this.children) { if (child.eval(target)) { return true; } } return this.children.length === 0; } } WebdaQL.OrExpression = OrExpression; /** * */ class QueryValidator { constructor(sql, builder = new ExpressionBuilder()) { this.sql = sql; this.lexer = new WebdaQLLexer(CharStreams.fromString(sql || "")); let tokenStream = new CommonTokenStream(this.lexer); let parser = new WebdaQLParserParser(tokenStream); parser.removeErrorListeners(); parser.addErrorListener({ syntaxError: (_recognizer, _offendingSymbol, _line, _charPositionInLine, msg, _e) => { throw new SyntaxError(`${msg} (Query: ${sql})`); } }); // Parse the input, where `compilationUnit` is whatever entry point you defined this.tree = parser.webdaql(); this.builder = builder; this.query = this.builder.visit(this.tree); } hasCondition() { const filter = this.query.filter; const isAnd = filter instanceof AndExpression; if (isAnd) { return filter.children.length > 0; } return true; } toString() { let res = this.query.filter.toString(); if (this.query.orderBy) { res += ` ORDER BY ${this.query.orderBy.map(o => `${o.field} ${o.direction}`).join(", ")}`; } if (this.query.limit) { res += ` LIMIT ${this.query.limit}`; } if (this.query.continuationToken) { res += ` OFFSET "${this.query.continuationToken}"`; } return res.trim(); } merge(query, type = "AND") { var _a; const adds = new QueryValidator(query); // Add additional conditions if (adds.hasCondition()) { if ((this.query.filter instanceof AndExpression && type === "AND") || (this.query.filter instanceof OrExpression && type === "OR")) { this.query.filter.children.push(adds.query.filter); } else { this.query.filter = new (type === "AND" ? AndExpression : OrExpression)([ this.query.filter, adds.query.filter ]); } } // Set the limit if overriden if (adds.query.limit) { this.query.limit = adds.query.limit; } // Set the offset if overriden if (adds.query.continuationToken) { this.query.continuationToken = adds.query.continuationToken; } // Add the order by if overriden if (adds.query.orderBy) { const fields = adds.query.orderBy.map(o => o.field); (_a = this.query).orderBy ?? (_a.orderBy = []); // Remove the fields that are already in the query this.query.orderBy = [...adds.query.orderBy, ...this.query.orderBy.filter(o => !fields.includes(o.field))]; } return this; } /** * Get offset * @returns */ getOffset() { return this.builder.getOffset() || ""; } /** * Get limit * @returns */ getLimit() { return this.builder.getLimit() || 1000; } /** * Get the expression by itself * @returns */ getExpression() { return this.query.filter; } /** * Retrieve parsed query * @returns */ getQuery() { return { ...this.query, // Use displayTree to get the truely executed query toString: () => this.displayTree() }; } /** * Verify if a target fit the expression * @param target * @returns */ eval(target) { return this.query.filter.eval(target); } /** * Display parse tree back as query * @param tree * @returns */ displayTree(tree = this.tree) { let res = ""; for (let i = 0; i < tree.childCount; i++) { const child = tree.getChild(i); if (child instanceof TerminalNode) { if (child.text === "<EOF>") { continue; } res += child.text.trim() + " "; } else { res += this.displayTree(child).trim() + " "; } } return res; } } WebdaQL.QueryValidator = QueryValidator; /** * For now reuse same parser */ class SetterValidator extends QueryValidator { constructor(sql) { super(sql); // Do one empty run to raise any issue with disallowed expression this.eval({}); } eval(target) { if (this.query.filter) { this.assign(target, this.query.filter); } return true; } assign(target, expression) { if (expression instanceof AndExpression) { expression.children.forEach(c => this.assign(target, c)); } else if (expression instanceof ComparisonExpression && expression.operator === "=") { expression.setAttributeValue(target); } else { throw new SyntaxError(`Set Expression can only contain And and assignment expression '='`); } } } WebdaQL.SetterValidator = SetterValidator; class PartialValidator extends QueryValidator { constructor(query, builder = new PartialExpressionBuilder()) { super(query, builder); } /** * Eval the query * @param target * @param partial * @returns */ eval(target, partial = true) { this.builder.setPartial(partial); this.builder.setPartialMatch(false); return this.query.filter.eval(target); } /** * Return if the result ignored some fields * @returns */ wasPartialMatch() { return this.builder.partialMatch; } } WebdaQL.PartialValidator = PartialValidator; class PartialComparisonExpression extends ComparisonExpression { constructor(builder, op, attribute, value) { super(op, attribute, value); this.builder = builder; } /** * Override the eval to check if the attribute is present * if not and we are in partial mode, return true * * @param target * @returns */ eval(target) { if (this.builder.partial) { const left = ComparisonExpression.getAttributeValue(target, this.attribute); if (left === undefined) { this.builder.setPartialMatch(true); return true; } } return super.eval(target); } } WebdaQL.PartialComparisonExpression = PartialComparisonExpression; class PartialExpressionBuilder extends ExpressionBuilder { setPartial(partial) { this.partial = partial; } setPartialMatch(partial) { this.partialMatch = partial; } /** * a LIKE "%A?" * @param ctx * @returns */ visitLikeExpression(ctx) { const [left, _, right] = ctx.children; let value = this.visit(right); return new PartialComparisonExpression(this, "LIKE", left.text, value); } /** * Implement the BinaryComparison with all methods managed */ visitBinaryComparisonExpression(ctx) { const [left, op, right] = ctx.children; // @ts-ignore return new PartialComparisonExpression(this, op.text, left.text, this.visit(right)); } /** * Map the a IN ['b','c'] */ visitInExpression(ctx) { const [left, _, right] = ctx.children; let value = this.visit(right); return new PartialComparisonExpression(this, "IN", left.text, value); } /** * Map the a CONTAINS 'b' */ visitContainsExpression(ctx) { const [left, _, right] = ctx.children; let value = this.visit(right); return new PartialComparisonExpression(this, "CONTAINS", left.text, value); } } WebdaQL.PartialExpressionBuilder = PartialExpressionBuilder; /** * Remove artifact from sanitize-html inside query * @param query * @returns */ function unsanitize(query) { return query.replace(/&lt;/g, "<").replace(/&gt;/g, ">"); } WebdaQL.unsanitize = unsanitize; })(WebdaQL || (WebdaQL = {})); //# sourceMappingURL=query.js.map