UNPKG

deep-nodes

Version:

Javascript object property descriptor and associate DSL : Object queries, RQL and traversal.

516 lines (504 loc) 13.1 kB
/** * A deep oriented implementation of RQL for JavaScript arrays based on rql/js-array from Kris Zyp (https://github.com/persvr/rql). * What's different from js-array ? It could handle schema properties, and node's type and/or depth when filtering. * It handle also dotted notation for properties path. * Schema, type, and depth need _deep_query_node_ to work. for this, use it through deep's chains. * @module deep * @submodule deep-rql * @example * rql([{a:{b:3}},{a:3}], "a.b=3") -> [{a:{b:3}] * @author Gilles Coomans <gilles.coomans@gmail.com> */ if (typeof define !== 'function') { var define = require('amdefine')(module); } define(["require", "deep-utils/lib/array", "rql/parser"], function(require, utils, parser) { "use strict"; function getJSPrimitiveType(obj) { if (obj && obj.forEach) return "array"; return typeof obj; }; var rqlParser = parser.parseQuery; var queryCache = {}; var rql = function(array, query) { if (query[0] == "?") query = query.substring(1); if (queryCache[query]) return queryCache[query].call(array); return rql.compile(query).call(array); }; rql.parse = function(input) { try { var r = rqlParser(input); r.toString = function() { return input; }; return r; } catch (e) { return null; } }; rql.compile = function(query) { var parsed = rql.parse(query); var func = rqlNodeToFunc(parsed); queryCache[query] = func; return func; }; rql.ops = { isPresent: function(path, items) { var res = []; var len = items.length; for (var i = 0; i < len; ++i) if (retrieve(items[i], path)) res.push(items[i]); return res; }, sort: function() { var terms = []; for (var i = 0; i < arguments.length; i++) { var sortAttribute = arguments[i]; var firstChar = sortAttribute.charAt(0); var term = { attribute: sortAttribute, ascending: true }; if (firstChar == "-" || firstChar == "+") { if (firstChar == "-") term.ascending = false; term.attribute = term.attribute.substring(1); } terms.push(term); } this.sort(function(a, b) { for (var term, i = 0; term = terms[i]; i++) { var ar = retrieve(a, term.attribute); var br = retrieve(b, term.attribute); if (ar != br) return term.ascending == ar > br ? 1 : -1; } return 0; }); return this; }, match: filter(function(value, regex) { return new RegExp(regex).test(value); }), "in": filter(function(value, values) { var ok = false; var count = 0; while (!ok && count < values.length) if (values[count++] == value) ok = true; return ok; }), out: filter(function(value, values) { var ok = true; var count = 0; while (ok && count < values.length) if (values[count++] == value) ok = false; return ok; }), contains: filter(function(array, value) { return utils.inArray(value, array); }), excludes: filter(function(array, value) { return !utils.inArray(value, array); }), or: function() { var items = []; //TODO: remove duplicates and use condition property for (var i = 0; i < arguments.length; ++i) { var a = arguments[i]; if (typeof a == 'function') items = items.concat(a.call(this)); else items = items.concat(rql.ops.isPresent(a, items)); } return items; }, and: function() { var items = this; for (var i = 0; i < arguments.length; ++i) { var a = arguments[i]; if (typeof a == 'function') items = a.call(items); else items = rql.ops.isPresent(a, items); } return items; }, select: function() { var args = arguments; var argc = arguments.length; var res = this.map(function(object) { var selected = {}; for (var i = 0; i < argc; i++) { var propertyName = args[i]; var value = evaluateProperty(object, propertyName); if (typeof value != "undefined") selected[propertyName] = value; } return selected; }); return res; }, /* _______________________________________________ WARNING : NOT IMPLEMENTED WITH PREFIX*/ unselect: function() { var args = arguments; var argc = arguments.length; return this.map(function(object) { var selected = {}; for (var i in object) if (object.hasOwnProperty(i)) selected[i] = object[i]; for (var i = 0; i < argc; i++) delete selected[args[i]]; return selected; }); }, values: function(first) { if (arguments.length == 1) return this.map(function(object) { return retrieve(object, first); }); var args = arguments; var argc = arguments.length; return this.map(function(object) { var realObject = retrieve(object); var selected = []; if (argc === 0) { for (var i in realObject) if (realObject.hasOwnProperty(i)) selected.push(realObject[i]); } else for (var i = 0; i < argc; i++) { var propertyName = args[i]; selected.push(realObject[propertyName]); } return selected; }); }, limit: function(limit, start, maxCount) { var totalCount = this.length; start = start || 0; var sliced = this.slice(start, start + limit); if (maxCount) { sliced.start = start; sliced.end = start + sliced.length - 1; sliced.totalCount = Math.min(totalCount, typeof maxCount === "number" ? maxCount : Infinity); } return sliced; }, distinct: function() { var primitives = {}; var needCleaning = []; var newResults = this.filter(function(value) { value = retrieve(value); if (value && typeof value == "object") { if (!value.__found__) { value.__found__ = function() {}; // get ignored by JSON serialization needCleaning.push(value); return true; } return false; } if (!primitives[value]) { primitives[value] = true; return true; } return false; }); needCleaning.forEach(function(object) { delete object.__found__; }); return newResults; }, recurse: function(property) { // TODO: this needs to use lazy-array var newResults = []; function recurse(value) { if (value.forEach) value.forEach(recurse); else { newResults.push(value); if (property) { value = value[property]; if (value && typeof value == "object") recurse(value); } else for (var i in value) if (value[i] && typeof value[i] == "object") recurse(value[i]); } } recurse(retrieve(this)); return newResults; }, aggregate: function() { var distinctives = []; var aggregates = []; for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; if (typeof arg === "function") aggregates.push(arg); else distinctives.push(arg); } var distinctObjects = {}; var dl = distinctives.length; this.forEach(function(object) { object = retrieve(object); var key = ""; for (var i = 0; i < dl; i++) key += '/' + object[distinctives[i]]; var arrayForKey = distinctObjects[key]; if (!arrayForKey) arrayForKey = distinctObjects[key] = []; arrayForKey.push(object); }); var al = aggregates.length; var newResults = []; for (var key in distinctObjects) { var arrayForKey = distinctObjects[key]; var newObject = {}; for (var i = 0; i < dl; i++) { var property = distinctives[i]; newObject[property] = arrayForKey[0][property]; } for (var i = 0; i < al; i++) { var aggregate = aggregates[i]; newObject[i] = aggregate.call(arrayForKey); } newResults.push(newObject); } return newResults; }, between: filter(function(value, range) { value = retrieve(value); return value >= range[0] && value < range[1]; }), sum: reducer(function(a, b) { a = retrieve(a); b = retrieve(b); return a + b; }), mean: function(property) { return rql.ops.sum.call(this, property) / this.length; }, max: reducer(function(a, b) { a = retrieve(a); b = retrieve(b); return Math.max(a, b); }), min: reducer(function(a, b) { a = retrieve(a); b = retrieve(b); return Math.min(a, b); }), count: function() { return this.length; }, first: function() { return this[0]; }, last: function() { return this[this.length - 1]; }, random: function() { return this[Math.round(Math.random() * (this.length - 1))]; }, one: function() { if (this.length > 1) throw new Error("RQLError : More than one object found"); return this[0]; } }; function rqlNodeToFunc(node) { if (typeof node === 'object') { var name = node.name; var args = node.args; if (node.forEach) return node.map(rqlNodeToFunc); else { var b = args[0], path = null; if (args.length == 2) { path = b; b = args[1]; } var func = null; var isFilter = false; switch (name) { case "eq": isFilter = true; func = function eq(a) { return (retrieve(a, path) || undefined) === b; }; break; case "ne": isFilter = true; func = function ne(a) { return (retrieve(a, path) || undefined) !== b; }; break; case "le": isFilter = true; func = function le(a) { return (retrieve(a, path) || undefined) <= b; }; break; case "ge": isFilter = true; func = function ge(a) { return (retrieve(a, path) || undefined) >= b; }; break; case "lt": isFilter = true; func = function lt(a) { return (retrieve(a, path) || undefined) < b; }; break; case "gt": isFilter = true; func = function gt(a) { return (retrieve(a, path) || undefined) > b; }; break; default: var ops = rql.ops[name]; if (!ops) throw new Error("RQLError : no operator found in rql with : " + name); if (args && args.length > 0) { args = args.map(rqlNodeToFunc); func = function() { return ops.apply(this, args); }; } else func = function() { return ops.call(this); }; } if (isFilter) return function() { var r = this.filter(func); return r; }; else return func; } } else return node; } function retrieve(obj, path) { if (!path) { if (obj._deep_query_node_) return obj.value; return obj; } var splitted = path.split("."); var tmp = obj; switch (splitted[0]) { case '_schema': if (obj._deep_query_node_) { splitted.shift(); tmp = obj.schema; } else return undefined; break; case '_depth': if (obj._deep_query_node_) return obj.depth; return undefined; case '_type': if (obj._deep_query_node_) return getJSPrimitiveType(obj.value); return getJSPrimitiveType(obj); default: if (obj._deep_query_node_) tmp = tmp.value; } if (!tmp) return tmp; var len = splitted.length, needtype = false; if (splitted[len - 1] == "_type") { len--; needtype = true; splitted.pop(); } if (len < 5) { var res; switch (len) { case 1: res = tmp[splitted[0]]; break; case 2: res = tmp[splitted[0]] && tmp[splitted[0]][splitted[1]]; break; case 3: res = tmp[splitted[0]] && tmp[splitted[0]][splitted[1]] && tmp[splitted[0]][splitted[1]][splitted[2]]; break; case 4: res = tmp[splitted[0]] && tmp[splitted[0]][splitted[1]] && tmp[splitted[0]][splitted[1]][splitted[2]] && tmp[splitted[0]][splitted[1]][splitted[2]][splitted[3]]; break; } if (typeof res !== 'undefined' && needtype) return getJSPrimitiveType(res); return res; } tmp = tmp[splitted[0]] && tmp[splitted[0]][splitted[1]] && tmp[splitted[0]][splitted[1]][splitted[2]] && tmp[splitted[0]][splitted[1]][splitted[2]][splitted[3]]; if (typeof tmp === 'undefined') return undefined; var count = 4, part = splitted[count]; while (part && tmp[part]) { tmp = tmp[part]; part = splitted[++count]; } if (count === len) { if (needtype) return getJSPrimitiveType(tmp); return tmp; } return undefined; } function filter(condition, not) { var filtr = function doFilter(property, second) { if (typeof second == "undefined") { second = property; property = undefined; } var args = arguments; var filtered = []; for (var i = 0, length = this.length; i < length; i++) { var item = this[i]; if (condition(evaluateProperty(item, property), second)) filtered.push(item); } return filtered; }; filtr.condition = condition; return filtr; } function reducer(func) { return function(property) { if (property) return this.map(function(object) { return retrieve(object, property); }).reduce(func); else return this.reduce(func); }; } function evaluateProperty(object, property) { if (property && property.forEach) return retrieve(object, decodeURIComponent(property)); if (typeof property === 'undefined') return retrieve(object); return retrieve(object, decodeURIComponent(property)); } return rql; });