@webda/core
Version:
Expose API with Lambda
709 lines • 23.3 kB
JavaScript
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(/</g, "<").replace(/>/g, ">");
}
WebdaQL.unsanitize = unsanitize;
})(WebdaQL || (WebdaQL = {}));
//# sourceMappingURL=query.js.map