Replace Filter class with expression structure
This is in order to be able to support UPPER() and LOWER() functions in queries which are needed for case-insensitive search.
This commit is contained in:
1 parent
7194fcab44
commit
c2d13e8364
25 files changed
+716
-578
No files matched your search
@@ -1,5 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const expression = require("./expression");
|
||||
|
||||
const validators = {
|
||||
pass: require("./validators/pass"),
|
||||
test: require("./validators/test")
|
||||
@@ -18,15 +20,13 @@ class Authorizer {
|
||||
return this.hasAccessCache[cacheKey];
|
||||
|
||||
let has = false;
|
||||
for (const permissionSet of this.permissionSets) {
|
||||
for (const perm of permissionSet) {
|
||||
if (perm[resourceType]) {
|
||||
for (let permissionSet of this.permissionSets) {
|
||||
for (let perm of permissionSet)
|
||||
if (perm[resourceType])
|
||||
if (perm[resourceType].access >= access) {
|
||||
has = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.hasAccessCache[cacheKey] = has;
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
"use strict";
|
||||
|
||||
function collectRanges(exp, ranges = new Map()) {
|
||||
if (["AND", "OR", "NOT"].includes(exp[0]))
|
||||
for (let i = 1; i < exp.length; ++i) collectRanges(exp[i], ranges);
|
||||
else if ([">", ">=", "<", "<=", "=", "<>"].includes(exp[0]))
|
||||
if (!Array.isArray(exp[2])) {
|
||||
const key = `${JSON.stringify(exp[1])}:${typeof exp[2]}`;
|
||||
let s = ranges.get(key);
|
||||
if (!s) ranges.set(key, (s = new Set()));
|
||||
s.add(exp[2]);
|
||||
} else if (!Array.isArray(exp[1])) {
|
||||
const key = `${JSON.stringify(exp[2])}:${typeof exp[1]}`;
|
||||
let s = ranges.get(key);
|
||||
if (!s) ranges.set(key, (s = new Set()));
|
||||
s.add(exp[2]);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function or(cnf1, cnf2) {
|
||||
if (cnf1.length === 0) return cnf2;
|
||||
else if (cnf2.length === 0) return cnf1;
|
||||
|
||||
const res = [];
|
||||
for (let i = 0; i < cnf1.length; ++i)
|
||||
for (let j = i; j < cnf2.length; ++j) res.push(cnf1[i].concat(cnf2[j]));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function booleanCnf(exp) {
|
||||
const ranges = collectRanges(exp);
|
||||
for (let [k, v] of ranges)
|
||||
if (k.endsWith(":number"))
|
||||
ranges.set(k, Array.from(v).sort((a, b) => a - b));
|
||||
else ranges.set(k, Array.from(v).sort());
|
||||
|
||||
const expressions = new Map();
|
||||
const mutuallyExclusive = new Map();
|
||||
|
||||
function getVariable(lhs, op, rhs = "") {
|
||||
const key = `${lhs}:${op}:${rhs}`;
|
||||
let f = expressions.get(key);
|
||||
if (!f) {
|
||||
expressions.set(key, (f = expressions.size + 1));
|
||||
let m = mutuallyExclusive.get(lhs);
|
||||
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
|
||||
m.add(f);
|
||||
if (rhs || key !== "null") m.add(getVariable(lhs, "null"));
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
function getComparisonVariables(lhs, from, fromInclusive, to, toInclusive) {
|
||||
const type = from != null ? typeof from : typeof to;
|
||||
const range = ranges.get(`${lhs}:${type}`);
|
||||
const vars = [];
|
||||
|
||||
if (from == null) vars.push(`${lhs}:--${range[0]}`);
|
||||
if (to == null) vars.push(`${lhs}:${range[range.length - 1]}--`);
|
||||
if (fromInclusive) vars.push(`${lhs}:${from}`);
|
||||
if (toInclusive) vars.push(`${lhs}:${to}`);
|
||||
|
||||
for (let i = 1; i < range.length; ++i)
|
||||
if ((from == null || range[i] > from) && (to == null || range[i] <= to)) {
|
||||
vars.push(`${lhs}:${range[i - 1]}`);
|
||||
vars.push(`${lhs}:${range[i]}`);
|
||||
vars.push(`${lhs}:${range[i - 1]}--${range[i]}`);
|
||||
}
|
||||
|
||||
return vars.map(key => {
|
||||
let f = expressions.get(key);
|
||||
if (!f) {
|
||||
expressions.set(key, (f = expressions.size + 1));
|
||||
let m = mutuallyExclusive.get(lhs);
|
||||
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
|
||||
m.add(f);
|
||||
}
|
||||
return f;
|
||||
});
|
||||
}
|
||||
|
||||
const comparisonOperators = {
|
||||
"=": v => [v, true, v, true],
|
||||
">": v => [v, false, null, null],
|
||||
">=": v => [v, true, null, null],
|
||||
"<": v => [null, null, v, false],
|
||||
"<=": v => [null, null, v, true]
|
||||
};
|
||||
|
||||
function recursive(clause, negate) {
|
||||
let op = clause[0];
|
||||
|
||||
if ((op === "AND" && !negate) || (op === "OR" && negate)) {
|
||||
let res = [];
|
||||
for (let i = 1; i < clause.length; ++i)
|
||||
res = res.concat(recursive(clause[i], negate));
|
||||
return res;
|
||||
} else if ((op === "OR" && !negate) || (op === "AND" && negate)) {
|
||||
let res = [];
|
||||
for (let i = 1; i < clause.length; ++i)
|
||||
res = or(res, recursive(clause[i], negate));
|
||||
return res;
|
||||
} else if (op === "NOT") {
|
||||
return recursive(clause[1], !negate);
|
||||
} else if (op === "<>") {
|
||||
const vars = getComparisonVariables(
|
||||
JSON.stringify(clause[1]),
|
||||
...comparisonOperators["="](clause[2])
|
||||
);
|
||||
if (!negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else if (op === "IS NULL") {
|
||||
const v = getVariable(JSON.stringify(clause[1]), "null");
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
} else if (op === "IS NOT NULL") {
|
||||
const v = getVariable(JSON.stringify(clause[1]), "null");
|
||||
if (!negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
} else if (op in comparisonOperators) {
|
||||
if (!Array.isArray(clause[2])) {
|
||||
const vars = getComparisonVariables(
|
||||
JSON.stringify(clause[1]),
|
||||
...comparisonOperators[op](clause[2])
|
||||
);
|
||||
if (negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else if (!Array.isArray(clause[1])) {
|
||||
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
|
||||
const vars = getComparisonVariables(
|
||||
JSON.stringify(clause[2]),
|
||||
...comparisonOperators[op](clause[1])
|
||||
);
|
||||
if (negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else {
|
||||
let lhs = JSON.stringify(clause[1]);
|
||||
let rhs = JSON.stringify(clause[2]);
|
||||
// For consistency
|
||||
if (rhs > lhs) {
|
||||
let t = rhs;
|
||||
rhs = lhs;
|
||||
lhs = t;
|
||||
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
|
||||
}
|
||||
|
||||
const v = getVariable(lhs, op, rhs);
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
}
|
||||
} else {
|
||||
const v = getVariable(JSON.stringify(clause), "");
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
}
|
||||
}
|
||||
|
||||
const cnf = recursive(exp, false);
|
||||
|
||||
for (let m of mutuallyExclusive.values()) {
|
||||
let ar = Array.from(m);
|
||||
for (let i = 0; i < ar.length; ++i)
|
||||
for (let j = i + 1; j < ar.length; ++j) cnf.push([0 - ar[i], 0 - ar[j]]);
|
||||
}
|
||||
|
||||
return { vars: expressions.size, clauses: cnf };
|
||||
}
|
||||
|
||||
exports.booleanCnf = booleanCnf;
|
||||
@@ -22,6 +22,21 @@ function interpretEscapes(str) {
|
||||
});
|
||||
}
|
||||
|
||||
function map(exp, callback) {
|
||||
if (!Array.isArray(exp)) return callback(exp);
|
||||
|
||||
let clone;
|
||||
for (let i = 1; i < exp.length; ++i) {
|
||||
let sub = map(exp[i], callback);
|
||||
if (sub !== exp[i]) {
|
||||
clone = clone || exp.slice();
|
||||
clone[i] = sub;
|
||||
}
|
||||
}
|
||||
|
||||
return callback(clone || exp);
|
||||
}
|
||||
|
||||
function binaryLeft(operatorsParser, nextParser) {
|
||||
return parsimmon.seqMap(
|
||||
nextParser,
|
||||
@@ -29,8 +44,9 @@ function binaryLeft(operatorsParser, nextParser) {
|
||||
(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));
|
||||
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
|
||||
if (Array.isArray(another) && op === another[0])
|
||||
return [op, acc].concat(another.slice(1));
|
||||
return [op, acc, another];
|
||||
}, first)
|
||||
);
|
||||
@@ -61,12 +77,14 @@ const lang = parsimmon.createLanguage({
|
||||
.result("IS NOT NULL")
|
||||
.desc("IS NOT NULL")
|
||||
)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace);
|
||||
},
|
||||
NotOperator: function() {
|
||||
return parsimmon
|
||||
.regexp(/not/i)
|
||||
.result("NOT")
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("NOT");
|
||||
},
|
||||
@@ -74,6 +92,7 @@ const lang = parsimmon.createLanguage({
|
||||
return parsimmon
|
||||
.regexp(/and/i)
|
||||
.result("AND")
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("AND");
|
||||
},
|
||||
@@ -81,6 +100,7 @@ const lang = parsimmon.createLanguage({
|
||||
return parsimmon
|
||||
.regexp(/or/i)
|
||||
.result("OR")
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("OR");
|
||||
},
|
||||
@@ -88,13 +108,13 @@ const lang = parsimmon.createLanguage({
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon.regexp(/[a-zA-Z0-9_.*]+/),
|
||||
r.ValueExpression.wrap(
|
||||
r.Expression.wrap(
|
||||
parsimmon.string("{").skip(parsimmon.optWhitespace),
|
||||
parsimmon.string("}")
|
||||
)
|
||||
)
|
||||
.atLeast(1)
|
||||
.map(x => (x.length > 1 ? ["||"].concat(x) : x[0]))
|
||||
.map(x => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("parameter");
|
||||
},
|
||||
@@ -120,13 +140,38 @@ const lang = parsimmon.createLanguage({
|
||||
.map(Number)
|
||||
.desc("number");
|
||||
},
|
||||
BooleanValue: function() {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon
|
||||
.regexp(/true/i)
|
||||
.result(true)
|
||||
.desc("TRUE"),
|
||||
parsimmon
|
||||
.regexp(/false/i)
|
||||
.result(false)
|
||||
.desc("FALSE")
|
||||
)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace);
|
||||
},
|
||||
NullValue: function() {
|
||||
return parsimmon
|
||||
.regexp(/null/i)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.result(null)
|
||||
.desc("NULL");
|
||||
},
|
||||
FuncValue: function(r) {
|
||||
return parsimmon.seqMap(
|
||||
parsimmon
|
||||
.regexp(/([a-zA-Z0-0_]+)/, 1)
|
||||
.regexp(/([a-zA-Z0-9_]+)/, 1)
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("function"),
|
||||
r.Value.sepBy(parsimmon.string(",").skip(parsimmon.optWhitespace)).wrap(
|
||||
r.ValueExpression.sepBy(
|
||||
parsimmon.string(",").skip(parsimmon.optWhitespace)
|
||||
).wrap(
|
||||
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
||||
parsimmon.string(")").skip(parsimmon.optWhitespace)
|
||||
),
|
||||
@@ -135,6 +180,8 @@ const lang = parsimmon.createLanguage({
|
||||
},
|
||||
Value: function(r) {
|
||||
return parsimmon.alt(
|
||||
r.NullValue,
|
||||
r.BooleanValue,
|
||||
r.NumberValue,
|
||||
r.StringValueSql,
|
||||
r.StringValueJs,
|
||||
@@ -143,7 +190,7 @@ const lang = parsimmon.createLanguage({
|
||||
},
|
||||
ValueExpression: function(r) {
|
||||
return binaryLeft(
|
||||
parsimmon.string("||"),
|
||||
parsimmon.string("||").skip(parsimmon.optWhitespace),
|
||||
binaryLeft(
|
||||
parsimmon
|
||||
.alt(parsimmon.string("+"), parsimmon.string("-"))
|
||||
@@ -154,7 +201,8 @@ const lang = parsimmon.createLanguage({
|
||||
.skip(parsimmon.optWhitespace),
|
||||
parsimmon.alt(
|
||||
r.Value,
|
||||
r.ValueExpression.wrap(
|
||||
r.Parameter,
|
||||
r.Expression.wrap(
|
||||
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
||||
parsimmon.string(")").skip(parsimmon.optWhitespace)
|
||||
)
|
||||
@@ -167,7 +215,7 @@ const lang = parsimmon.createLanguage({
|
||||
return parsimmon.alt(
|
||||
parsimmon.seqMap(r.Parameter, r.IsNullOperator, (p, o) => [o, p]),
|
||||
parsimmon.seqMap(
|
||||
r.Parameter,
|
||||
r.ValueExpression,
|
||||
r.ComparisonOperator,
|
||||
r.ValueExpression,
|
||||
(p, o, v) => [o, p, v]
|
||||
@@ -183,129 +231,78 @@ const lang = parsimmon.createLanguage({
|
||||
r.OrOperator,
|
||||
binaryLeft(
|
||||
r.AndOperator,
|
||||
unary(
|
||||
r.NotOperator,
|
||||
r.Comparison.or(
|
||||
r.Expression.wrap(
|
||||
parsimmon.string("("),
|
||||
parsimmon.string(")").skip(parsimmon.optWhitespace)
|
||||
)
|
||||
)
|
||||
)
|
||||
unary(r.NotOperator, r.Comparison.or(r.ValueExpression))
|
||||
)
|
||||
).trim(parsimmon.optWhitespace);
|
||||
}
|
||||
});
|
||||
|
||||
function parse(filter) {
|
||||
return lang.Expression.tryParse(filter);
|
||||
function parse(str) {
|
||||
if (!str) return null;
|
||||
return lang.Expression.tryParse(str);
|
||||
}
|
||||
|
||||
function parseExpression(exp) {
|
||||
return lang.ValueExpression.tryParse(exp);
|
||||
}
|
||||
function stringify(exp) {
|
||||
if (!Array.isArray(exp)) return JSON.stringify(exp);
|
||||
|
||||
function evaluateExpressions(ast, funcCallback) {
|
||||
return map(ast, exp => {
|
||||
if (exp[0] === "FUNC") {
|
||||
return funcCallback(exp);
|
||||
} else if (exp[0] === "*") {
|
||||
let v = exp[1];
|
||||
for (let i = 2; i < exp.length; ++i) v *= exp[i];
|
||||
return v;
|
||||
} else if (exp[0] === "/") {
|
||||
let v = exp[1];
|
||||
for (let i = 2; i < exp.length; ++i) v /= exp[i];
|
||||
return v;
|
||||
} else if (exp[0] === "+") {
|
||||
let v = exp[1];
|
||||
for (let i = 2; i < exp.length; ++i) v += exp[i];
|
||||
return v;
|
||||
} else if (exp[0] === "-") {
|
||||
let v = exp[1];
|
||||
for (let i = 2; i < exp.length; ++i) v -= exp[i];
|
||||
return v;
|
||||
} else if (exp[0] === "||") {
|
||||
return exp.slice(1).join("");
|
||||
}
|
||||
});
|
||||
}
|
||||
const opLevels = {
|
||||
OR: 10,
|
||||
AND: 11,
|
||||
NOT: 12,
|
||||
"=": 20,
|
||||
"<>": 21,
|
||||
">": 22,
|
||||
">=": 23,
|
||||
"<": 24,
|
||||
"<=": 25,
|
||||
"||": 30,
|
||||
"+": 31,
|
||||
"-": 31,
|
||||
"*": 32,
|
||||
"/": 32
|
||||
};
|
||||
|
||||
function parseParameter(p) {
|
||||
return lang.Parameter.tryParse(p);
|
||||
}
|
||||
const op = exp[0].toUpperCase();
|
||||
|
||||
function stringify(filter) {
|
||||
function value(v) {
|
||||
if (Array.isArray(v))
|
||||
if (v[0].toUpperCase() === "FUNC") {
|
||||
return `${v[1]}(${v
|
||||
.slice(2)
|
||||
.map(e => value(e))
|
||||
.join(", ")})`;
|
||||
} else {
|
||||
return v
|
||||
.slice(1)
|
||||
.map(e => value(e))
|
||||
.join(` ${v[0]} `);
|
||||
}
|
||||
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
|
||||
function expressions(v) {
|
||||
return v.map(e => {
|
||||
let str = stringify(e);
|
||||
if (e[0] === "AND" || e[0] === "OR") return `(${str})`;
|
||||
else return str;
|
||||
});
|
||||
}
|
||||
|
||||
function parameter(v) {
|
||||
if (Array.isArray(v) && v[0] === "||")
|
||||
return v
|
||||
if (op === "FUNC") {
|
||||
return `${exp[1]}(${exp
|
||||
.slice(2)
|
||||
.map(e => stringify(e))
|
||||
.join(", ")})`;
|
||||
} else if (op === "PARAM") {
|
||||
if (typeof exp[1] === "string") return exp[1];
|
||||
else if (Array.isArray(exp[1]) && exp[1][0] === "||")
|
||||
return exp[1]
|
||||
.slice(1)
|
||||
.map(x => {
|
||||
if (Array.isArray(x)) return `{${value(x)}}`;
|
||||
else return x;
|
||||
.map(p => {
|
||||
if (typeof p === "string") return p;
|
||||
else return `{${stringify(p)}}`;
|
||||
})
|
||||
.join("");
|
||||
else return `{${stringify(exp[1])}}`;
|
||||
} else if (op === "IS NULL" || op === "IS NOT NULL") {
|
||||
return `${stringify(exp[1])} ${op}`;
|
||||
} else if (op in opLevels) {
|
||||
const parts = exp.slice(1).map((e, i) => {
|
||||
if (
|
||||
Array.isArray(e) &&
|
||||
(opLevels[e[0]] < opLevels[exp[0]] ||
|
||||
(opLevels[e[0]] < opLevels[exp[0]] && i > 0) ||
|
||||
(opLevels[exp[0]] >= 20 &&
|
||||
opLevels[e[0]] >= 20 &&
|
||||
opLevels[e[0]] < 30))
|
||||
)
|
||||
return `(${stringify(e)})`;
|
||||
else return stringify(e);
|
||||
});
|
||||
|
||||
return v;
|
||||
if (op === "NOT") return `${op} ${parts[0]}`;
|
||||
else return parts.join(` ${op} `);
|
||||
} else {
|
||||
throw new Error(`Unrecognized operator ${exp[0]}`);
|
||||
}
|
||||
|
||||
const op = filter[0].toUpperCase();
|
||||
|
||||
if (["AND", "OR"].includes(op))
|
||||
return expressions(filter.slice(1)).join(` ${op} `);
|
||||
else if (op === "NOT") return `NOT ${expressions(filter.slice(1))[0]}`;
|
||||
else if ([">=", "<>", "<=", "=", ">", "<"].includes(op))
|
||||
return `${parameter(filter[1])} ${op} ${value(filter[2])}`;
|
||||
else if (["IS NULL", "IS NOT NULL"].includes(op))
|
||||
return `${parameter(filter[1])} ${op}`;
|
||||
else throw new Error("Unrecognized operator");
|
||||
}
|
||||
|
||||
function map(filter, callback) {
|
||||
let clone;
|
||||
for (let i = 1; i < filter.length; ++i)
|
||||
if (Array.isArray(filter[i])) {
|
||||
let sub = map(filter[i], callback);
|
||||
if (sub != null && sub !== filter[i]) {
|
||||
clone = clone || filter.slice();
|
||||
clone[i] = sub;
|
||||
}
|
||||
}
|
||||
|
||||
let r = callback(clone || filter);
|
||||
if (r != null) return r;
|
||||
|
||||
return clone || filter;
|
||||
}
|
||||
|
||||
exports.parse = parse;
|
||||
exports.parseExpression = parseExpression;
|
||||
exports.evaluateExpressions = evaluateExpressions;
|
||||
exports.parseParameter = parseParameter;
|
||||
exports.stringify = stringify;
|
||||
exports.map = map;
|
||||
@@ -0,0 +1,188 @@
|
||||
"use strict";
|
||||
|
||||
const expressionParser = require("./expression-parser");
|
||||
const expressionCnf = require("./expression-cnf");
|
||||
const satSolver = require("./sat-solver");
|
||||
|
||||
const isArray = Array.isArray;
|
||||
|
||||
function* permute(arr) {
|
||||
if (arr.length <= 1) {
|
||||
for (let i = 0; i < arr[0]; ++i) yield [i];
|
||||
return;
|
||||
}
|
||||
|
||||
let slc = arr.slice(1);
|
||||
for (let i = 0; i < arr[0]; ++i)
|
||||
for (let innerArr of permute(slc)) yield [i].concat(innerArr);
|
||||
}
|
||||
|
||||
function reduce(exp, callback) {
|
||||
let loop = true;
|
||||
while (loop) {
|
||||
loop = false;
|
||||
for (let i = 2; i < exp.length; ++i) {
|
||||
let res = callback(exp[i - 1], exp[i]);
|
||||
if (res !== undefined) {
|
||||
loop = true;
|
||||
exp = exp.slice();
|
||||
exp.splice(i - 1, 2, res);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (exp.length === 2) return exp[1];
|
||||
return exp;
|
||||
}
|
||||
|
||||
function evaluate(exp, obj, now) {
|
||||
return expressionParser.map(exp, e => {
|
||||
if (!isArray(e)) return e;
|
||||
|
||||
if (e[0] === "FUNC") {
|
||||
if (e[1] === "NOW") {
|
||||
if (now) return now;
|
||||
} else if (e[1] === "UPPER") {
|
||||
if (e[2] == null) return null;
|
||||
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
|
||||
} else if (e[1] === "LOWER") {
|
||||
if (e[12] == null) return null;
|
||||
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
|
||||
}
|
||||
} else if (e[0] === "PARAM") {
|
||||
if (e[1] == null) return null;
|
||||
if (obj && !isArray(e[1]))
|
||||
if (typeof obj[e[1]] === "object")
|
||||
return obj[e[1]].value ? obj[e[1]].value[0] : null;
|
||||
else return obj[e[1]];
|
||||
} else if (e[0] === "AND") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a)) return a ? b : a;
|
||||
if (!isArray(b)) return b ? a : b;
|
||||
});
|
||||
} else if (e[0] === "OR") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a)) return a ? a : b;
|
||||
if (!isArray(b)) return b ? b : a;
|
||||
});
|
||||
} else if (e[0] === "NOT") {
|
||||
if (!isArray(e[1])) return !e[1];
|
||||
else if (e[1][0] === "NOT") return e[1][1];
|
||||
} else if (e[0] === "IS NULL") {
|
||||
if (isArray(e[1])) return e;
|
||||
return e[1] == null;
|
||||
} else if (e[0] === "IS NOT NULL") {
|
||||
if (isArray(e[1])) return e;
|
||||
return e[1] != null;
|
||||
} else if (e[0] === "=") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] === e[2];
|
||||
} else if (e[0] === "<>") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] !== e[2];
|
||||
} else if (e[0] === ">") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] > e[2];
|
||||
} else if (e[0] === ">=") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] >= e[2];
|
||||
} else if (e[0] === "<") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] < e[2];
|
||||
} else if (e[0] === "<=") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] <= e[2];
|
||||
} else if (e[0] === "*") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return a * b;
|
||||
}
|
||||
});
|
||||
} else if (e[0] === "/") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return a / b;
|
||||
}
|
||||
});
|
||||
} else if (e[0] === "+") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return a + b;
|
||||
}
|
||||
});
|
||||
} else if (e[0] === "-") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return a - b;
|
||||
}
|
||||
});
|
||||
} else if (e[0] === "||") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return `${a}${b}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
function and(exp1, exp2) {
|
||||
if (!isArray(exp1)) return exp1 ? exp2 : exp1;
|
||||
if (!isArray(exp2)) return exp2 ? exp1 : exp2;
|
||||
|
||||
let res = ["AND"];
|
||||
|
||||
if (exp1[0] === "AND") res = res.concat(exp1.slice(1));
|
||||
else res.push(exp1);
|
||||
|
||||
if (exp2[0] === "AND") res = res.concat(exp2.slice(1));
|
||||
else res.push(exp2);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function or(exp1, exp2) {
|
||||
if (!isArray(exp1)) return exp1 ? exp1 : exp2;
|
||||
if (!isArray(exp2)) return exp2 ? exp2 : exp1;
|
||||
|
||||
let res = ["OR"];
|
||||
|
||||
if (exp1[0] === "OR") res = res.concat(exp1.slice(1));
|
||||
else res.push(exp1);
|
||||
|
||||
if (exp2[0] === "OR") res = res.concat(exp2.slice(1));
|
||||
else res.push(exp2);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function not(exp) {
|
||||
if (isArray(exp) && exp[0] === "NOT") return exp[1];
|
||||
return ["NOT", exp];
|
||||
}
|
||||
|
||||
function subset(exp1, exp2) {
|
||||
const e = evaluate(["NOT", ["OR", ["NOT", exp1], exp2]]);
|
||||
if (!isArray(e)) return !e;
|
||||
const { vars, clauses } = expressionCnf.booleanCnf(e);
|
||||
return !satSolver.naiveDpll(clauses, vars);
|
||||
}
|
||||
|
||||
exports.evaluate = evaluate;
|
||||
exports.and = and;
|
||||
exports.or = or;
|
||||
exports.not = not;
|
||||
exports.subset = subset;
|
||||
exports.parse = expressionParser.parse;
|
||||
exports.stringify = expressionParser.stringify;
|
||||
@@ -1,138 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
function collectRanges(filter, ranges = new Map()) {
|
||||
if (["AND", "OR", "NOT"].includes(filter[0])) {
|
||||
for (let i = 1; i < filter.length; ++i) collectRanges(filter[i], ranges);
|
||||
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(filter[0])) {
|
||||
const key = `${filter[1]}:${typeof filter[2]}`;
|
||||
let s = ranges.get(key);
|
||||
if (!s) ranges.set(key, (s = new Set()));
|
||||
s.add(filter[2]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function or(cnf1, cnf2) {
|
||||
if (cnf1.length === 0) return cnf2;
|
||||
else if (cnf2.length === 0) return cnf1;
|
||||
|
||||
const res = [];
|
||||
for (let i = 0; i < cnf1.length; ++i)
|
||||
for (let j = i; j < cnf2.length; ++j) res.push(cnf1[i].concat(cnf2[j]));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function booleanCnf(filter) {
|
||||
const ranges = collectRanges(filter);
|
||||
for (let [k, v] of ranges)
|
||||
if (k.endsWith(":number"))
|
||||
ranges.set(k, Array.from(v).sort((a, b) => a - b));
|
||||
else ranges.set(k, Array.from(v).sort());
|
||||
|
||||
const expressions = new Map();
|
||||
const mutuallyExclusive = new Map();
|
||||
|
||||
function getIsNullVariable(param) {
|
||||
const key = `${param}:null`;
|
||||
let f = expressions.get(key);
|
||||
if (!f) {
|
||||
expressions.set(key, (f = expressions.size + 1));
|
||||
let m = mutuallyExclusive.get(param);
|
||||
if (!m) mutuallyExclusive.set(param, (m = new Set()));
|
||||
m.add(f);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
function getComparisonVariables(param, from, fromInclusive, to, toInclusive) {
|
||||
const type = from != null ? typeof from : typeof to;
|
||||
const range = ranges.get(`${param}:${type}`);
|
||||
const vars = [];
|
||||
|
||||
if (from == null) vars.push(`${param}:--${range[0]}`);
|
||||
if (to == null) vars.push(`${param}:${range[range.length - 1]}--`);
|
||||
if (fromInclusive) vars.push(`${param}:${from}`);
|
||||
if (toInclusive) vars.push(`${param}:${to}`);
|
||||
|
||||
for (let i = 1; i < range.length; ++i)
|
||||
if ((from == null || range[i] > from) && (to == null || range[i] <= to)) {
|
||||
vars.push(`${param}:${range[i - 1]}`);
|
||||
vars.push(`${param}:${range[i]}`);
|
||||
vars.push(`${param}:${range[i - 1]}--${range[i]}`);
|
||||
}
|
||||
|
||||
return vars.map(key => {
|
||||
let f = expressions.get(key);
|
||||
if (!f) {
|
||||
expressions.set(key, (f = expressions.size + 1));
|
||||
let m = mutuallyExclusive.get(param);
|
||||
if (!m) mutuallyExclusive.set(param, (m = new Set()));
|
||||
m.add(f);
|
||||
}
|
||||
return f;
|
||||
});
|
||||
}
|
||||
|
||||
const comparisonOperators = {
|
||||
"=": v => [v, true, v, true],
|
||||
">": v => [v, false, null, null],
|
||||
">=": v => [v, true, null, null],
|
||||
"<": v => [null, null, v, false],
|
||||
"<=": v => [null, null, v, true]
|
||||
};
|
||||
|
||||
function recursive(clause, negate) {
|
||||
const op = clause[0];
|
||||
|
||||
if ((op === "AND" && !negate) || (op === "OR" && negate)) {
|
||||
let res = [];
|
||||
for (let i = 1; i < clause.length; ++i)
|
||||
res = res.concat(recursive(clause[i], negate));
|
||||
return res;
|
||||
} else if ((op === "OR" && !negate) || (op === "AND" && negate)) {
|
||||
let res = [];
|
||||
for (let i = 1; i < clause.length; ++i)
|
||||
res = or(res, recursive(clause[i], negate));
|
||||
return res;
|
||||
} else if (op === "NOT") {
|
||||
return recursive(clause[1], !negate);
|
||||
} else if (op in comparisonOperators) {
|
||||
const vars = getComparisonVariables(
|
||||
clause[1],
|
||||
...comparisonOperators[op](clause[2])
|
||||
);
|
||||
if (negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else if (op === "<>") {
|
||||
const vars = getComparisonVariables(
|
||||
clause[1],
|
||||
...comparisonOperators["="](clause[2])
|
||||
);
|
||||
if (!negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else if (op === "IS NULL") {
|
||||
const v = getIsNullVariable(clause[1]);
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
} else if (op === "IS NOT NULL") {
|
||||
const v = getIsNullVariable(clause[1]);
|
||||
if (!negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
} else {
|
||||
throw new Error(`Unknown operator ${op}`);
|
||||
}
|
||||
}
|
||||
|
||||
const cnf = recursive(filter, false);
|
||||
|
||||
for (let m of mutuallyExclusive.values()) {
|
||||
let ar = Array.from(m);
|
||||
for (let i = 0; i < ar.length; ++i)
|
||||
for (let j = i + 1; j < ar.length; ++j) cnf.push([0 - ar[i], 0 - ar[j]]);
|
||||
}
|
||||
|
||||
return { vars: expressions.size, clauses: cnf };
|
||||
}
|
||||
|
||||
exports.booleanCnf = booleanCnf;
|
||||
@@ -1,145 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const filterParser = require("./filter-parser");
|
||||
const filterCnf = require("./filter-cnf");
|
||||
const satSolver = require("./sat-solver");
|
||||
const funcCache = require("./func-cache");
|
||||
|
||||
function* permute(arr) {
|
||||
if (arr.length <= 1) {
|
||||
for (let i = 0; i < arr[0]; ++i) yield [i];
|
||||
return;
|
||||
}
|
||||
|
||||
let slc = arr.slice(1);
|
||||
for (let i = 0; i < arr[0]; ++i)
|
||||
for (let innerArr of permute(slc)) yield [i].concat(innerArr);
|
||||
}
|
||||
|
||||
class Filter {
|
||||
constructor(str) {
|
||||
if (!str) this.ast = null;
|
||||
else if (Array.isArray(str)) this.ast = str;
|
||||
else if (str instanceof Filter) this.ast = str.ast;
|
||||
else if (Array.isArray(str.ast) || str.ast === null) this.ast = str.ast;
|
||||
else this.ast = filterParser.parse(str);
|
||||
}
|
||||
|
||||
static parse(str) {
|
||||
return new Filter(str);
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (!this.string)
|
||||
if (this.ast) this.string = filterParser.stringify(this.ast);
|
||||
else this.string = "";
|
||||
|
||||
return this.string;
|
||||
}
|
||||
|
||||
or(fltr) {
|
||||
if (!fltr || !this.ast) return this;
|
||||
else if (!fltr.ast) return fltr;
|
||||
|
||||
let f = ["OR"];
|
||||
|
||||
if (this.ast[0] === "OR") f = f.concat(this.ast.slice(1));
|
||||
else f.push(this.ast);
|
||||
|
||||
if (fltr.ast[0] === "OR") f = f.concat(fltr.ast.slice(1));
|
||||
else f.push(fltr.ast);
|
||||
|
||||
return new Filter(f);
|
||||
}
|
||||
|
||||
and(fltr) {
|
||||
if (!fltr || !fltr.ast) return this;
|
||||
else if (!this.ast) return fltr;
|
||||
|
||||
let f = ["AND"];
|
||||
|
||||
if (this.ast[0] === "AND") f = f.concat(this.ast.slice(1));
|
||||
else f.push(this.ast);
|
||||
|
||||
if (fltr.ast[0] === "AND") f = f.concat(fltr.ast.slice(1));
|
||||
else f.push(fltr.ast);
|
||||
|
||||
return new Filter(f);
|
||||
}
|
||||
|
||||
not() {
|
||||
if (this.ast[0] === "NOT") return new Filter(this.ast[1]);
|
||||
else return new Filter(["NOT", this.ast]);
|
||||
}
|
||||
|
||||
evaluateExpressions(now = Date.now()) {
|
||||
if (!this.ast) return this;
|
||||
|
||||
let ast = filterParser.evaluateExpressions(this.ast, exp => {
|
||||
if (exp[0] === "FUNC" && exp[1] === "NOW") return now;
|
||||
});
|
||||
|
||||
if (ast === this.ast) return this;
|
||||
else return new Filter(ast);
|
||||
}
|
||||
|
||||
subset(fltr) {
|
||||
if (!fltr.ast) return true;
|
||||
else if (!this.ast) return false;
|
||||
|
||||
const f = this.not()
|
||||
.or(fltr)
|
||||
.not();
|
||||
const { vars, clauses } = filterCnf.booleanCnf(f.ast);
|
||||
return !satSolver.naiveDpll(clauses, vars);
|
||||
}
|
||||
|
||||
test(obj) {
|
||||
if (!this.ast) return true;
|
||||
return filterParser.map(this.ast, exp => {
|
||||
const op = exp[0];
|
||||
|
||||
if (op === "AND") {
|
||||
for (let i = 1; i < exp.length; ++i) if (!exp[i]) return false;
|
||||
return true;
|
||||
} else if (op === "OR") {
|
||||
for (let i = 1; i < exp.length; ++i) if (exp[i]) return true;
|
||||
return false;
|
||||
} else if (op === "NOT") {
|
||||
return !exp[1];
|
||||
} else if (op === "=") {
|
||||
const val =
|
||||
typeof obj[exp[1]] === "object" ? obj[exp[1]].value[0] : obj[exp[1]];
|
||||
return val === exp[2];
|
||||
} else if (op === "<>") {
|
||||
const val =
|
||||
typeof obj[exp[1]] === "object" ? obj[exp[1]].value[0] : obj[exp[1]];
|
||||
return val !== exp[2];
|
||||
} else if (op === ">=") {
|
||||
const val =
|
||||
typeof obj[exp[1]] === "object" ? obj[exp[1]].value[0] : obj[exp[1]];
|
||||
return typeof val === typeof exp[2] && val >= exp[2];
|
||||
} else if (op === ">") {
|
||||
const val =
|
||||
typeof obj[exp[1]] === "object" ? obj[exp[1]].value[0] : obj[exp[1]];
|
||||
return typeof val === typeof exp[2] && val > exp[2];
|
||||
} else if (op === "<=") {
|
||||
const val =
|
||||
typeof obj[exp[1]] === "object" ? obj[exp[1]].value[0] : obj[exp[1]];
|
||||
return typeof val === typeof exp[2] && val <= exp[2];
|
||||
} else if (op === "<") {
|
||||
const val =
|
||||
typeof obj[exp[1]] === "object" ? obj[exp[1]].value[0] : obj[exp[1]];
|
||||
return typeof val === typeof exp[2] && val < exp[2];
|
||||
} else if (op === "IS NULL") {
|
||||
return obj[exp[1]] == null;
|
||||
} else if (op === "IS NOT NULL") {
|
||||
return obj[exp[1]] != null;
|
||||
} else {
|
||||
throw new Error(`Unsupported operator ${op}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Filter;
|
||||
@@ -22,6 +22,8 @@ function purge() {
|
||||
cache1 = new WeakMap();
|
||||
}
|
||||
|
||||
setInterval(purge, 120000);
|
||||
|
||||
exports.get = get;
|
||||
exports.getter = getter;
|
||||
exports.purge = purge;
|
||||
Reference in new issue
Block a user