"use strict"; const mongodb = require("mongodb"); const expressionParser = require("../common/expression-parser"); const expression = require("../common/expression"); function processDeviceFilter(filter) { return expressionParser.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 = expressionParser.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") { let alt = e.slice(); alt[2] = new Date(e[2]); e = ["OR", e, alt]; } return e; } return exp; }); } function processTasksFilter(filter) { return expressionParser.map(filter, exp => { if (!Array.isArray(exp)) return exp; if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { let e = exp.slice(); if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new mongodb.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; }); } function processFaultsFilter(filter) { return expressionParser.map(filter, exp => { if (!Array.isArray(exp)) return exp; if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { let 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; }); } 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]; let 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 = expression.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; } 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; } 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 === "Events.Inform") s["_lastInform"] = v; else s[k] = v; return s; } function flattenDevice(device) { function recursive(input, root, output, timestamp) { for (let [name, tree] of Object.entries(input)) { if (!root) if (name === "_lastInform") { output["Events.Inform"] = { value: [Date.parse(tree), "xsd:dateTime"], valueTimestamp: timestamp, writable: false, writableTimestamp: timestamp, object: false, objectTimestamp: timestamp }; } else if (name === "_registered") { output["Events.Registered"] = { value: [Date.parse(tree), "xsd:dateTime"], valueTimestamp: timestamp, writable: false, writableTimestamp: timestamp, object: false, objectTimestamp: timestamp }; } else if (name === "_lastBoot") { output["Events.1_BOOT"] = { value: [Date.parse(tree), "xsd:dateTime"], valueTimestamp: timestamp, writable: false, writableTimestamp: timestamp, object: false, objectTimestamp: timestamp }; } else if (name === "_lastBootstrap") { output["Events.0_BOOTSTRAP"] = { value: [Date.parse(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 (let 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"], 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; } let r = root ? `${root}.${name}` : name; output[r] = attrs; if (attrs["object"]) recursive(tree, r, output, childrenTimestamp); } } const newDevice = {}; const timestamp = new Date(device["_lastInform"] || 1).getTime(); recursive(device, "", newDevice, timestamp); return newDevice; } function flattenFault(fault) { const f = Object.assign({}, fault); if (f.timestamp) f.timestamp = +f.timestamp; if (f.expiry) f.expiry = +f.expiry; if (f.detail) { for (let [k, v] of Object.entries(f.detail)) fault[`detail.${k}`] = v; delete f.detail; } return f; } function flattenTask(task) { let 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) { let expressions = []; function recursive(_query) { for (let [k, v] of Object.entries(_query)) if (k[0] == "$") { if (k == "$and") for (let 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 (let [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 (let [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; } function flattenPreset(preset) { let p = Object.assign({}, preset); if (p.precondition) { p.precondition = mongoQueryToFilter(JSON.parse(p.precondition)); p.precondition = p.precondition.length ? expression.stringify(p.precondition) : ""; } if (p.events) { let e = []; for (let [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`); p.events = e.join(", "); } let 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; } function flattenFile(file) { let 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; } function preProcessPreset(data) { let preset = Object.assign({}, data); preset.precondition = preset.precondition ? filterToMongoQuery(expression.parse(preset.precondition)) : {}; preset.precondition = JSON.stringify(preset.precondition); let 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"); let 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; } function preProcessProvision(data) { return data.script; } function preProcessVirtualParameters(data) { return data.script; } exports.processDeviceFilter = processDeviceFilter; exports.processTasksFilter = processTasksFilter; exports.processFaultsFilter = processFaultsFilter; exports.filterToMongoQuery = filterToMongoQuery; exports.processDeviceProjection = processDeviceProjection; exports.processDeviceSort = processDeviceSort; exports.flattenDevice = flattenDevice; exports.flattenFault = flattenFault; exports.flattenTask = flattenTask; exports.flattenPreset = flattenPreset; exports.flattenFile = flattenFile; exports.preProcessPreset = preProcessPreset; exports.preProcessProvision = preProcessProvision; exports.preProcessVirtualParameters = preProcessVirtualParameters;