From eda43083ec1afbfca352fe19b2286a254602c7e9 Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Fri, 29 Dec 2017 15:14:21 -0800 Subject: [PATCH] Implement parser for SQL-like queries --- package.json | 3 +- server/filter-parser.js | 159 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 server/filter-parser.js diff --git a/package.json b/package.json index e7cc313..e31be69 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "koa-bodyparser": "^4.2.0", "koa-router": "^7.3.0", "koa-static": "^4.0.2", - "mithril": "^1.1.5" + "mithril": "^1.1.5", + "parsimmon": "^1.6.2" }, "devDependencies": { "cssnano": "^3.10.0", diff --git a/server/filter-parser.js b/server/filter-parser.js new file mode 100644 index 0000000..080143a --- /dev/null +++ b/server/filter-parser.js @@ -0,0 +1,159 @@ +"use strict"; + +const parsimmon = require("parsimmon"); + +// Turn escaped characters into real ones (e.g. "\\n" becomes "\n"). +function interpretEscapes(str) { + let escapes = { + b: "\b", + f: "\f", + n: "\n", + r: "\r", + t: "\t" + }; + return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => { + let type = escape.charAt(0); + let hex = escape.slice(1); + if (type === "u") return String.fromCharCode(parseInt(hex, 16)); + + if (escapes.hasOwnProperty(type)) return escapes[type]; + + return type; + }); +} + +const lang = parsimmon.createLanguage({ + ComparisonOperator: function(r) { + return parsimmon + .alt( + parsimmon.string(">="), + parsimmon.string("<>"), + parsimmon.string("<="), + parsimmon.string("="), + parsimmon.string(">"), + parsimmon.string("<") + ) + .skip(r._); + }, + IsNullOperator: function(r) { + return parsimmon + .alt( + parsimmon + .regexp(/is\s+null/i) + .result("IS NULL") + .desc("IS NULL"), + parsimmon + .regexp(/is\s+not\s+null/i) + .result("IS NOT NULL") + .desc("IS NOT NULL") + ) + .skip(r._); + }, + NotOperator: function(r) { + return parsimmon + .regexp(/not/i) + .result("NOT") + .skip(r._) + .desc("NOT"); + }, + AndOperator: function(r) { + return parsimmon + .regexp(/and/i) + .result("AND") + .skip(r._) + .desc("AND"); + }, + OrOperator: function(r) { + return parsimmon + .regexp(/or/i) + .result("OR") + .skip(r._) + .desc("OR"); + }, + Parameter: function(r) { + return parsimmon + .regexp(/[a-zA-Z0-9_.*]+/) + .skip(r._) + .desc("parameter"); + }, + StringValueSql: function(r) { + return parsimmon + .regexp(/'([^']*)'/, 1) + .atLeast(1) + .skip(r._) + .map(s => s.join("'")) + .desc("string"); + }, + StringValueJs: function(r) { + return parsimmon + .regexp(/"((?:\\.|.)*?)"/, 1) + .skip(r._) + .map(interpretEscapes) + .desc("string"); + }, + NumberValue: function(r) { + return parsimmon + .regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/) + .skip(r._) + .map(Number) + .desc("number"); + }, + Value: function(r) { + return parsimmon.alt(r.NumberValue, r.StringValueSql, r.StringValueJs); + }, + Comparison: function(r) { + return parsimmon.alt( + parsimmon.seqMap(r.Parameter, r.IsNullOperator, (p, o) => [o, p]), + parsimmon.seqMap( + r.Parameter, + r.ComparisonOperator, + r.Value, + (p, o, v) => [o, p, v] + ) + ); + }, + Expression: function(r) { + function binaryLeft(operatorsParser, nextParser) { + return parsimmon.seqMap( + nextParser, + parsimmon.seq(operatorsParser, nextParser).many(), + (first, rest) => + rest.reduce((acc, ch) => { + let [op, another] = ch; + if (op === acc[0]) return acc.concat([another]); + if (op === another[0]) return [op, acc].concat(another.slice(1)); + return [op, acc, another]; + }, first) + ); + } + + function unary(operatorsParser, nextParser) { + return parsimmon.seq(operatorsParser, nextParser).or(nextParser); + } + + return binaryLeft( + r.OrOperator, + binaryLeft( + r.AndOperator, + unary( + r.NotOperator, + r.Comparison.or( + r.Expression.wrap( + parsimmon.string("("), + parsimmon.string(")").skip(r._) + ) + ) + ) + ) + ).trim(r._); + }, + _: function() { + return parsimmon.optWhitespace; + } +}); + +function parse(filter) { + return lang.Expression.tryParse(filter); +} + +exports.parse = parse;