Convert query.coffee to JS

This commit is contained in:
Zaid Abdulla committed 2018-08-21 21:00:32 -07:00
1 parent 269f57dec1
commit 3bde08671d
3 files changed
+427 -373

No files matched your search

-372
View File
@@ -1,372 +0,0 @@
###
# Copyright 2013-2017 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/>.
#
# This file incorporates work covered by the following copyright and
# permission notice:
#
# Copyright 2013 Fanoos Telecom
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
###
common = require './common'
mongodb = require 'mongodb'
stringToRegexp = (input, flags) ->
if (input.indexOf('*') == -1)
return false
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)
normalize = (input) ->
if common.typeOf(input) is common.STRING_TYPE
vals = [input]
if (m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input))
vals.push({'$regex' : new RegExp(m[1], m[2])})
if +input == parseFloat(input)
vals.push(+input)
d = new Date(input)
if input.length >= 8 and d.getFullYear() > 1983
vals.push(d)
r = stringToRegexp(input)
if r isnt false
vals.push({'$regex' : r})
return vals
return input
expandValue = (value) ->
if common.typeOf(value) is common.ARRAY_TYPE
a = []
for j in value
a = a.concat(expandValue(j))
return [a]
else if common.typeOf(value) isnt common.OBJECT_TYPE
n = normalize(value)
if common.typeOf(n) isnt common.ARRAY_TYPE
return [n]
else
return n
objs = []
indices = []
keys = []
values = []
for k,v of value
keys.push(k)
values.push(expandValue(v))
indices.push(0)
i = 0
while i < indices.length
obj = {}
for i in [0...keys.length]
obj[keys[i]] = values[i][indices[i]]
objs.push(obj)
for i in [0...indices.length]
indices[i] += 1
if indices[i] < values[i].length
break
indices[i] = 0
return objs
permute = (param, val) ->
conditions = []
values = expandValue(val)
if param[param.lastIndexOf('.') + 1] != '_'
param += '._value'
for v in values
obj = {}
obj[param] = v
conditions.push(obj)
return conditions
expand = (query) ->
new_query = {}
for k,v of query
if k[0] == '$' # operator
expressions = []
for e in v
expressions.push(expand(e))
new_query[k] = expressions
else
conditions = permute(k, v)
if conditions.length > 1
new_query['$and'] ?= []
if v?['$ne']? or v?['$not']?
if Object.keys(v) > 1
throw new Error("Cannot mix $ne or $not with other operators")
for c in conditions
new_query['$and'].push(c)
else
new_query['$and'].push({'$or' : conditions})
else
Object.assign(new_query, conditions[0])
return new_query
# Generate parameter projection from a query
# If second arg is given, it's edited and returned
queryProjection = (query, proj) ->
proj ?= {}
for k,v of query
if k.charAt(0) == '$' # this is a logical operator
for q in v
queryProjection(q, proj)
else
proj[k] = 1
return proj
testExpressions = (params, expressions, lop) ->
for f in expressions
res = test(params, f)
switch lop
when '$and'
return false if not res
when '$or'
return true if res
when '$nor'
return false if res
return switch lop
when '$and' then true
when '$or' then false
when '$nor' then true
else throw new Error('Unknown logical operator')
test = (params, query) ->
for k,v of query
if k.charAt(0) == '$' # this is a logical operator
res = testExpressions(params, v, k)
else
value = params[k]
if common.typeOf(v) isnt common.OBJECT_TYPE
# TODO comparing array to regex, array to array, and object to object
if common.typeOf(value) is common.ARRAY_TYPE
res = value.indexOf(v) != -1
else
if common.typeOf(v) is common.REGEXP_TYPE
res = v.test(value)
else
res = v == value
else
for k2,v2 of v
switch k2
when '$ne'
if common.typeOf(value) is common.ARRAY_TYPE
res = value.indexOf(v2) == -1
else
res = value != v2
when '$lt'
res = value < v2
when '$lte'
res = value <= v2
when '$gt'
res = value > v2
when '$gte'
res = value >= v2
when '$regex'
res = v2.test(value)
when '$in'
throw new Error('Operator not supported')
when '$nin'
throw new Error('Operator not supported')
when '$all'
throw new Error('Operator not supported')
when '$exists'
throw new Error('Operator not supported')
else
throw new Error('Operator not supported')
if not res
return false
return true
matchType = (src, dst) ->
if typeof src is 'string'
return String(dst)
else if typeof src is 'number'
if +dst == parseFloat(dst)
return +dst
else if not isNaN(Date.parse(dst))
return Date.parse(dst)
else if typeof src is 'boolean'
v = String(dst).trim()
if v is 'true' or v is 'TRUE' or v is 'True' or v is '1'
return true
else if v is 'false' or v is 'FALSE' or v is 'False' or v is '0'
return false
return dst
testFilter = (obj, filter) ->
for k, v of filter
[param, op] = k.split(/([^a-zA-Z0-9\-\_\.].*)/, 2)
val = matchType(obj[param], v)
switch op
when '=', undefined
return false if obj[param] != val
when '>'
return false if not (obj[param] > val)
when '>='
return false if not (obj[param] >= val)
when '<'
return false if not (obj[param] < val)
when '<='
return false if not (obj[param] <= val)
when '!'
return false if obj[param] == val
else
throw new Error("Unrecognized operator #{op}")
return true
convertMongoQueryToFilters = (query, filters) ->
filters ?= {}
for k, v of query
if k[0] == '$'
if k == '$and'
for vv in v
convertMongoQueryToFilters(vv, filters)
else
throw new Error("Operator #{k} not supported")
else if k == '_tags'
if common.typeOf(v) is common.OBJECT_TYPE
for kk, vv of v
vv = vv.replace(/[^a-zA-Z0-9\-]+/g, '_')
switch kk
when '$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
when '_id' then k = 'DeviceID.ID'
when '_deviceId._Manufacturer' then k = 'DeviceID.Manufacturer'
when '_deviceId._OUI' then k = 'DeviceID.OUI'
when '_deviceId._ProductClass' then k = 'DeviceID.ProductClass'
when '_deviceId._SerialNumber' then k = 'DeviceID.SerialNumber'
when '_lastInform' then k = 'Events.Inform'
when '_lastBootstrap' then k = 'Events.0_BOOTSTRAP'
when '_lastBoot' then k = 'Events.1_BOOT'
when '_registered' then k = 'Events.Registered'
if common.typeOf(v) is common.OBJECT_TYPE
for kk, vv of v
switch kk
when '$eq'
filters[k] = vv
when '$ne'
filters["#{k}!"] = vv
when '$lt'
filters["#{k}<"] = vv
when '$lte'
filters["#{k}<="] = vv
when '$gt'
filters["#{k}>"] = vv
when '$gte'
filters["#{k}>="] = vv
else
throw new Error("Oprator #{kk} not supported")
else
filters[k] = v
return filters
sanitizeQueryTypes = (query, types) ->
for k, v of query
if k[0] == '$' # logical operator
sanitizeQueryTypes(vv, types) for vv in v
else if k of types
if common.typeOf(v) is common.OBJECT_TYPE
for kk, vv of v
switch kk # operator
when '$in', '$nin'
vv[i] = types[k](vv[i]) for i in [0...vv.length]
when '$eq', '$gt', '$gte', '$lt', '$lte', '$ne'
v[kk] = types[k](vv)
when '$exists', '$type' then
# ignore
else
throw new Error('Operator not supported')
else
query[k] = types[k](query[k])
return query
exports.expand = expand
exports.queryProjection = queryProjection
exports.test = test
exports.sanitizeQueryTypes = sanitizeQueryTypes
exports.convertMongoQueryToFilters = convertMongoQueryToFilters
exports.testFilter = testFilter
+427
View File
@@ -0,0 +1,427 @@
/**
* 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";
const common = require("./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];
let 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);
let d = new Date(input);
if (input.length >= 8 && d.getFullYear() > 1983)
vals.push(d);
let r = stringToRegexp(input);
if (r !== false)
vals.push({"$regex": r});
return vals;
}
return input;
}
function expandValue(value) {
if (common.typeOf(value) === common.ARRAY_TYPE) {
const a = [];
for (let 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 (let [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 (let v of values) {
const obj = {}
obj[param] = v;
conditions.push(obj);
}
return conditions;
}
function expand(query) {
const newQuery = {};
for (let [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 (let 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");
}
}
function test(params, query) {
let res;
for (let [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) {
// TODO comparing array to regex, array to array, and object to object
if (common.typeOf(value) === common.ARRAY_TYPE) {
res = value.indexOf(v) !== -1;
} else {
if (common.typeOf(v) === common.REGEXP_TYPE) {
res = v.test(value);
} else {
res = v === value;
}
}
} else {
for (let [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");
break;
case "$nin":
throw new Error("Operator not supported");
break;
case "$all":
throw new Error("Operator not supported");
break;
case "$exists":
throw new Error("Operator not supported");
break;
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;
}
function testFilter(obj, filter) {
for (let [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;
}
function convertMongoQueryToFilters(query, filters) {
filters = filters || {}
for (let [k, v] of Object.entries(query)) {
if (k[0] === "$") {
if (k === "$and")
for (let 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 (let [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;
}
function sanitizeQueryTypes(query, types) {
for (let [k, v] of Object.entries(query)) {
if (k[0] === "$") { // Logical operator
for (let vv of v)
sanitizeQueryTypes(vv, types);
} else if (k in types) {
if (common.typeOf(v) === common.OBJECT_TYPE) {
for (let [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;
}
exports.expand = expand;
exports.test = test;
exports.sanitizeQueryTypes = sanitizeQueryTypes;
exports.convertMongoQueryToFilters = convertMongoQueryToFilters;
exports.testFilter = testFilter;