azurite
Version:
An open source Azure Storage API compatible server
237 lines • 9.53 kB
JavaScript
"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