Merge remote-tracking branch 'ui/master'
This commit is contained in:
commit
4530967b6b
70 files changed
+10951
-8
No files matched your search
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
|
||||
const validators = {
|
||||
pass: require("./validators/pass"),
|
||||
test: require("./validators/test")
|
||||
};
|
||||
|
||||
class Authorizer {
|
||||
constructor(permissionSets) {
|
||||
this.permissionSets = permissionSets;
|
||||
this.validatorCache = new WeakMap();
|
||||
this.hasAccessCache = {};
|
||||
}
|
||||
|
||||
hasAccess(resourceType, access) {
|
||||
const cacheKey = `${resourceType}-${access}`;
|
||||
if (cacheKey in this.hasAccessCache) return this.hasAccessCache[cacheKey];
|
||||
|
||||
let has = false;
|
||||
for (const permissionSet of this.permissionSets) {
|
||||
for (const perm of permissionSet) {
|
||||
if (perm[resourceType]) {
|
||||
if (perm[resourceType].access >= access) {
|
||||
has = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.hasAccessCache[cacheKey] = has;
|
||||
return has;
|
||||
}
|
||||
|
||||
getValidator(resourceType, resource) {
|
||||
if (this.validatorCache.has(resource))
|
||||
return this.validatorCache.get(resource);
|
||||
|
||||
let funcs = {};
|
||||
|
||||
for (const permissionSet of this.permissionSets) {
|
||||
for (const perm of permissionSet) {
|
||||
if (perm[resourceType]) {
|
||||
if (perm[resourceType].access >= 3) {
|
||||
if (perm[resourceType].validate) {
|
||||
for (const [k, v] of Object.entries(perm[resourceType].validate))
|
||||
funcs[k] = v;
|
||||
}
|
||||
} else {
|
||||
funcs = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validator = (mutationType, mutation, any) => {
|
||||
let valid = false;
|
||||
for (const [k, v] of Object.entries(funcs)) {
|
||||
if (v) {
|
||||
const res = validators[k](
|
||||
resourceType,
|
||||
resource,
|
||||
mutationType,
|
||||
mutation,
|
||||
any
|
||||
);
|
||||
|
||||
if (res > 0) valid = true;
|
||||
else if (res < 0) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
};
|
||||
|
||||
this.validatorCache.set(resource, validator);
|
||||
return validator;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Authorizer;
|
||||
@@ -0,0 +1,255 @@
|
||||
"use strict";
|
||||
|
||||
const expressionParser = require("./expression-parser");
|
||||
|
||||
function or(cnf1, cnf2) {
|
||||
if (cnf1.length === 0) return cnf2;
|
||||
else if (cnf2.length === 0) return cnf1;
|
||||
|
||||
if (cnf1.length > cnf2.length) {
|
||||
const c = cnf1;
|
||||
cnf1 = cnf2;
|
||||
cnf2 = c;
|
||||
}
|
||||
|
||||
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 = new Map();
|
||||
const likePatterns = new Map();
|
||||
expressionParser.map(exp, e => {
|
||||
if (!Array.isArray(e)) return e;
|
||||
if (e[0] === "LIKE" || e[0] === "NOT LIKE") {
|
||||
if (
|
||||
typeof e[2] === "string" &&
|
||||
(e.length >= 4 || (e[3] != null && !Array.isArray(e[3])))
|
||||
) {
|
||||
const key = JSON.stringify(e[1]);
|
||||
let s = likePatterns.get(key);
|
||||
if (!s) likePatterns.set(key, (s = new Map()));
|
||||
s.set(
|
||||
`${e[3] || ""}:${e[2]}`,
|
||||
expressionParser.parseLikePattern(e[2], e[3])
|
||||
);
|
||||
}
|
||||
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(e[0])) {
|
||||
if (!Array.isArray(e[2])) {
|
||||
const key = `${JSON.stringify(e[1])}:${typeof e[2]}`;
|
||||
let s = ranges.get(key);
|
||||
if (!s) ranges.set(key, (s = new Set()));
|
||||
s.add(e[2]);
|
||||
} else if (!Array.isArray(e[1])) {
|
||||
const key = `${JSON.stringify(e[2])}:${typeof e[1]}`;
|
||||
let s = ranges.get(key);
|
||||
if (!s) ranges.set(key, (s = new Set()));
|
||||
s.add(e[2]);
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
|
||||
for (const [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 || op !== "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]}`);
|
||||
|
||||
for (let i = 0; i < range.length; ++i) {
|
||||
if (
|
||||
i > 0 &&
|
||||
(from == null || range[i] > from) &&
|
||||
(to == null || range[i] <= to)
|
||||
)
|
||||
vars.push(`${lhs}:${range[i - 1]}--${range[i]}`);
|
||||
|
||||
if (
|
||||
(range[i] > from && range[i] < to) ||
|
||||
(range[i] === from && fromInclusive) ||
|
||||
(range[i] === to && toInclusive)
|
||||
)
|
||||
vars.push(`${lhs}:${range[i]}`);
|
||||
}
|
||||
|
||||
if (to == null) vars.push(`${lhs}:${range[range.length - 1]}--`);
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
function getLikeVariables(lhs, pat, esc) {
|
||||
const vars = [];
|
||||
const pats = likePatterns.get(lhs);
|
||||
pat = pats.get(`${esc || ""}:${pat}`);
|
||||
for (const p of pats.values()) {
|
||||
if (pat === p || likePatternIncludes(pat, p))
|
||||
vars.push(`${lhs}:like:${JSON.stringify(p)}`);
|
||||
}
|
||||
|
||||
return vars.map(key => {
|
||||
let f = expressions.get(key);
|
||||
if (!f) expressions.set(key, (f = expressions.size + 1));
|
||||
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) {
|
||||
const t = rhs;
|
||||
rhs = lhs;
|
||||
lhs = t;
|
||||
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
|
||||
}
|
||||
|
||||
const v = getVariable(lhs, op, rhs);
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
}
|
||||
} else if (
|
||||
(op === "LIKE" || op === "NOT LIKE") &&
|
||||
!Array.isArray(clause[2]) &&
|
||||
clause[2] != null &&
|
||||
(clause.length >= 4 || clause[3] != null)
|
||||
) {
|
||||
if (op === "NOT LIKE") negate = !negate;
|
||||
const vars = getLikeVariables(
|
||||
JSON.stringify(clause[1]),
|
||||
clause[2],
|
||||
clause[3]
|
||||
);
|
||||
if (negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else {
|
||||
const v = getVariable(JSON.stringify(clause), "");
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
}
|
||||
}
|
||||
|
||||
const cnf = recursive(exp, false);
|
||||
|
||||
for (const m of mutuallyExclusive.values()) {
|
||||
const 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 };
|
||||
}
|
||||
|
||||
function likePatternIncludes(pat1, pat2, idx1 = 0, idx2 = 0) {
|
||||
while (idx1 < pat1.length && idx2 < pat2.length) {
|
||||
if (pat1[idx1] === "\\%") {
|
||||
for (let i = idx2; i <= pat2.length; ++i)
|
||||
if (likePatternIncludes(pat1, pat2, idx1 + 1, i)) return true;
|
||||
return false;
|
||||
} else if (pat2[idx2] === "\\%") {
|
||||
if (pat1[idx1] !== "\\%") return false;
|
||||
} else if (pat1[idx1] === "\\_") {
|
||||
// Ignore
|
||||
} else if (pat2[idx2] === "\\_") {
|
||||
return false;
|
||||
} else if (pat2[idx2] !== pat1[idx1]) {
|
||||
return false;
|
||||
}
|
||||
++idx1;
|
||||
++idx2;
|
||||
}
|
||||
|
||||
if (idx1 === pat1.length && idx2 === pat2.length) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
exports.booleanCnf = booleanCnf;
|
||||
@@ -0,0 +1,384 @@
|
||||
"use strict";
|
||||
|
||||
const parsimmon = require("parsimmon");
|
||||
|
||||
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
|
||||
function interpretEscapes(str) {
|
||||
const escapes = {
|
||||
b: "\b",
|
||||
f: "\f",
|
||||
n: "\n",
|
||||
r: "\r",
|
||||
t: "\t"
|
||||
};
|
||||
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
|
||||
const type = escape.charAt(0);
|
||||
const hex = escape.slice(1);
|
||||
if (type === "u") return String.fromCharCode(parseInt(hex, 16));
|
||||
|
||||
if (escapes.hasOwnProperty(type)) return escapes[type];
|
||||
|
||||
return type;
|
||||
});
|
||||
}
|
||||
|
||||
function map(exp, callback) {
|
||||
if (!Array.isArray(exp)) return callback(exp);
|
||||
|
||||
let clone;
|
||||
for (let i = 1; i < exp.length; ++i) {
|
||||
const 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,
|
||||
parsimmon.seq(operatorsParser, nextParser).many(),
|
||||
(first, rest) =>
|
||||
rest.reduce((acc, ch) => {
|
||||
const [op, another] = ch;
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
const lang = parsimmon.createLanguage({
|
||||
ComparisonOperator: function() {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon.string(">="),
|
||||
parsimmon.string("<>"),
|
||||
parsimmon.string("<="),
|
||||
parsimmon.string("="),
|
||||
parsimmon.string(">"),
|
||||
parsimmon.string("<")
|
||||
)
|
||||
.skip(parsimmon.optWhitespace);
|
||||
},
|
||||
LikeOperator: function() {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon
|
||||
.regexp(/like/i)
|
||||
.result("LIKE")
|
||||
.desc("LIKE"),
|
||||
parsimmon
|
||||
.regexp(/not\s+like/i)
|
||||
.result("NOT LIKE")
|
||||
.desc("NOT LIKE")
|
||||
)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace);
|
||||
},
|
||||
IsNullOperator: function() {
|
||||
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")
|
||||
)
|
||||
.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");
|
||||
},
|
||||
AndOperator: function() {
|
||||
return parsimmon
|
||||
.regexp(/and/i)
|
||||
.result("AND")
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("AND");
|
||||
},
|
||||
OrOperator: function() {
|
||||
return parsimmon
|
||||
.regexp(/or/i)
|
||||
.result("OR")
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("OR");
|
||||
},
|
||||
Parameter: function(r) {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon.regexp(/[a-zA-Z0-9_.*-]+/),
|
||||
r.Expression.wrap(
|
||||
parsimmon.string("{").skip(parsimmon.optWhitespace),
|
||||
parsimmon.string("}")
|
||||
)
|
||||
)
|
||||
.atLeast(1)
|
||||
.map(x => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("parameter");
|
||||
},
|
||||
StringValueSql: function() {
|
||||
return parsimmon
|
||||
.regexp(/'([^']*)'/, 1)
|
||||
.atLeast(1)
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.map(s => s.join("'"))
|
||||
.desc("string");
|
||||
},
|
||||
StringValueJs: function() {
|
||||
return parsimmon
|
||||
.regexp(/"((?:\\.|.)*?)"/, 1)
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.map(interpretEscapes)
|
||||
.desc("string");
|
||||
},
|
||||
NumberValue: function() {
|
||||
return parsimmon
|
||||
.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.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-9_]+)/, 1)
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("function"),
|
||||
r.ValueExpression.sepBy(
|
||||
parsimmon.string(",").skip(parsimmon.optWhitespace)
|
||||
).wrap(
|
||||
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
||||
parsimmon.string(")").skip(parsimmon.optWhitespace)
|
||||
),
|
||||
(f, args) => ["FUNC", f.toUpperCase()].concat(args)
|
||||
);
|
||||
},
|
||||
Value: function(r) {
|
||||
return parsimmon.alt(
|
||||
r.NullValue,
|
||||
r.BooleanValue,
|
||||
r.NumberValue,
|
||||
r.StringValueSql,
|
||||
r.StringValueJs,
|
||||
r.FuncValue
|
||||
);
|
||||
},
|
||||
ValueExpression: function(r) {
|
||||
return binaryLeft(
|
||||
parsimmon.string("||").skip(parsimmon.optWhitespace),
|
||||
binaryLeft(
|
||||
parsimmon
|
||||
.alt(parsimmon.string("+"), parsimmon.string("-"))
|
||||
.skip(parsimmon.optWhitespace),
|
||||
binaryLeft(
|
||||
parsimmon
|
||||
.alt(parsimmon.string("*"), parsimmon.string("/"))
|
||||
.skip(parsimmon.optWhitespace),
|
||||
parsimmon.alt(
|
||||
r.Value,
|
||||
r.Parameter,
|
||||
r.Expression.wrap(
|
||||
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
||||
parsimmon.string(")").skip(parsimmon.optWhitespace)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
Comparison: function(r) {
|
||||
return parsimmon.alt(
|
||||
parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]),
|
||||
parsimmon.seqMap(
|
||||
r.ValueExpression,
|
||||
r.ComparisonOperator,
|
||||
r.ValueExpression,
|
||||
(p, o, v) => [o, p, v]
|
||||
),
|
||||
parsimmon.seqMap(
|
||||
r.ValueExpression,
|
||||
r.LikeOperator,
|
||||
r.ValueExpression.skip(
|
||||
parsimmon
|
||||
.regexp(/escape/i)
|
||||
.result("ESCAPE")
|
||||
.skip(parsimmon.whitespace)
|
||||
.desc("ESCAPE")
|
||||
),
|
||||
r.ValueExpression,
|
||||
(a, b, c, d) => [b, a, c, d]
|
||||
),
|
||||
parsimmon.seqMap(
|
||||
r.ValueExpression,
|
||||
r.LikeOperator,
|
||||
r.ValueExpression,
|
||||
(a, b, c) => [b, a, c]
|
||||
)
|
||||
);
|
||||
},
|
||||
Expression: function(r) {
|
||||
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.ValueExpression))
|
||||
)
|
||||
).trim(parsimmon.optWhitespace);
|
||||
}
|
||||
});
|
||||
|
||||
function parse(str) {
|
||||
if (!str) return null;
|
||||
return lang.Expression.tryParse(str);
|
||||
}
|
||||
|
||||
function stringify(exp, level = 0) {
|
||||
if (!Array.isArray(exp)) return JSON.stringify(exp);
|
||||
|
||||
const opLevels = {
|
||||
OR: 10,
|
||||
AND: 11,
|
||||
NOT: 12,
|
||||
"=": 20,
|
||||
"<>": 20,
|
||||
">": 20,
|
||||
">=": 20,
|
||||
"<": 20,
|
||||
"<=": 20,
|
||||
LIKE: 20,
|
||||
"NOT LIKE": 20,
|
||||
"IS NULL": 20,
|
||||
"IS NOT NULL": 20,
|
||||
"||": 30,
|
||||
"+": 31,
|
||||
"-": 31,
|
||||
"*": 32,
|
||||
"/": 32
|
||||
};
|
||||
|
||||
const op = exp[0].toUpperCase();
|
||||
|
||||
function wrap(e) {
|
||||
if (opLevels[op] <= level) return `(${e})`;
|
||||
else return e;
|
||||
}
|
||||
|
||||
if (op === "FUNC") {
|
||||
return wrap(
|
||||
`${exp[1]}(${exp
|
||||
.slice(2)
|
||||
.map(e => stringify(e))
|
||||
.join(", ")})`
|
||||
);
|
||||
} else if (op === "PARAM") {
|
||||
if (typeof exp[1] === "string") {
|
||||
return wrap(exp[1]);
|
||||
} else if (Array.isArray(exp[1]) && exp[1][0] === "||") {
|
||||
return wrap(
|
||||
exp[1]
|
||||
.slice(1)
|
||||
.map(p => {
|
||||
if (typeof p === "string") return p;
|
||||
else return `{${stringify(p)}}`;
|
||||
})
|
||||
.join("")
|
||||
);
|
||||
} else {
|
||||
return wrap(`{${stringify(exp[1])}}`);
|
||||
}
|
||||
} else if (op === "IS NULL" || op === "IS NOT NULL") {
|
||||
return wrap(`${stringify(exp[1], opLevels[op])} ${op}`);
|
||||
} else if (op === "LIKE" || op === "NOT LIKE") {
|
||||
if (exp[3]) {
|
||||
return wrap(
|
||||
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
|
||||
exp[2],
|
||||
opLevels[op]
|
||||
)} ESCAPE ${stringify(exp[3], opLevels[op])}`
|
||||
);
|
||||
} else {
|
||||
return wrap(
|
||||
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
|
||||
exp[2],
|
||||
opLevels[op]
|
||||
)}`
|
||||
);
|
||||
}
|
||||
} else if (op in opLevels) {
|
||||
const parts = exp.slice(1).map((e, i) => {
|
||||
return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0));
|
||||
});
|
||||
|
||||
if (op === "NOT") return wrap(`${op} ${parts[0]}`);
|
||||
else return wrap(parts.join(` ${op} `));
|
||||
} else {
|
||||
throw new Error(`Unrecognized operator ${exp[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseLikePattern(pat, esc) {
|
||||
const chars = pat.split("");
|
||||
|
||||
for (let i = 0; i < chars.length; ++i) {
|
||||
const c = chars[i];
|
||||
if (c === esc) {
|
||||
chars[i] = chars[i + 1] || "";
|
||||
chars[i + 1] = "";
|
||||
} else if (c === "_") {
|
||||
chars[i] = "\\_";
|
||||
} else if (c === "%") {
|
||||
chars[i] = "\\%";
|
||||
while (chars[i + 1] === "%") chars[++i] = "";
|
||||
}
|
||||
}
|
||||
return chars.filter(c => c);
|
||||
}
|
||||
|
||||
exports.parse = parse;
|
||||
exports.stringify = stringify;
|
||||
exports.map = map;
|
||||
exports.parseLikePattern = parseLikePattern;
|
||||
@@ -0,0 +1,254 @@
|
||||
"use strict";
|
||||
|
||||
const expressionParser = require("./expression-parser");
|
||||
const expressionCnf = require("./expression-cnf");
|
||||
const satSolver = require("./sat-solver");
|
||||
|
||||
const isArray = Array.isArray;
|
||||
|
||||
const regExpCache = new WeakMap();
|
||||
|
||||
const REDUCE_SKIP = {};
|
||||
function reduce(exp, callback) {
|
||||
let loop = true;
|
||||
while (loop) {
|
||||
loop = false;
|
||||
for (let i = 2; i < exp.length; ++i) {
|
||||
const res = callback(exp[i - 1], exp[i]);
|
||||
if (res !== REDUCE_SKIP) {
|
||||
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, cb) {
|
||||
function getRegExp(pat, esc) {
|
||||
const k = `${esc || ""}:${pat}`;
|
||||
let c = regExpCache.get(exp);
|
||||
if (!c) regExpCache.set(exp, (c = {}));
|
||||
if (!c[k]) c[k] = likePatternToRegExp(pat, esc);
|
||||
return c[k];
|
||||
}
|
||||
|
||||
return expressionParser.map(exp, e => {
|
||||
if (cb) e = cb(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[2] == 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])) {
|
||||
let v = obj[e[1]];
|
||||
if (v == null) return null;
|
||||
if (typeof v === "object") v = v.value ? v.value[0] : null;
|
||||
return v;
|
||||
}
|
||||
} else if (e[0] === "AND") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a)) return a ? b : a;
|
||||
if (!isArray(b)) return b ? a : b;
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} else if (e[0] === "OR") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a)) return a ? a : b;
|
||||
if (!isArray(b)) return b ? b : a;
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} 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;
|
||||
else if (e[1] == null) return true;
|
||||
else return null;
|
||||
} else if (e[0] === "IS NOT NULL") {
|
||||
if (isArray(e[1])) return e;
|
||||
else if (e[1] != null) return true;
|
||||
else return null;
|
||||
} else if (e[0] === "LIKE") {
|
||||
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
|
||||
else if (e[1] == null || e[2] == null || (e.length >= 4 && e[3] == null))
|
||||
return null;
|
||||
|
||||
const r = getRegExp(e[2], e[3]);
|
||||
return r.test(e[1]);
|
||||
} else if (e[0] === "NOT LIKE") {
|
||||
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
|
||||
else if (e[1] == null || e[2] == null || (e.length >= 4 && e[3] == null))
|
||||
return null;
|
||||
const r = getRegExp(e[2], e[3]);
|
||||
return !r.test(e[1]);
|
||||
} 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;
|
||||
}
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} 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 REDUCE_SKIP;
|
||||
});
|
||||
} 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 REDUCE_SKIP;
|
||||
});
|
||||
} 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 REDUCE_SKIP;
|
||||
});
|
||||
} 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 REDUCE_SKIP;
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
function likePatternToRegExp(pat, esc = "", flags = "") {
|
||||
const convChars = {
|
||||
"-": "\\-",
|
||||
"/": "\\/",
|
||||
"\\": "\\/",
|
||||
"^": "\\^",
|
||||
$: "\\$",
|
||||
"*": "\\*",
|
||||
"+": "\\+",
|
||||
"?": "\\?",
|
||||
".": "\\.",
|
||||
"(": "\\(",
|
||||
")": "\\)",
|
||||
"|": "\\|",
|
||||
"[": "\\[",
|
||||
"]": "\\]",
|
||||
"{": "\\{",
|
||||
"}": "\\}",
|
||||
"\\%": ".*",
|
||||
"\\_": "."
|
||||
};
|
||||
let chars = expressionParser.parseLikePattern(pat, esc);
|
||||
if (!chars.length) return new RegExp("^$", flags);
|
||||
chars = chars.map(c => convChars[c] || c);
|
||||
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
|
||||
const l = chars.length - 1;
|
||||
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
|
||||
return new RegExp(chars.join(""), flags);
|
||||
}
|
||||
|
||||
function extractParams(exp) {
|
||||
const params = new Set();
|
||||
expressionParser.map(exp, e => {
|
||||
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
|
||||
return e;
|
||||
});
|
||||
return Array.from(params);
|
||||
}
|
||||
|
||||
exports.evaluate = evaluate;
|
||||
exports.and = and;
|
||||
exports.or = or;
|
||||
exports.not = not;
|
||||
exports.subset = subset;
|
||||
exports.parse = expressionParser.parse;
|
||||
exports.stringify = expressionParser.stringify;
|
||||
exports.likePatternToRegExp = likePatternToRegExp;
|
||||
exports.extractParams = extractParams;
|
||||
@@ -0,0 +1,47 @@
|
||||
"use strict";
|
||||
|
||||
let cache1 = new Map();
|
||||
let cache2 = new Map();
|
||||
const keys = new WeakMap();
|
||||
|
||||
function getKey(obj) {
|
||||
if (obj === null) return "null";
|
||||
else if (obj === undefined) return "undefined";
|
||||
|
||||
const t = typeof obj;
|
||||
if (t === "number" || t === "boolean" || t === "string") return `${t}:${obj}`;
|
||||
if (t !== "function" && t !== "object")
|
||||
throw new Error(`Cannot memoize ${t} arguments`);
|
||||
|
||||
let k = keys.get(obj);
|
||||
if (!k) {
|
||||
const rnd = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
k = `${t}:${rnd.toString(36)}`;
|
||||
keys.set(obj, k);
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
function memoize(func) {
|
||||
return (...args) => {
|
||||
const key = JSON.stringify(args.map(getKey)) + getKey(func);
|
||||
|
||||
if (cache1.has(key)) return cache1.get(key);
|
||||
|
||||
let r;
|
||||
if (cache2.has(key)) r = cache2.get(key);
|
||||
else r = func(...args);
|
||||
cache1.set(key, r);
|
||||
return r;
|
||||
};
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
cache2 = cache1;
|
||||
cache1 = new Map();
|
||||
}, 120000);
|
||||
|
||||
// Don't hold Node.js process
|
||||
if (interval.unref) interval.unref();
|
||||
|
||||
module.exports = memoize;
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Naive DPLL solver for propositional logic adapted from
|
||||
* https://github.com/tammet/logictools
|
||||
*
|
||||
* Origianl code by Tanel Tammet released under the MIT license.
|
||||
*
|
||||
* Check whether a clause set is satisfiable or contradictory, using a naive
|
||||
* implementation of the dpll (Davis-Putnam-Logemann-Loveland) procedure
|
||||
* combining truth table search with the resolution-like unit propagation.
|
||||
*
|
||||
* The standard pure-literal rule is not implemented in this naive version,
|
||||
* since it is not really crucial for the algorithm.
|
||||
*
|
||||
* The code is very similar to the pure table solver, except we use a
|
||||
* unit-propagating, derived-varlist-returing satisfiableAt.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
function satisfiableAt(clauses, varVals, varnr, val) {
|
||||
if (varnr !== 0) varVals[varnr] = val;
|
||||
|
||||
const propRes = unitPropagate(clauses, varVals);
|
||||
if (propRes === 1) {
|
||||
varVals[varnr] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// if result false (-1) unitPropagate restores the varVals state itself
|
||||
if (propRes === -1) {
|
||||
varVals[varnr] = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// find next unassigned var to split
|
||||
let nextVar = 0;
|
||||
for (let i = 1; i < varVals.length; i++) {
|
||||
if (varVals[i] === 0) {
|
||||
nextVar = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextVar === 0) throw new Error("Error in satisfiableAt");
|
||||
|
||||
if (
|
||||
satisfiableAt(clauses, varVals, nextVar, 1) ||
|
||||
satisfiableAt(clauses, varVals, nextVar, -1)
|
||||
)
|
||||
// not necessary to restore split var: search is halted anyway
|
||||
return true;
|
||||
|
||||
// restore split var
|
||||
varVals[nextVar] = 0;
|
||||
|
||||
// restore derived vars
|
||||
for (let i = 0; i < propRes.length; i++) varVals[propRes[i]] = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether clauses are satisfiable under the varVals assignment, without
|
||||
* search.
|
||||
*
|
||||
* Iterate through all clauses and check whether
|
||||
*
|
||||
* - all literals in the clause are assigned false: undo derived units (set
|
||||
* values in varVals to 0) and return false immediately
|
||||
*
|
||||
* - a clause contains a literal assigned true: skip further actions on this
|
||||
* clause
|
||||
*
|
||||
* - a single literal in the clause is unassigned, all the rest are assigned
|
||||
* false: derive this literal as a unit clause.
|
||||
*
|
||||
* In case at least one literal is derived during the process above, iterate
|
||||
* the whole process until no more new literals are derived.
|
||||
*
|
||||
* Returns:
|
||||
*
|
||||
* 1: all clauses were assigned true, hence the clause set is satisfiable.
|
||||
* The whole search will terminate after that.
|
||||
*
|
||||
* -1: clause set is contradictory. Derived units have been restored.
|
||||
*
|
||||
* a list of derived variables: the status of the clause set is
|
||||
* undetermined. The values of variables on the list will be restored (set to
|
||||
* 0) during backtracking.
|
||||
*/
|
||||
function unitPropagate(clauses, varVals) {
|
||||
// all vars with a value derived in the forthcoming main loop
|
||||
const derivedVars = [];
|
||||
|
||||
// the main loop is run, deriving new unit clauses, until either:
|
||||
// - the clause set is detected to be unsatisfiable
|
||||
// - the clause set is detected to be satisfiable
|
||||
// - there are no more derived unit clauses
|
||||
|
||||
for (;;) {
|
||||
let allClausesFound = true; // set to false if we find an undetermined clause
|
||||
const derivedQueue = []; // derived units (literals) during one iteration
|
||||
for (let i = 0; i < clauses.length; i++) {
|
||||
const clause = clauses[i];
|
||||
let clauseVal = 0;
|
||||
let unassignedCount = 0; // count unassigned vars in clause
|
||||
let unassignedLit = 0; // 0 means none found for this clause
|
||||
|
||||
for (let j = 0; j < clause.length; j++) {
|
||||
const lit = clause[j];
|
||||
let nr, polarity;
|
||||
if (lit < 0) {
|
||||
nr = 0 - lit;
|
||||
polarity = -1;
|
||||
} else {
|
||||
nr = lit;
|
||||
polarity = 1;
|
||||
}
|
||||
|
||||
if (varVals[nr] === polarity) {
|
||||
clauseVal = 1;
|
||||
break;
|
||||
} else if (varVals[nr] === 0) {
|
||||
unassignedCount++;
|
||||
unassignedLit = lit;
|
||||
}
|
||||
}
|
||||
|
||||
if (clauseVal === 1) continue; // clause is subsumed
|
||||
|
||||
// clause is not subsumed by varVals
|
||||
if (unassignedCount === 0) {
|
||||
for (let j = 0; j < derivedVars.length; j++)
|
||||
varVals[derivedVars[j]] = 0; // restore derived
|
||||
return -1;
|
||||
} else if (unassignedCount === 1) {
|
||||
// unassignedLit is a derived literal
|
||||
derivedQueue.push(unassignedLit);
|
||||
}
|
||||
|
||||
if (unassignedCount !== 0) allClausesFound = false;
|
||||
}
|
||||
|
||||
// if all clauses were subsumed or all the variables in a clause had a value
|
||||
// no need to restore varVals: whole search finished
|
||||
if (allClausesFound) return 1;
|
||||
|
||||
// in case there are any derived units present, store to varVals and
|
||||
// iterate the main loop
|
||||
if (derivedQueue.length === 0) break;
|
||||
|
||||
// some units were derived: iterate the main loop
|
||||
for (let j = 0; j < derivedQueue.length; j++) {
|
||||
const lit = derivedQueue[j];
|
||||
let nr, polarity;
|
||||
if (lit < 0) {
|
||||
nr = 0 - lit;
|
||||
polarity = -1;
|
||||
} else {
|
||||
nr = lit;
|
||||
polarity = 1;
|
||||
}
|
||||
varVals[nr] = polarity;
|
||||
derivedVars.push(nr);
|
||||
}
|
||||
}
|
||||
|
||||
return derivedVars;
|
||||
}
|
||||
|
||||
function naiveDpll(clauses, maxvarnr) {
|
||||
// variable values are 0 if not set, 1 if positive, -1 if negative
|
||||
const varVals = new Int32Array(maxvarnr + 1);
|
||||
return satisfiableAt(clauses, varVals, 0, 0);
|
||||
}
|
||||
|
||||
exports.naiveDpll = naiveDpll;
|
||||
@@ -0,0 +1,5 @@
|
||||
function pass() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
module.exports = pass;
|
||||
@@ -0,0 +1,14 @@
|
||||
function test(resourceType, resource) {
|
||||
if (resourceType === "devices") {
|
||||
if (resource["Tags.test"] && resource["Tags.test"].value[0]) return 1;
|
||||
} else if (resourceType === "presets") {
|
||||
if (resource["_id"].startsWith("test-"))
|
||||
// TODO check "test" tag too
|
||||
return 1;
|
||||
} else if (resourceType === "provisions") {
|
||||
if (resource["_id"].startsWith("test-")) return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
module.exports = test;
|
||||
Reference in new issue
Block a user