UNPKG

azurite

Version:

An open source Azure Storage API compatible server

237 lines 9.53 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const QueryLexer_1 = require("./QueryLexer"); const AndNode_1 = tslib_1.__importDefault(require("./QueryNodes/AndNode")); const BigNumberNode_1 = tslib_1.__importDefault(require("./QueryNodes/BigNumberNode")); const BinaryDataNode_1 = tslib_1.__importDefault(require("./QueryNodes/BinaryDataNode")); const ConstantNode_1 = tslib_1.__importDefault(require("./QueryNodes/ConstantNode")); const DateTimeNode_1 = tslib_1.__importDefault(require("./QueryNodes/DateTimeNode")); const EqualsNode_1 = tslib_1.__importDefault(require("./QueryNodes/EqualsNode")); const GreaterThanEqualNode_1 = tslib_1.__importDefault(require("./QueryNodes/GreaterThanEqualNode")); const GreaterThanNode_1 = tslib_1.__importDefault(require("./QueryNodes/GreaterThanNode")); const GuidNode_1 = tslib_1.__importDefault(require("./QueryNodes/GuidNode")); const IdentifierNode_1 = tslib_1.__importDefault(require("./QueryNodes/IdentifierNode")); const LessThanEqualNode_1 = tslib_1.__importDefault(require("./QueryNodes/LessThanEqualNode")); const LessThanNode_1 = tslib_1.__importDefault(require("./QueryNodes/LessThanNode")); const NotEqualsNode_1 = tslib_1.__importDefault(require("./QueryNodes/NotEqualsNode")); const NotNode_1 = tslib_1.__importDefault(require("./QueryNodes/NotNode")); const OrNode_1 = tslib_1.__importDefault(require("./QueryNodes/OrNode")); function parseQuery(query) { return new QueryParser(query).visit(); } exports.default = parseQuery; /** * A recursive descent parser for the Azure Table Storage $filter query syntax. * * This parser is implemented using a recursive descent strategy, which composes * layers of syntax hierarchy, roughly corresponding to the structure of an EBNF * grammar. Each layer of the hierarchy is implemented as a method which consumes * the syntax for that layer, and then calls the next layer of the hierarchy. * * So for example, the syntax tree that we currently use is composed of: * - QUERY := EXPRESSION * - EXPRESSION := OR * - OR := AND ("or" OR)* * - AND := UNARY ("and" AND)* * - UNARY := ("not")? EXPRESSION_GROUP * - EXPRESSION_GROUP := ("(" EXPRESSION ")") | BINARY * - BINARY := IDENTIFIER_OR_CONSTANT (OPERATOR IDENTIFIER_OR_CONSTANT)? * - IDENTIFIER_OR_CONSTANT := CONSTANT | IDENTIFIER * - CONSTANT := NUMBER | STRING | BOOLEAN | DATETIME | GUID | BINARY * - NUMBER := ("-" | "+")? [0-9]+ ("." [0-9]+)? ("L")? */ class QueryParser { constructor(query) { this.tokens = new QueryLexer_1.QueryLexer(query); } /** * Visits the root of the query syntax tree, returning the corresponding root node. * * @returns {IQueryNode} */ visit() { return this.visitQuery(); } /** * Visits the QUERY layer of the query syntax tree, returning the appropriate node. * * @returns {IQueryNode} */ visitQuery() { const tree = this.visitExpression(); this.tokens.next(token => token.kind === "end-of-query") || this.throwUnexpectedToken("end-of-query"); return tree; } /** * Visits the EXPRESSION layer of the query syntax tree, returning the appropriate node. * * EXPRESSION := OR * * @returns {IQueryNode} */ visitExpression() { return this.visitOr(); } /** * Visits the OR layer of the query syntax tree, returning the appropriate node. * * OR := AND ("or" OR)* * * @returns {IQueryNode} */ visitOr() { const left = this.visitAnd(); if (this.tokens.next(t => t.kind === "logic-operator" && t.value?.toLowerCase() === "or")) { const right = this.visitOr(); return new OrNode_1.default(left, right); } else { return left; } } /** * Visits the AND layer of the query syntax tree, returning the appropriate node. * * AND := UNARY ("and" AND)* * * @returns {IQueryNode} */ visitAnd() { const left = this.visitUnary(); if (this.tokens.next(t => t.kind === "logic-operator" && t.value?.toLowerCase() === "and")) { const right = this.visitAnd(); return new AndNode_1.default(left, right); } else { return left; } } /** * Visits the UNARY layer of the query syntax tree, returning the appropriate node. * * UNARY := ("not")? EXPRESSION_GROUP * * @returns {IQueryNode} */ visitUnary() { const hasNot = !!this.tokens.next(t => t.kind === "unary-operator" && t.value?.toLowerCase() === "not"); const right = this.visitExpressionGroup(); if (hasNot) { return new NotNode_1.default(right); } else { return right; } } /** * Visits the EXPRESSION_GROUP layer of the query syntax tree, returning the appropriate node. * * EXPRESSION_GROUP := ("(" OR ")") | BINARY * * @returns {IQueryNode} */ visitExpressionGroup() { if (this.tokens.next(t => t.kind === "open-paren")) { const child = this.visitExpression(); this.tokens.next(t => t.kind === "close-paren") || this.throwUnexpectedToken("close-paren"); return child; } else { return this.visitBinary(); } } /** * Visits the BINARY layer of the query syntax tree, returning the appropriate node. * * BINARY := IDENTIFIER_OR_CONSTANT (OPERATOR IDENTIFIER_OR_CONSTANT)? * * @returns {IQueryNode} */ visitBinary() { const left = this.visitIdentifierOrConstant(); const operator = this.tokens.next(t => t.kind === "comparison-operator"); if (!operator) { return left; } const binaryOperators = { "eq": EqualsNode_1.default, "ne": NotEqualsNode_1.default, "ge": GreaterThanEqualNode_1.default, "gt": GreaterThanNode_1.default, "le": LessThanEqualNode_1.default, "lt": LessThanNode_1.default }; const operatorType = binaryOperators[operator.value?.toLowerCase() || ""] || null; if (!operatorType) { throw new Error(`Got an unexpected operator '${operator?.value}' at :${operator?.position}, expected one of: ${Object.keys(binaryOperators).join(", ")}.`); } const right = this.visitIdentifierOrConstant(); return new operatorType(left, right); } /** * Visits the IDENTIFIER_OR_CONSTANT layer of the query syntax tree, returning the appropriate node. * * IDENTIFIER_OR_CONSTANT := (TYPE_HINT STRING) | NUMBER | STRING | BOOL | IDENTIFIER * * @returns {IQueryNode} */ visitIdentifierOrConstant() { switch (this.tokens.peek().kind) { case "identifier": return new IdentifierNode_1.default(this.tokens.next().value); case "bool": return new ConstantNode_1.default(this.tokens.next().value?.toLowerCase() === "true"); case "string": return new ConstantNode_1.default(this.tokens.next().value); case "number": return this.visitNumber(); case "type-hint": return this.visitTypeHint(); default: this.throwUnexpectedToken("identifier", "bool", "string", "number", "type-hint"); } } visitTypeHint() { const typeHint = this.tokens.next(t => t.kind === "type-hint") || this.throwUnexpectedToken("type-hint"); const value = this.tokens.next(t => t.kind === "string") || this.throwUnexpectedToken("string"); switch (typeHint.value?.toLowerCase()) { case "datetime": return new DateTimeNode_1.default(value.value); case "guid": return new GuidNode_1.default(value.value); case "binary": case "x": return new BinaryDataNode_1.default(value.value); default: throw new Error(`Got an unexpected type hint '${typeHint.value}' at :${typeHint.position} (this implies that the parser is missing a match arm).`); } } /** * Visits the NUMBER layer of the query syntax tree, returning the appropriate node. * * NUMBER := ("-" | "+")? [0-9]+ ("." [0-9]+)? ("L")? * * @returns {IQueryNode} */ visitNumber() { const token = this.tokens.next(t => t.kind === "number") || this.throwUnexpectedToken("number"); if (token.value.endsWith("L")) { // This is a "long" number, which should be represented by its string equivalent return new BigNumberNode_1.default(token.value.substring(0, token.value.length - 1)); } else { return new ConstantNode_1.default(parseFloat(token.value)); } } /** * Raises an exception if the next token in the query is not one of the expected tokens. * * @param {QueryTokenKind} expected The type of tokens which were expected. */ throwUnexpectedToken(...expected) { const actualToken = this.tokens.peek(); throw new Error(`Unexpected token '${actualToken.kind}' at ${actualToken.value || ''}:${actualToken.position}+${actualToken.length} (expected one of: ${expected.join(", ")}).`); } } //# sourceMappingURL=QueryParser.js.map