Bundling code now with Rollup for its flat bundling and tree shaking support. Front end code still goes through Webpack for browser support.
386 lines
10 KiB
JavaScript
386 lines
10 KiB
JavaScript
/**
|
|
* Copyright 2013-2018 Zaid Abdulla
|
|
*
|
|
* This file is part of GenieACS.
|
|
*
|
|
* GenieACS is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* GenieACS is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
"use strict";
|
|
|
|
import * as common from "./common";
|
|
|
|
function stringToRegexp(input, flags) {
|
|
if (input.indexOf("*") === -1) return false;
|
|
|
|
let output = input.replace(/[[\]\\^$.|?+()]/, "\\$&");
|
|
if (output[0] === "*") output = output.replace(/^\*+/g, "");
|
|
else output = "^" + output;
|
|
|
|
if (output[output.length - 1] === "*") output = output.replace(/\*+$/g, "");
|
|
else output = output + "$";
|
|
|
|
output = output.replace(/[*]/, ".*");
|
|
return new RegExp(output, flags);
|
|
}
|
|
|
|
function normalize(input) {
|
|
if (common.typeOf(input) === common.STRING_TYPE) {
|
|
const vals = [input];
|
|
const m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input);
|
|
if (m) vals.push({ $regex: new RegExp(m[1], m[2]) });
|
|
|
|
if (+input === parseFloat(input)) vals.push(+input);
|
|
|
|
const d = new Date(input);
|
|
if (input.length >= 8 && d.getFullYear() > 1983) vals.push(d);
|
|
|
|
const r = stringToRegexp(input);
|
|
if (r !== false) vals.push({ $regex: r });
|
|
|
|
return vals;
|
|
}
|
|
return input;
|
|
}
|
|
|
|
export function expandValue(value) {
|
|
if (common.typeOf(value) === common.ARRAY_TYPE) {
|
|
let a = [];
|
|
for (const j of value) a = a.conact(expandValue(j));
|
|
return [a];
|
|
} else if (common.typeOf(value) !== common.OBJECT_TYPE) {
|
|
const n = normalize(value);
|
|
if (common.typeOf(n) !== common.ARRAY_TYPE) return [n];
|
|
else return n;
|
|
}
|
|
|
|
const objs = [];
|
|
const indices = [];
|
|
const keys = [];
|
|
const values = [];
|
|
for (const [k, v] of Object.entries(value)) {
|
|
keys.push(k);
|
|
values.push(expandValue(v));
|
|
indices.push(0);
|
|
}
|
|
|
|
let i = 0;
|
|
while (i < indices.length) {
|
|
const obj = {};
|
|
for (let j = 0; j < keys.length; ++j) obj[keys[j]] = values[j][indices[j]];
|
|
objs.push(obj);
|
|
|
|
for (i = 0; i < indices.length; ++i) {
|
|
indices[i] += 1;
|
|
if (indices[i] < values[i].length) break;
|
|
indices[i] = 0;
|
|
}
|
|
}
|
|
return objs;
|
|
}
|
|
|
|
function permute(param, val) {
|
|
const conditions = [];
|
|
const values = expandValue(val);
|
|
|
|
if (param[param.lastIndexOf(".") + 1] !== "_") param += "._value";
|
|
|
|
for (const v of values) {
|
|
const obj = {};
|
|
obj[param] = v;
|
|
conditions.push(obj);
|
|
}
|
|
|
|
return conditions;
|
|
}
|
|
|
|
export function expand(query) {
|
|
const newQuery = {};
|
|
for (const [k, v] of Object.entries(query)) {
|
|
if (k[0] === "$") {
|
|
// Operator
|
|
newQuery[k] = v.map(e => expand(e));
|
|
} else {
|
|
const conditions = permute(k, v);
|
|
if (conditions.length > 1) {
|
|
newQuery["$and"] = newQuery["$and"] || [];
|
|
if (v && (v["$ne"] != null || v["$not"] != null)) {
|
|
if (Object.keys(v).length > 1)
|
|
throw new Error("Cannot mix $ne or $not with other operators");
|
|
for (const c of conditions) newQuery["$an"].push(c);
|
|
} else {
|
|
newQuery["$and"].push({ $or: conditions });
|
|
}
|
|
} else {
|
|
Object.assign(newQuery, conditions[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return newQuery;
|
|
}
|
|
|
|
function testExpressions(params, expressions, lop) {
|
|
for (const f of expressions) {
|
|
const res = test(params, f);
|
|
switch (lop) {
|
|
case "$and":
|
|
if (!res) return false;
|
|
break;
|
|
case "$or":
|
|
if (res) return true;
|
|
break;
|
|
case "$nor":
|
|
if (res) return false;
|
|
}
|
|
}
|
|
|
|
switch (lop) {
|
|
case "$and":
|
|
return true;
|
|
case "$or":
|
|
return false;
|
|
case "$nor":
|
|
return true;
|
|
default:
|
|
throw new Error("Unknown logical operator");
|
|
}
|
|
}
|
|
|
|
export function test(params, query) {
|
|
let res;
|
|
for (const [k, v] of Object.entries(query)) {
|
|
if (k.charAt(0) === "$") {
|
|
// Logical operator
|
|
res = testExpressions(params, v, k);
|
|
} else {
|
|
const value = params[k];
|
|
if (common.typeOf(v) !== common.OBJECT_TYPE) {
|
|
if (common.typeOf(value) === common.ARRAY_TYPE) {
|
|
// TODO comparing array to regex, array to array, and object to object
|
|
res = value.indexOf(v) !== -1;
|
|
} else {
|
|
if (common.typeOf(v) === common.REGEXP_TYPE) res = v.test(value);
|
|
else res = v === value;
|
|
}
|
|
} else {
|
|
for (const [k2, v2] of Object.entries(v)) {
|
|
switch (k2) {
|
|
case "$ne":
|
|
if (common.typeOf(value) === common.ARRAY_TYPE)
|
|
res = value.indexOf(v2) === -1;
|
|
else res = value !== v2;
|
|
break;
|
|
case "$lt":
|
|
res = value < v2;
|
|
break;
|
|
case "$lte":
|
|
res = value <= v2;
|
|
break;
|
|
case "$gt":
|
|
res = value > v2;
|
|
break;
|
|
case "$gte":
|
|
res = value >= v2;
|
|
break;
|
|
case "$regex":
|
|
res = v2.test(value);
|
|
break;
|
|
case "$in":
|
|
throw new Error("Operator not supported");
|
|
case "$nin":
|
|
throw new Error("Operator not supported");
|
|
case "$all":
|
|
throw new Error("Operator not supported");
|
|
case "$exists":
|
|
throw new Error("Operator not supported");
|
|
default:
|
|
throw new Error("Operator not supported");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!res) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function matchType(src, dst) {
|
|
const type = typeof src;
|
|
if (type === "string") {
|
|
return String(dst);
|
|
} else if (type === "number") {
|
|
if (+dst === parseFloat(dst)) return +dst;
|
|
else if (!isNaN(Date.parse(dst))) return Date.parse(dst);
|
|
} else if (type === "boolean") {
|
|
const v = String(dst)
|
|
.trim()
|
|
.toLowerCase();
|
|
if (v === "true" || v === "1") return true;
|
|
else if (v === "false" || v === "0") return false;
|
|
}
|
|
return dst;
|
|
}
|
|
|
|
export function testFilter(obj, filter) {
|
|
for (const [k, v] of Object.entries(filter)) {
|
|
const [param, op] = k.split(/([^a-zA-Z0-9\-_.].*)/, 2);
|
|
const val = matchType(obj[param], v);
|
|
switch (op) {
|
|
case "=":
|
|
case undefined:
|
|
if (obj[param] !== val) return false;
|
|
break;
|
|
case ">":
|
|
if (!(obj[param] > val)) return false;
|
|
break;
|
|
case ">=":
|
|
if (!(obj[param] >= val)) return false;
|
|
break;
|
|
case "<":
|
|
if (!(obj[param] < val)) return false;
|
|
break;
|
|
case "<=":
|
|
if (!(obj[param] <= val)) return false;
|
|
break;
|
|
case "!":
|
|
if (obj[param] === val) return false;
|
|
break;
|
|
default:
|
|
throw new Error(`Unrecognized operator ${op}`);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function convertMongoQueryToFilters(query, filters) {
|
|
filters = filters || {};
|
|
for (let [k, v] of Object.entries(query)) {
|
|
if (k[0] === "$") {
|
|
if (k === "$and")
|
|
for (const vv of v) convertMongoQueryToFilters(vv, filters);
|
|
else throw new Error(`Operator ${k} not supported`);
|
|
} else if (k === "_tags") {
|
|
if (common.typeOf(v) === common.OBJECT_TYPE) {
|
|
for (let [kk, vv] of Object.entries(v)) {
|
|
vv = vv.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
if (kk === "$ne") filters[`Tags.${vv}!`] = true;
|
|
else throw new Error(`Operator ${kk} not supported`);
|
|
}
|
|
} else {
|
|
v = v.replace(/[^a-zA-Z0-9-]+/g, "_");
|
|
filters[`Tags.${v}`] = true;
|
|
}
|
|
} else {
|
|
switch (k) {
|
|
case "_id":
|
|
k = "DeviceID.ID";
|
|
break;
|
|
case "_deviceId._Manufacturer":
|
|
k = "DeviceID.Manufacturer";
|
|
break;
|
|
case "_deviceId._OUI":
|
|
k = "DeviceID.OUI";
|
|
break;
|
|
case "_deviceId._ProductClass":
|
|
k = "DeviceID.ProductClass";
|
|
break;
|
|
case "_deviceId._SerialNumber":
|
|
k = "DeviceID.SerialNumber";
|
|
break;
|
|
case "_lastInform":
|
|
k = "Events.Inform";
|
|
break;
|
|
case "_lastBootstrap":
|
|
k = "Events.0_BOOTSTRAP";
|
|
break;
|
|
case "_lastBoot":
|
|
k = "Events.1_BOOT";
|
|
break;
|
|
case "_registered":
|
|
k = "Events.Registered";
|
|
}
|
|
|
|
if (common.typeOf(v) === common.OBJECT_TYPE) {
|
|
for (const [kk, vv] of Object.entries(v)) {
|
|
switch (kk) {
|
|
case "$eq":
|
|
filters[k] = vv;
|
|
break;
|
|
case "$ne":
|
|
filters[`${k}!`] = vv;
|
|
break;
|
|
case "$lt":
|
|
filters[`${k}<`] = vv;
|
|
break;
|
|
case "$lte":
|
|
filters[`${k}<=`] = vv;
|
|
break;
|
|
case "$gt":
|
|
filters[`${k}>`] = vv;
|
|
break;
|
|
case "$gte":
|
|
filters[`${k}>=`] = vv;
|
|
break;
|
|
default:
|
|
throw new Error(`Oprator ${kk} not supported`);
|
|
}
|
|
}
|
|
} else {
|
|
filters[k] = v;
|
|
}
|
|
}
|
|
}
|
|
|
|
return filters;
|
|
}
|
|
|
|
export function sanitizeQueryTypes(query, types) {
|
|
for (const [k, v] of Object.entries(query)) {
|
|
if (k[0] === "$") {
|
|
// Logical operator
|
|
for (const vv of v) sanitizeQueryTypes(vv, types);
|
|
} else if (k in types) {
|
|
if (common.typeOf(v) === common.OBJECT_TYPE) {
|
|
for (const [kk, vv] of Object.entries(v)) {
|
|
switch (kk) {
|
|
case "$in":
|
|
case "$nin":
|
|
for (let i = 0; i < vv.length; ++i) vv[i] = types[k](vv[i]);
|
|
break;
|
|
case "$eq":
|
|
case "$gt":
|
|
case "$gte":
|
|
case "$lt":
|
|
case "$lte":
|
|
case "$ne":
|
|
v[kk] = types[k](vv);
|
|
break;
|
|
case "$exists":
|
|
case "$type":
|
|
// Ignore
|
|
break;
|
|
default:
|
|
throw new Error("Operator not supported");
|
|
}
|
|
}
|
|
} else {
|
|
query[k] = types[k](query[k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return query;
|
|
}
|