Bundling code now with Rollup for its flat bundling and tree shaking support. Front end code still goes through Webpack for browser support.
533 lines
16 KiB
JavaScript
533 lines
16 KiB
JavaScript
"use strict";
|
|
import { ObjectID } from "mongodb";
|
|
import { map, parse, stringify } from "../common/expression-parser";
|
|
import { likePatternToRegExp } from "../common/expression";
|
|
|
|
export function processDeviceFilter(filter) {
|
|
return map(filter, exp => {
|
|
if (!Array.isArray(exp)) return exp;
|
|
|
|
if (exp[0] === "PARAM") {
|
|
const p = exp[1];
|
|
if (p === "DeviceID.ID") return ["PARAM", "_id"];
|
|
else if (p === "DeviceID") return ["PARAM", "_deviceId"];
|
|
else if (p.startsWith("DeviceID."))
|
|
return ["PARAM", "_deviceId._" + p.slice(9)];
|
|
else if (p === "Tags") return ["PARAM", "_tags"];
|
|
else if (p === "tag") return ["PARAM", "_tags"];
|
|
else if (p === "Events.Inform") return ["PARAM", "_lastInform"];
|
|
else if (p === "Events.Registered") return ["PARAM", "_registered"];
|
|
else if (p === "Events.0_BOOTSTRAP") return ["PARAM", "_lastBootstrap"];
|
|
else if (p === "Events.1_BOOT") return ["PARAM", "_lastBoot"];
|
|
} else if (
|
|
Array.isArray(exp[1]) &&
|
|
exp[1][0] === "PARAM" &&
|
|
exp[1][1].startsWith("Tags.")
|
|
) {
|
|
const t = exp[1][1].slice(5);
|
|
if (exp[0] === "IS NULL") return ["<>", ["PARAM", "_tags"], t];
|
|
else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t];
|
|
else if (exp[0] === "=" && exp[2] === true)
|
|
return ["=", ["PARAM", "_tags"], t];
|
|
} else if (
|
|
["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0])
|
|
) {
|
|
let e = map(exp, ee => {
|
|
if (
|
|
Array.isArray(ee) &&
|
|
ee[0] === "PARAM" &&
|
|
typeof ee[1] === "string" &&
|
|
!ee[1].startsWith("_")
|
|
)
|
|
return ["PARAM", `${ee[1]}._value`];
|
|
return ee;
|
|
});
|
|
|
|
if (typeof e[2] === "number") {
|
|
const alt = e.slice();
|
|
alt[2] = new Date(e[2]);
|
|
e = ["OR", e, alt];
|
|
}
|
|
return e;
|
|
}
|
|
return exp;
|
|
});
|
|
}
|
|
|
|
export function processTasksFilter(filter) {
|
|
return map(filter, exp => {
|
|
if (!Array.isArray(exp)) return exp;
|
|
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
|
|
const e = exp.slice();
|
|
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]);
|
|
else if (e[1][0] === "PARAM" && e[1][1] === "timestamp")
|
|
e[2] = new Date(e[2]);
|
|
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
|
|
e[2] = new Date(e[2]);
|
|
return e;
|
|
}
|
|
return exp;
|
|
});
|
|
}
|
|
|
|
export function processFaultsFilter(filter) {
|
|
return map(filter, exp => {
|
|
if (!Array.isArray(exp)) return exp;
|
|
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
|
|
const e = exp.slice();
|
|
if (e[1][0] === "PARAM" && e[1][1] === "timestamp") e[2] = new Date(e[2]);
|
|
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
|
|
e[2] = new Date(e[2]);
|
|
return e;
|
|
}
|
|
return exp;
|
|
});
|
|
}
|
|
|
|
export function filterToMongoQuery(filter, negate = false, res = {}) {
|
|
const op = filter[0];
|
|
|
|
if ((!negate && op === "AND") || (negate && op === "OR")) {
|
|
res["$and"] = res["$and"] || [];
|
|
for (let i = 1; i < filter.length; ++i)
|
|
res["$and"].push(filterToMongoQuery(filter[i], negate));
|
|
} else if ((!negate && op === "OR") || (negate && op === "AND")) {
|
|
res["$or"] = res["$or"] || [];
|
|
|
|
for (let i = 1; i < filter.length; ++i)
|
|
res["$or"].push(filterToMongoQuery(filter[i], negate));
|
|
} else if (op === "NOT") {
|
|
filterToMongoQuery(filter[1], !negate, res);
|
|
} else if (op === "=") {
|
|
const param = filter[1][1];
|
|
const p = (res[param] = res[param] || {});
|
|
if (negate) p["$ne"] = filter[2];
|
|
else p["$eq"] = filter[2];
|
|
} else if (op === "<>") {
|
|
const param = filter[1][1];
|
|
let p = (res[param] = res[param] || {});
|
|
if (negate) p = p["$not"] = p["$not"] || {};
|
|
p["$ne"] = filter[2];
|
|
p["$exists"] = true;
|
|
} else if (op === ">") {
|
|
const param = filter[1][1];
|
|
let p = (res[param] = res[param] || {});
|
|
if (negate) p = p["$not"] = p["$not"] || {};
|
|
p["$gt"] = filter[2];
|
|
} else if (op === ">=") {
|
|
const param = filter[1][1];
|
|
let p = (res[param] = res[param] || {});
|
|
if (negate) p = p["$not"] = p["$not"] || {};
|
|
p["$gte"] = filter[2];
|
|
} else if (op === "<") {
|
|
const param = filter[1][1];
|
|
let p = (res[param] = res[param] || {});
|
|
if (negate) p = p["$not"] = p["$not"] || {};
|
|
p["$lt"] = filter[2];
|
|
} else if (op === "<=") {
|
|
const param = filter[1][1];
|
|
let p = (res[param] = res[param] || {});
|
|
if (negate) p = p["$not"] = p["$not"] || {};
|
|
p["$lte"] = filter[2];
|
|
} else if (op === "IS NULL") {
|
|
const param = filter[1][1];
|
|
res[param] = { $exists: negate };
|
|
} else if (op === "IS NOT NULL") {
|
|
const param = filter[1][1];
|
|
res[param] = { $exists: !negate };
|
|
} else if (op === "LIKE" || op === "NOT LIKE") {
|
|
if (op === "NOT LIKE") negate = !negate;
|
|
let param;
|
|
let flags;
|
|
if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") {
|
|
if (filter[2] !== filter[2].toUpperCase()) {
|
|
throw new Error(
|
|
"Cannot compare UPPER() against non upper case pattern"
|
|
);
|
|
}
|
|
param = filter[1][2][1];
|
|
flags = "i";
|
|
} else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") {
|
|
if (filter[2] !== filter[2].toLowerCase()) {
|
|
throw new Error(
|
|
"Cannot compare LOWER() against non lower case pattern"
|
|
);
|
|
}
|
|
param = filter[1][2][1];
|
|
flags = "i";
|
|
} else {
|
|
param = filter[1][1];
|
|
flags = "";
|
|
}
|
|
const r = likePatternToRegExp(filter[2], filter[3], flags);
|
|
if (negate) res[param] = { $not: r };
|
|
else res[param] = r;
|
|
} else {
|
|
throw new Error(`Unrecognized operator ${op}`);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
export function processDeviceProjection(projection) {
|
|
if (!projection) return projection;
|
|
const p = {};
|
|
for (const [k, v] of Object.entries(projection)) {
|
|
if (k === "DeviceID.ID") {
|
|
p["_id"] = 1;
|
|
} else if (k.startsWith("DeviceID")) {
|
|
p["_deviceId._SerialNumber"] = v;
|
|
p["_deviceId._OUI"] = v;
|
|
p["_deviceId._ProductClass"] = v;
|
|
p["_deviceId._Manufacturer"] = v;
|
|
} else if (k.startsWith("Tags")) {
|
|
p["_tags"] = v;
|
|
} else if (k.startsWith("Events")) {
|
|
p["_lastInform"] = v;
|
|
p["_registered"] = v;
|
|
p["_lastBoot"] = v;
|
|
p["_lastBootstrap"] = v;
|
|
} else {
|
|
p[k] = v;
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
export function processDeviceSort(sort) {
|
|
if (!sort) return sort;
|
|
const s = {};
|
|
for (const [k, v] of Object.entries(sort)) {
|
|
if (k === "DeviceID.ID") s["_id"] = v;
|
|
else if (k.startsWith("DeviceID.")) s[`_deviceId._${k.slice(9)}`] = v;
|
|
else if (k === "Events.Inform") s["_lastInform"] = v;
|
|
else if (k === "Events.Registered") s["_registered"] = v;
|
|
else if (k === "Events.1_BOOT") s["_lastBoot"] = v;
|
|
else if (k === "Events.0_BOOTSTRAP") s["_lastBootstrap"] = v;
|
|
else s[`${k}._value`] = v;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
function parseDate(d) {
|
|
const n = +d;
|
|
return isNaN(n) ? "" + d : n;
|
|
}
|
|
|
|
export function flattenDevice(device) {
|
|
function recursive(input, root, output, timestamp) {
|
|
for (const [name, tree] of Object.entries(input)) {
|
|
if (!root) {
|
|
if (name === "_lastInform") {
|
|
output["Events.Inform"] = {
|
|
value: [parseDate(tree), "xsd:dateTime"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
} else if (name === "_registered") {
|
|
output["Events.Registered"] = {
|
|
value: [parseDate(tree), "xsd:dateTime"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
} else if (name === "_lastBoot") {
|
|
output["Events.1_BOOT"] = {
|
|
value: [parseDate(tree), "xsd:dateTime"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
} else if (name === "_lastBootstrap") {
|
|
output["Events.0_BOOTSTRAP"] = {
|
|
value: [parseDate(tree), "xsd:dateTime"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
} else if (name === "_id") {
|
|
output["DeviceID.ID"] = {
|
|
value: [tree, "xsd:string"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
} else if (name === "_deviceId") {
|
|
output["DeviceID.Manufacturer"] = {
|
|
value: [tree["_Manufacturer"], "xsd:string"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
output["DeviceID.OUI"] = {
|
|
value: [tree["_OUI"], "xsd:string"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
output["DeviceID.ProductClass"] = {
|
|
value: [tree["_ProductClass"], "xsd:string"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
output["DeviceID.SerialNumber"] = {
|
|
value: [tree["_SerialNumber"], "xsd:string"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
} else if (name === "_tags") {
|
|
output["Tags"] = {
|
|
writable: true,
|
|
writableTimestamp: timestamp,
|
|
object: true,
|
|
objectTimestamp: timestamp
|
|
};
|
|
|
|
for (const t of tree) {
|
|
output[`Tags.${t}`] = {
|
|
value: [true, "xsd:boolean"],
|
|
valueTimestamp: timestamp,
|
|
writable: false,
|
|
writableTimestamp: timestamp,
|
|
object: false,
|
|
objectTimestamp: timestamp
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (name.startsWith("_")) continue;
|
|
|
|
let childrenTimestamp = timestamp;
|
|
|
|
if (!root) childrenTimestamp = +(input["_timestamp"] || 1);
|
|
else if (+input["_timestamp"] > timestamp)
|
|
childrenTimestamp = +input["_timestamp"];
|
|
|
|
const attrs = {};
|
|
if (tree["_value"] != null) {
|
|
attrs["value"] = [
|
|
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
|
|
tree["_type"]
|
|
];
|
|
attrs["valueTimestamp"] = +(tree["_timestamp"] || childrenTimestamp);
|
|
attrs["object"] = false;
|
|
attrs["objectTimestamp"] = childrenTimestamp;
|
|
} else if (tree["_object"] != null) {
|
|
attrs["object"] = tree["_object"];
|
|
attrs["objectTimestamp"] = childrenTimestamp;
|
|
}
|
|
|
|
if (tree["_writable"] != null) {
|
|
attrs["writable"] = tree["_writable"];
|
|
attrs["writableTimestamp"] = childrenTimestamp;
|
|
}
|
|
|
|
const r = root ? `${root}.${name}` : name;
|
|
output[r] = attrs;
|
|
|
|
if (attrs["object"] || tree["object"] == null)
|
|
recursive(tree, r, output, childrenTimestamp);
|
|
}
|
|
}
|
|
|
|
const newDevice = {};
|
|
const timestamp = new Date(device["_lastInform"] || 1).getTime();
|
|
recursive(device, "", newDevice, timestamp);
|
|
return newDevice;
|
|
}
|
|
|
|
export function flattenFault(fault) {
|
|
const f = Object.assign({}, fault);
|
|
if (f.timestamp) f.timestamp = +f.timestamp;
|
|
if (f.expiry) f.expiry = +f.expiry;
|
|
return f;
|
|
}
|
|
|
|
export function flattenTask(task) {
|
|
const t = Object.assign({}, task);
|
|
t._id = "" + t._id;
|
|
if (t.timestamp) t.timestamp = +t.timestamp;
|
|
if (t.expiry) t.expiry = +t.expiry;
|
|
return t;
|
|
}
|
|
|
|
function mongoQueryToFilter(query) {
|
|
const expressions = [];
|
|
function recursive(_query) {
|
|
for (const [k, v] of Object.entries(_query)) {
|
|
if (k[0] === "$") {
|
|
if (k === "$and") for (const vv of Object.values(v)) recursive(vv);
|
|
else throw new Error(`Operator ${k} not supported`);
|
|
} else if (k === "_tags") {
|
|
if (typeof v === "object") {
|
|
if (Array.isArray(v)) throw new Error(`Invalid type`);
|
|
|
|
for (const [kk, vv] of Object.entries(v)) {
|
|
if (kk === "$ne")
|
|
expressions.push(["IS NULL", ["PARAM", `Tags.${vv}`]]);
|
|
else throw new Error(`Operator ${kk} not supported`);
|
|
}
|
|
} else {
|
|
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]);
|
|
}
|
|
} else if (k.startsWith("Tags.")) {
|
|
expressions.push([
|
|
v["$exists"] ? "IS NOT NULL" : "IS NULL",
|
|
["PARAM", k]
|
|
]);
|
|
} else if (typeof v === "object") {
|
|
if (Array.isArray(v)) throw new Error(`Invalid type`);
|
|
|
|
for (const [kk, vv] of Object.entries(v)) {
|
|
let op;
|
|
switch (kk) {
|
|
case "$eq":
|
|
op = "=";
|
|
break;
|
|
case "$ne":
|
|
op = "<>";
|
|
break;
|
|
case "$lt":
|
|
op = "<";
|
|
break;
|
|
case "$lte":
|
|
op = "<=";
|
|
break;
|
|
case "$gt":
|
|
op = ">";
|
|
break;
|
|
case "$gte":
|
|
op = ">=";
|
|
break;
|
|
default:
|
|
throw new Error(`Operator ${kk} not supported`);
|
|
}
|
|
expressions.push([op, ["PARAM", k], vv]);
|
|
}
|
|
} else {
|
|
expressions.push(["=", ["PARAM", k], v]);
|
|
}
|
|
}
|
|
}
|
|
recursive(query);
|
|
|
|
if (expressions.length > 1) return ["AND"].concat(expressions);
|
|
|
|
if (expressions.length === 1) return expressions[0];
|
|
|
|
return expressions;
|
|
}
|
|
|
|
export function flattenPreset(preset) {
|
|
const p = Object.assign({}, preset);
|
|
if (p.precondition) {
|
|
p.precondition = mongoQueryToFilter(JSON.parse(p.precondition));
|
|
p.precondition = p.precondition.length ? stringify(p.precondition) : "";
|
|
}
|
|
|
|
if (p.events) {
|
|
const e = [];
|
|
for (const [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`);
|
|
p.events = e.join(", ");
|
|
}
|
|
|
|
const provision = p.configurations[0];
|
|
if (
|
|
p.configurations.length !== 1 ||
|
|
provision.type !== "provision" ||
|
|
!provision.name ||
|
|
!provision.name.length
|
|
)
|
|
throw new Error("Invalid preset provision");
|
|
p.provision = provision.name;
|
|
p.provisionArgs = provision.args
|
|
? JSON.stringify(provision.args).slice(1, -1)
|
|
: "";
|
|
delete p.configurations;
|
|
return p;
|
|
}
|
|
|
|
export function flattenFile(file) {
|
|
const f = {};
|
|
f["_id"] = file["_id"];
|
|
if (file.metadata) {
|
|
f["metadata.fileType"] = file.metadata.fileType || "";
|
|
f["metadata.oui"] = file.metadata.oui || "";
|
|
f["metadata.productClass"] = file.metadata.productClass || "";
|
|
f["metadata.version"] = file.metadata.version || "";
|
|
}
|
|
return f;
|
|
}
|
|
|
|
export function preProcessPreset(data) {
|
|
const preset = Object.assign({}, data);
|
|
preset.precondition = preset.precondition
|
|
? filterToMongoQuery(parse(preset.precondition))
|
|
: {};
|
|
preset.precondition = JSON.stringify(preset.precondition);
|
|
|
|
preset.weight = parseInt(preset.weight) || 0;
|
|
|
|
const events = {};
|
|
if (preset.events) {
|
|
for (let e of preset.events.split(",")) {
|
|
let v = true;
|
|
e = e.trim();
|
|
if (e.startsWith("-")) {
|
|
v = false;
|
|
e = e.slice(1).trim();
|
|
}
|
|
if (e) events[e] = v;
|
|
}
|
|
}
|
|
|
|
preset.events = events;
|
|
|
|
if (!preset.provision) throw new Error("Invalid preset provision");
|
|
|
|
const configuration = {
|
|
type: "provision",
|
|
name: preset.provision
|
|
};
|
|
|
|
if (preset.provisionArgs)
|
|
configuration.args = JSON.parse(`[${preset.provisionArgs}]`);
|
|
|
|
delete preset.provision;
|
|
delete preset.provisionArgs;
|
|
preset.configurations = [configuration];
|
|
return preset;
|
|
}
|
|
|
|
export function preProcessProvision(data) {
|
|
return data.script;
|
|
}
|
|
|
|
export function preProcessVirtualParameters(data) {
|
|
return data.script;
|
|
}
|