UNPKG

deep-nodes

Version:

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

761 lines (728 loc) 20.2 kB
/** * A other proposal for (json/object)-query which : * - use simple slash delimitted syntax, * - could handle regular expression for step selection, * - could handle rql (for filtering) on each step selection, * - could be relative to where the query are placed in a object/json * - so could handle steps toward any ancestor * - could handle json-schema in rql filtering * - could handle ancestor in rql filtering * * @author Gilles Coomans <gilles.coomans@gmail.com> * * TODO : introduce OCM resolution and traversal while querying. */ if (typeof define !== 'function') { var define = require('amdefine')(module); } define(["require", "../index", "./rql", "deep-utils/lib/array", "deep-utils/lib/catch-parenthesis"], function(require, nodes, rqler, arrayUtils, parenthesis) { "use strict"; var queryCache = {}; /** * @class Query * @namespace deep * @constructor */ var DQ = function() {}; DQ.prototype.analyseEreg = function analyseEreg(path, parts) { // console.log("PARSE EREG : path : "+JSON.stringify(path)) var parsed = parenthesis(path); var reg = parsed.value; var regOptions = ""; var rest = parsed.rest; var self = this; if (rest[0] == "g" || rest[0] == "i") { regOptions = rest[0]; if (rest[1] == "g" || rest[1] == "i") regOptions += rest[1]; } rest = rest.substring(regOptions.length); parts.push({ type: "selector", value: reg, options: regOptions, handler: function(parent) { var res = []; for (var i in parent.value) { //if(!parent.value.hasOwnProperty(i)) // continue; if (new RegExp(this.value, this.options).test(i)) { var filtered = self.returnProperty(parent, i); if (typeof filtered !== 'undefined' && filtered !== null) res.push(filtered); } } return res; } }); return rest; }; /** * analyse path and return parsed paths objects * @method analyse * @param {String} path * @return {Array} parsed paths */ DQ.prototype.analyse = function dqAnalyse(path) { //console.log("analyse") var paths = []; var rest = path; this.asked = path; while (rest.length > 0) { rest = this.analyseMoves(rest, paths); if (paths.length === 0) throw new Error("deep-queries need at least one move path : " + path); if (rest.length === 0) break; rest = this.analyseSelector(rest, paths); //console.log("selector analysed : rest : ",rest) if (rest.length === 0) break; if (rest[0] == "?") rest = this.analyseRQL(rest, paths); } var self = this; if (paths.length > 0 && paths[paths.length - 1].slashes == "/") paths.push({ type: "selector", value: "*", handler: function(parent) { /*if(parent) return [parent]; return [];*/ return self.returnAllProps(parent); } }); //console.log("dq analayse : ", path); //console.log(" : gives : "+JSON.stringify(paths, null, ' ')); return paths; }; DQ.prototype.analyseRQL = function dqanalyseRQL(path, parts) { if (path[0] != "?") return path; path = path.substring(1); //console.log("will analyse rql : ", path) if (path[0] == '(') { var parsed = parenthesis(path); parts.push({ type: "rql", value: parsed.value, handler: function(items) { //console.log("will do rql : ", items, parsed.value); return rql(items, parsed.value); } }); return parsed.rest; } var count = 0; var rql = ""; while (path[count] != "/" && count < path.length) rql += path[count++]; parts.push({ type: "rql", value: rql, handler: function(items) { var res = null; try { //console.log("doRQLHANDLER : items : ", items) //console.log("will do rql 2 : ", items, rql); res = rqler(items, rql); //console.log("doRQLHANDLER : res : ", res) } catch (e) { console.log("deep-query : rql errors : ", e); return []; } return res; } }); //console.log("rql analyse gives : ", rql) return path.substring(count); }; DQ.prototype.analyseIndexAccess = function dqanalyseIndexAccess(path, parts) { var tmp = ""; var count = 0; while (path[count] != "/" && path[count] != "?" && count < path.length) tmp += path[count++]; var splitted = tmp.split(":"); var self = this; var range = { type: "selector", handler: function(parent) { var st = this.start(parent); if (st == -1) return []; var prop = null; if (this.end === null) { prop = self.returnProperty(parent, st); if (prop !== null && typeof prop !== 'undefined') return [prop]; return []; } var res = []; for (var i = st; i <= this.end(parent); i += this.step(parent)) { prop = self.returnProperty(parent, i); if (prop !== null && typeof prop !== 'undefined') res.push(prop); } return res; }, start: null, end: null, step: function(parent) { return 1; } }; var pos = 0, value = null; splitted.forEach(function(e) { if (e === "") { if (pos === 0) value = function(parent) { return 0; }; else if (pos == 1) value = function(parent) { if (parent.value.length !== undefined) return parent.value.length - 1; else return -1; }; else value = function(parent) { return 1; }; } else if (e.substring(0, 8) == '@.length') { var rest = e.substring(8); if (rest.length === 0 || rest[0] != "-") throw new Error("DeepQueryError : when you use @.length : you could only use minus '-' operator followed by an integer. " + path + " - " + parts.join(",")); var integ = parseInt(rest.substring(1)); if (isNaN(integ)) throw new Error("DeepQueryError : when you use @.length : you could only use minus '-' operator followed by an integer. " + path + " - " + parts.join(",")); value = function(parent) { if (parent.value.length !== undefined) return parent.value.length - integ; else return -1; }; } else { var integs = parseInt(e); if (isNaN(integs)) throw new Error("DeepQueryError : bad index : index unknown. " + path + " - " + parts.join(",")); value = function(parent) { return integs; }; } if (pos === 0) range.start = value; else if (pos == 1) range.end = value; else range.step = value; pos++; }); parts.push(range); return path.substring(count); }; DQ.prototype.analyseUnionAccess = function dqanalyseUnionAccess(path, parts) { if (path[0] != '[') throw new Error("DeepQueryError : union access need to start with '['. " + path + " - " + parts.join(",")); var inner = ""; var othis = this; var count = 1; while (path[count] != ']' && count < path.length) inner += path[count++]; var splitted = inner.split(","); var self = this; if (splitted.length === 0) { parts.push({ type: "selector", value: "*", handler: function(parent) { return self.returnAllProps(parent); } }); return path.substring(count + 1); } var union = { type: "selector", selectors: [], handler: function(parent) { var res = []; this.selectors.forEach(function(selector) { var locals = selector.handler(parent); if (locals) res = res.concat(locals); }); return res; // make unique on path } }; splitted.forEach(function(spl) { othis.analyseSelector(spl, union.selectors, true); }); parts.push(union); return path.substring(count + 1); }; DQ.prototype.returnProperty = function(entry, key) { if (typeof entry.value === 'string' && key !== 'length') return null; var obj = entry.value; if (obj && typeof obj[key] !== 'undefined') entry = nodes.create(key, entry); else entry = null; //console.log("returnProperty : ", key, " - on : ", obj, " - entry : ", entry) return entry; }; DQ.prototype.returnAllProps = function(entry) { //if(typeof entry.value === "string") // return [this.createEntry('length', entry)]; if (typeof entry.value === "string") return []; var obj = entry.value; var childs = []; for (var i in obj) { if (i == "_deep_shared_") continue; if (!obj.hasOwnProperty(i)) continue; var ent = nodes.create(i, entry); if (typeof ent !== 'undefined') childs.push(ent); } return childs; }; DQ.prototype.returnRecursiveProps = function(entry) { //console.log("recursive props : ", entry.path) if (typeof entry.value === "string") return []; var obj = entry.value; var childs = []; var self = this; for (var i in obj) { if (!obj.hasOwnProperty(i)) continue; var child = nodes.create(i, entry); if (typeof child !== "undefined") childs.push(child); if (typeof obj[i] === 'object') childs = childs.concat(self.returnRecursiveProps(child)); } return childs; }; DQ.prototype.analyseSelector = function(path, parts, fromUnion) { //console.log("analyseSelector : ", path); var count = 0; var self = this; if (path.length === 0) { if (parts.length > 1 && parts[parts.length - 1].slashes == "//") return path; if (fromUnion) parts.push({ type: "selector", value: "*", handler: function(parent) { return self.returnAllProps(parent); } }); else parts.push({ type: "selector", value: "*", handler: function(parent) { return self.returnAllProps(parent); if (parent) return [parent]; return []; //return self.returnAllProps(parent); } }); return path; } if (path[0] == "?") { //console.log("analyseSelector : find rql directly : preious is // : ",parts[parts.length-1].value == "//") if (fromUnion) throw new Error("you couldn't write '?' in union of selectors. " + path + " - " + parts.join(",")); if (parts[parts.length - 1].slashes == "//") return path; parts.push({ type: "selector", value: "*", handler: function(parent) { return self.returnAllProps(parent); if (parent) return [parent]; return []; //return self.returnAllProps(parent); } }); return path; } if (path[0] == "!") { parts.push({ type: "selector", value: "!", handler: function(parent) { //console.log("apply direct acces : ", path) if (parent) return [parent]; return []; } }); //console.log("git drect access : ", path, JSON.stringify(parts)); return path.substring(1); } if (path[0] == "(") return this.analyseEreg(path, parts); if (/^[0-9]/.test(path[0]) || path[0] == '@' || path[0] == ":") return this.analyseIndexAccess(path, parts); if (path[0] == '[') { if (fromUnion) throw new Error("DeepQueryError : you couldn't have union in union of selectors. " + this.currentQuery); return this.analyseUnionAccess(path, parts); } var string = ""; while (path[count] != '/' && path[count] != '?' && count < path.length) string += path[count++]; //console.log("analyseSelector : got string", string); if (string == "*") { if (parts.length > 0 && parts[parts.length - 1].slashes == "//") return path.substring(count); parts.push({ type: "selector", value: "*", handler: function(parent) { return self.returnAllProps(parent); } }); return path.substring(count); } parts.push({ type: "selector", value: string, handler: function(parent) { // console.log("analyseSelector string handler : ", string, parent); var res = self.returnProperty(parent, string); if (res !== null && typeof res !== 'undefined') return [res]; return []; } }); return path.substring(count); }; DQ.prototype.analyseMoves = function(path, paths) { var steps = []; var tmp = ""; var a = 0; //console.log("analyseMoves"); while (a < path.length && (path[a] == "." || path[a] == "/")) { //console.log("analyse move: ", path[a]) while (path[a] == '/') { tmp += '/'; a++; } if (tmp.length > 0) { if (tmp.length > 2) throw new Error("DeepQueryError : bad move : " + path); steps.push(tmp); tmp = ''; } while (path[a] == '.') { tmp += '.'; a++; } if (tmp.length > 0) { if (tmp.length > 3) throw new Error("DeepQueryError :bad move : " + path); steps.push(tmp); tmp = ''; } //console.log("a ?",a, path.length) //break; } //console.log("analyseMoves : steps : ", steps); //return ""; var last = steps[steps.length - 1]; if (!last) throw new Error("DeepQueryError : missformed query : " + this.asked); if (last[0] == ".") a -= last.length; while (steps.length > 0) { var res = { type: "move", points: null, slashes: null }; var step = steps.shift(); if (step[0] == ".") { res.points = step; step = steps.shift(); } if (step != '/' && step != '//') throw new Error("DeepQueryError : bad move : " + JSON.stringify(path) + " - " + step); res.slashes = step; paths.push(res); //console.log("analyse move give : ",res) } return path.substring(a); }; DQ.prototype.doMove = function(move, items) { var newItems = []; var toDo = move; var self = this; items.forEach(function(item) { if (toDo.points) switch (toDo.points) { case ".": newItems.push(item); break; case "..": if (item.ancestor) newItems.push(item.ancestor); break; case "...": var tmp = item; while (tmp.ancestor) { newItems.push(tmp.ancestor); tmp = tmp.ancestor; } break; default: throw new Error("DeepQueryError : bad move : " + toDo); } else newItems.push(item); if (toDo.slashes == "//") newItems = newItems.concat(self.returnRecursiveProps(item)); }); //console.log("DO MOVE gives : ", newItems); return newItems; }; var straightRegExp = /(\?)|(\/\/)|(\[)|(\()|(\*)/gi; /** * * perform the query on object * * @method query * @param {Object} obj any object to query on * @param {String} q the query * @param {Object} options (optional) : options : fullOutput:true when you want to get the array of nodes results, not only the values results. * @return {Array} an array of results (maybe empty) */ DQ.prototype.query = function(obj, q, options) { if (typeof obj !== 'object' || !obj) return []; this.currentQuery = q; //console.log("DQ.query : ", obj, q, options) options = options || {}; var items = null, currentRoot = null; if (q[0] === '#') q = q.substring(1); //console.log("DeepQuery : will do : ",q); if (obj._deep_query_node_) { // console.log("DQ : start with _deep_query_node_", obj, q) currentRoot = obj.root || obj; if (q[0] == '/') items = [currentRoot]; else items = [obj]; } else { // console.log("DQ : start with direct object", obj, q) currentRoot = nodes.root(obj, options.schema); //this.root.root = this.root; items = [currentRoot]; } //items[0].root = this.root; if (q == '/') if (options.allowStraightQueries !== false) { if (options.fullOutput) return items[0]; return items[0].value; } else { if (options.fullOutput) return items; return items.map(function(n) { //if(n.value && n.value._deep_ocm_) // return n.value(); return n.value; }); } this.straightQuery = false; if (options.allowStraightQueries !== false && !q.match(straightRegExp)) { //console.log("straight query") this.straightQuery = true; /*if(!q.match(/(\.\.)/g)) { if(q[0] == ".") q = q.substring(1); //console.log("STRAIGHT QUERY : ",q) var r = utils.fromPath(items[0].value, q, "/"); if(typeof r === 'undefined') return []; return r; }*/ } var parts; if (queryCache[q]) parts = queryCache[q]; else queryCache[q] = parts = this.analyse(q); //parts = parts.slice(); //console.log("Query Cache gives : ", parts); if (parts.length === 0 || parts[0].type != "move") throw new Error("DeepQueryError : query need to start with move : " + q); var self = this, start = true, count = 1, part = parts[0]; while (part) { switch (part.type) { case 'move': if (start && (part.slashes == "/" && !part.points)) { items = [currentRoot]; break; } items = self.doMove(part, items); //console.log("do move : ", items) break; case 'selector': var results = []; var len = items.length; for (var i = 0; i < len; ++i) { var item = items[i]; var r = part.handler(item); if (r && r.length > 0) results = results.concat(r); } items = results; break; case 'rql': items = part.handler(items); break; } //console.log("catch results : ", items) start = false; part = parts[count++]; } items = arrayUtils.arrayUnique(items, "path"); //console.log("DQ :"+q+" raw results : ", items) if (options.fullOutput) { if (this.straightQuery) if (items.length == 1) return items.shift(); else return undefined; items._deep_array_ = true; return items; } var finalRes = items.map(function(e) { return e.value; }); //console.log("QUERY "+q+" : straight ? ", this.straightQuery, finalRes); if (this.straightQuery) if (items.length == 1) return finalRes.shift(); else return undefined; return finalRes; }; var globalQuerier = null; /** * * perform the query (static access of query method) * * @static * @method query * @param {Object} root * @param {String} path * @param {Object} options * @return {Array} results */ DQ.query = function(root, path, options) { if (!globalQuerier) globalQuerier = new DQ(); return globalQuerier.query(root, path, options); }; DQ.remove = function(obj, what, schema) { var removed = []; function finalise(r) { if (!r.ancestor) return; removed.push(r); if (r.ancestor.value instanceof Array) r.ancestor.value.splice(r.key, 1); else { delete r.ancestor.value[r.key]; } } var r = DQ.query(obj, what, { fullOutput: true, schema: schema }); //console.log("deep.Querier.remove : ", obj, what, r); if (!r) return r; if (r._deep_query_node_) { finalise(r); return removed.shift(); } r.forEach(finalise); return removed; }; DQ.replace = function(target, what, by, schema) { var replaced = []; function finalise(r) { if (!r.ancestor) return; r.ancestor.value[r.key] = r.value = by; replaced.push(r); } var r = DQ.query(target, what, { fullOutput: true, schema: schema }); if (!r) return r; if (r._deep_query_node_) { finalise(r); return replaced.shift(); } r.forEach(finalise); return replaced; }; nodes.find = function(s, q, opt) { // console.log("deep chain query : start : ", s, e); if (q[0] === "?") q = "./*" + q; opt = opt || {}; opt.fullOutput = true; var r; if (s._deep_array_) { var root = null; if (q[0] == '/') { s.some(function(n) { // normally the first root should be the good one. root = n.root; return root; }); if (root) s = root; } if (!root) { r = []; s.forEach(function(n) { r = r.concat(DQ.query(n, q, opt)); }); if (r.length) r = arrayUtils.arrayUnique(r, "path"); r._deep_array_ = true; return r; } } r = DQ.query(s, q, opt); if (r._deep_query_node_) return r; r._deep_array_ = true; return r; }; return DQ; });