From aa993326df6e34b99fda679fa36b7048e49fceef Mon Sep 17 00:00:00 2001 From: michiel Date: Wed, 24 Oct 2018 08:39:57 +0200 Subject: [PATCH 1/4] Merge njust/rql master for "fixed webpack amd support" Merge burashka/rql master for "like support" --- .gitignore | 0 .travis.yml | 0 LICENSE | 12 + README.md | 51 +-- js-array.js | 678 ++++++++++++++++------------ parser.js | 490 ++++++++++---------- query.js | 468 +++++++++---------- specification/draft-zyp-rql-00.html | 0 specification/draft-zyp-rql-00.xml | 0 9 files changed, 904 insertions(+), 795 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .travis.yml create mode 100755 LICENSE mode change 100644 => 100755 parser.js mode change 100644 => 100755 specification/draft-zyp-rql-00.html mode change 100644 => 100755 specification/draft-zyp-rql-00.xml diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..8d9ebf4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (c) 2012-2015, Parallels Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index b8d30af..092009c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -[![Build Status](https://travis-ci.org/persvr/rql.svg?branch=master)](https://travis-ci.org/persvr/rql) - Resource Query Language (RQL) is a query language designed for use in URIs with object style data structures. This project includes the RQL specification and provides a JavaScript implementation of query @@ -27,8 +25,14 @@ Such that this can be used in URIs like: Using the JavaScript library we can construct queries using chained operator calls in JavaScript. We could execute the query above like this: - var Query = require("rql/query").Query; - var fooEq3Query = new Query().eq("foo",3); + var query = require("rql/js-array").query; + + var fooEq3Query = query("eq(foo,3)"); + var results = fooEq3Query(data); + + // or + var results = query("eq(foo,3)", undefined, data); + # RQL Rules @@ -118,8 +122,10 @@ for more less operators): * contains(<property>,<value | expression>) - Filters for objects where the specified property's value is an array and the array contains any value that equals the provided value or satisfies the provided expression. * excludes(<property>,<value | expression>) - Filters for objects where the specified property's value is an array and the array does not contain any of value that equals the provided value or satisfies the provided expression. * limit(count,start,maxCount) - Returns the given range of objects from the result set +* like(property, pattern) - Search for the specified pattern in the specified property. * and(<query>,<query>,...) - Applies all the given queries * or(<query>,<query>,...) - The union of the given queries +* not(query) - negation * eq(<property>,<value>) - Filters for objects where the specified property's value is equal to the provided value * lt(<property>,<value>) - Filters for objects where the specified property's value is less than the provided value * le(<property>,<value>) - Filters for objects where the specified property's value is less than or equal to the provided value @@ -138,6 +144,12 @@ for more less operators): # JavaScript Modules +## rql/js-array + +An implementation of RQL for JavaScript arrays. For example: + + require("./js-array").query("a=3", {}, [{a:1},{a:3}]) -> [{a:3}] + ## rql/query var newQuery = require("rql/query").Query(); @@ -181,42 +193,25 @@ For example: ] } -Installation -======== - -RQL can be installed using any standard package manager, for example with NPM: - - npm install rql - -or CPM: - - cpm install rql - -or RingoJS: - - ringo-admin install persvr/rql - Licensing -------- -The RQL implementation is part of the Persevere project, and therefore is licensed under the -AFL or BSD license. The Persevere project is administered under the Dojo foundation, -and all contributions require a Dojo CLA. +This RQL implementation is part of APS JavaScript SDK and licensed under the BSD license. Project Links ------------ -See the main Persevere project for more information: +See the main APS Standard for more information: ### Homepage: -* [http://persvr.org/](http://persvr.org/) +* [http://apsstandard.org/](http://apsstandard.org/) -### Mailing list: +### About RQL in APS: -* [http://groups.google.com/group/json-query](http://groups.google.com/group/json-query) +* [https://doc.apsstandard.org/2.1/spec/rql/](https://doc.apsstandard.org/2.1/spec/rql/) -### IRC: +### RQL Specification: -* [\#persevere on irc.freenode.net](http://webchat.freenode.net/?channels=persevere) +* [https://github.com/persvr/rql/tree/master/specification](https://github.com/persvr/rql/tree/master/specification) diff --git a/js-array.js b/js-array.js index b4b5367..d09f683 100644 --- a/js-array.js +++ b/js-array.js @@ -1,326 +1,436 @@ +// Copyright (c) 2012-2015, Parallels Inc. All rights reserved. +// Available via the modified BSD license. See LICENSE for details. /* * An implementation of RQL for JavaScript arrays. For example: * require("./js-array").query("a=3", {}, [{a:1},{a:3}]) -> [{a:3}] - * */ + function exportFactory(exports, parser, QUERY, each, contains) { -({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"), require("./query"), require("./util/each"), require("./util/contains"));}}). -define(["exports", "./parser", "./query", "./util/each", "./util/contains"], function(exports, parser, QUERY, each, contains){ -//({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"));}}). -//define(["exports", "./parser"], function(exports, parser){ + var parseQuery = parser.parse, + stringify = JSON.stringify; -var parseQuery = parser.parseQuery; -var stringify = typeof JSON !== "undefined" && JSON.stringify || function(str){ - return '"' + str.replace(/"/g, "\\\"") + '"'; -}; -var nextId = 1; -exports.jsOperatorMap = { - "eq" : "===", - "ne" : "!==", - "le" : "<=", - "ge" : ">=", - "lt" : "<", - "gt" : ">" -}; -exports.operators = { - 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); + function escape(s){ + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + } + + function likeToString(param){ + if(!param) return "false;"; + + var start = param[0] == "*", + end = param[param.length-1] == "*"; + + if(start) param = param.slice(1); + if(end) param = param.slice(0, param.length-1); + + var stars = param.split("*"); + if(stars.length > 1){ + for(var i=0; i b[term.attribute] ? 1 : -1; - } + + if(start||end||stars.length>1) + param = new RegExp((start ? "" : "^") + param + (end ? "" : "$"), "i"); + + if(param && param.test) + return param + ".test(value);"; + else + return "\"" + decodeURIComponent(param).toUpperCase() + "\"===value.toUpperCase();"; + } + + function filter(condition, not){ + var filt = function(property, second){ + if(typeof second == "undefined"){ + second = property; + property = undefined; } - return 0; - }); - return this; - }, - match: filter(function(value, regex){ - return new RegExp(regex).test(value); - }), - "in": filter(function(value, values){ - return contains(values, value); - }), - out: filter(function(value, values){ - return !contains(values, value); - }), - contains: filter(function(array, value){ - if(typeof value == "function"){ - return array instanceof Array && each(array, function(v){ - return value.call([v]).length; - }); - } - else{ - return array instanceof Array && contains(array, value); + 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; + }; + filt.condition = condition; + return filt; + } + + function contains(array, item){ + for(var i = 0, l = array.length; i < l; i++){ + if(array[i] === item) return true; } - }), - excludes: filter(function(array, value){ - if(typeof value == "function"){ - return !each(array, function(v){ - return value.call([v]).length; + } + + function evaluateProperty(object, property){ + if(property.indexOf(".") != -1){ + property = property.split("."); + property.forEach(function(part){ + object = object[decodeURIComponent(part)]; }); + return object; + }else if(typeof property == "undefined"){ + return object; + }else{ + return object[decodeURIComponent(property)]; } - else{ - return !contains(array, value); + } + + function query(quer, options, target){ + options = options || {}; + quer = parseQuery(quer, options.parameters); + function T(){} + T.prototype = exports.operators; + var operators = new T(); + // inherit from exports.operators + for(var i in options.operators) + operators[i] = options.operators[i]; + window.op = function(name){ + return operators[name]||exports.missingOperator(name); + }; + var parameters = options.parameters || []; + var js = ""; + function queryToJS(value){ + var item, + path, + escaped = []; + + if(value && typeof value === "object" && value.constructor !== Date){ + if(value instanceof Array){ + return "[" + value.map(queryToJS) + "]"; + }else{ + var jsOperator = exports.jsOperatorMap[value.name]; + if(jsOperator || /_*like/.test(value.name)){ + // item["foo.bar"] ==> (item && item.foo && item.foo.bar && ...) + path = value.args[0]; + var target = value.args[1]; + + if (typeof target == "undefined"){ + item = "item"; + target = path; + } else { + escaped = []; + item = "item"; + path = path.split("."); + for(var i = 0; i < path.length; i++){ + escaped.push(stringify(path[i])); + item +="&&item[" + escaped.join("][") + "]"; + } + } + var condition; + if(/_*like/.test(value.name)){ + var items = item.split("&&"); + item = items.slice(-1); + + condition = items.slice(0,-1); + condition.push("(function(value){ return " + + (value.name === "_like" ? "!" : "") + + likeToString(value.args[1]) + "})(" + item + ")" + ); + condition = condition.join("&&"); + } else { + if(target.constructor === Date) + condition = "(function(){ if(!(" + item + ")) var val = new Date(0); else { val = item[" + escaped.join("][") + + "]; if(val.length !== undefined) val = new Date(val); } return val.valueOf()" + + jsOperator + queryToJS(target) + "})()"; + else + condition = item + jsOperator + queryToJS(target); + } + + return "(function(){return this.filter(function(item){return " + condition + "})})"; + } else { + switch(value.name){ + case "true": return true; + case "false": return false; + case "null": return null; + case "empty": return stringify(""); + case "not": return value && value.args && value.args.length > 0 ? queryToJS(value.args[0]) : ""; + default: + return "(function(){return window.op('" + value.name + "').call(this" + + (value && value.args && value.args.length > 0 ? (", " + value.args.map(queryToJS).join(",")) : "") + + ")})"; + } + } + } + }else{ + if(typeof value === "string") + return stringify(value); + if(value.constructor === Date) + return value.valueOf(); + return value; + } } - }), - or: function(){ - var items = []; - var idProperty = "__rqlId" + nextId++; - try{ + var _evaluator = new Function("target", "return " + queryToJS(quer) + ".call(target);"), + evaluator = function(target){ + return _evaluator(target.slice(0)); + }; + + return target ? evaluator(target) : evaluator; + } + + exports.jsOperatorMap = { + "eq" : "===", + "ne" : "!==", + "le" : "<=", + "ge" : ">=", + "lt" : "<", + "gt" : ">" + }; + exports.operators = { + sort: function(){ + var terms = [], + reg = /\./gi; for(var i = 0; i < arguments.length; i++){ - var group = arguments[i].call(this); - for(var j = 0, l = group.length;j < l;j++){ - var item = group[j]; - // use marker to do a union in linear time. - if(!item[idProperty]){ - item[idProperty] = true; - items.push(item); + var sortAttribute = arguments[i], + firstChar = sortAttribute.charAt(0), + term = {attribute: sortAttribute, ascending: true}; + if (firstChar == "-" || firstChar == "+") { + if(firstChar == "-"){ + term.ascending = false; } + term.attribute = term.attribute.substring(1); } + term.getItem = new Function("item", "return item[\"" + term.attribute.replace(reg, "\"][\"") + "\"];"); + terms.push(term); } - }finally{ - // cleanup markers - for(var i = 0, l = items.length; i < l; i++){ - delete items[idProperty]; + + this.sort(function(a, b){ + for (var term, i = 0; term = terms[i]; i++) + if(term.getItem(a) != term.getItem(b)) + return term.ascending == term.getItem(a) > term.getItem(b) ? 1 : -1; + return 0; + }); + return this; + }, + "in": filter(function(value, values){ + return values.indexOf(value) > -1; + }), + out: filter(function(value, values){ + return values.indexOf(value) == -1; + }), + contains: filter(function(array, value){ + if(typeof value == "function"){ + return array instanceof Array && each(array, function(v){ + return value.call([v]).length; + }); } - } - return items; - }, - and: function(){ - var items = this; - // TODO: use condition property - for(var i = 0; i < arguments.length; i++){ - items = arguments[i].call(items); - } - return items; - }, - select: function(){ - var args = arguments; - var argc = arguments.length; - return each(this, function(object, emit){ - 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; - } + else{ + return array instanceof Array && contains(array, value); } - emit(selected); - }); - }, - unselect: function(){ - var args = arguments; - var argc = arguments.length; - return each(this, function(object, emit){ - var selected = {}; - for (var i in object) if (object.hasOwnProperty(i)) { - selected[i] = object[i]; + }), + excludes: filter(function(array, value){ + if(typeof value == "function"){ + return !each(array, function(v){ + return value.call([v]).length; + }); } - for(var i = 0; i < argc; i++) { - delete selected[args[i]]; + else{ + return !contains(array, value); } - emit(selected); - }); - }, - values: function(first){ - if(arguments.length == 1){ + }), + or: function(){ + var items = []; + for(var i = 0; i < arguments.length; i++) + items = items.concat(arguments[i].call(this)); + return items; + }, + and: function(){ + var items = this; + for(var i = 0; i < arguments.length; i++) + items = arguments[i].call(items); + return items; + }, + select: function(){ + var args = arguments; + var argc = arguments.length; return each(this, function(object, emit){ - emit(object[first]); - }); - } - var args = arguments; - var argc = arguments.length; - return each(this, function(object, emit){ - var selected = []; - if (argc === 0) { - for(var i in object) if (object.hasOwnProperty(i)) { - selected.push(object[i]); - } - } else { + var selected = {}; for(var i = 0; i < argc; i++){ var propertyName = args[i]; - selected.push(object[propertyName]); + var value = evaluateProperty(object, propertyName); + if(typeof value != "undefined"){ + selected[propertyName] = value; + } } + emit(selected); + }); + }, + unselect: function(){ + var args = arguments; + var argc = arguments.length; + return each(this, function(object, emit){ + var selected = {}, i; + for (i in object) if (object.hasOwnProperty(i)) selected[i] = object[i]; + for(i = 0; i < argc; i++) delete selected[args[i]]; + emit(selected); + }); + }, + values: function(first){ + if(arguments.length == 1){ + return each(this, function(object, emit){ + emit(object[first]); + }); } - emit(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){ - if(value && typeof value == "object"){ - if(!value.__found__){ - value.__found__ = function(){};// get ignored by JSON serialization - needCleaning.push(value); - return true; - } - }else{ - if(!primitives[value]){ - primitives[value] = true; - return true; + var args = arguments; + var argc = arguments.length; + return each(this, function(object, emit){ + var selected = []; + if (argc === 0) { + for(var i in object) if (object.hasOwnProperty(i)) { + selected.push(object[i]); + } + } else { + for(var i = 0; i < argc; i++){ + var propertyName = args[i]; + selected.push(object[propertyName]); + } } + emit(selected); + }); + }, + limit: function(start, limit, 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); } - }); - each(needCleaning, function(object){ - delete object.__found__; - }); - return newResults; - }, - recurse: function(property){ - // TODO: this needs to use lazy-array - var newResults = []; - function recurse(value){ - if(value instanceof Array){ - each(value, recurse); - }else{ - newResults.push(value); - if(property){ - value = value[property]; - if(value && typeof value == "object"){ - recurse(value); + return sliced; + }, + distinct: function(){ + var primitives = {}; + var needCleaning = []; + var newResults = this.filter(function(value){ + if(value && typeof value == "object"){ + if(!value.__found__){ + value.__found__ = function(){};// get ignored by JSON serialization + needCleaning.push(value); + return true; } }else{ - for(var i in value){ - if(value[i] && typeof value[i] == "object"){ - recurse(value[i]); + if(!primitives[value]){ + primitives[value] = true; + return true; + } + } + }); + each(needCleaning, function(object){ + delete object.__found__; + }); + return newResults; + }, + recurse: function(property){ + // TODO: this needs to use lazy-array + var newResults = []; + function recurse(value){ + if(value instanceof Array){ + each(value, 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(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; - each(this, function(object){ - var key = ""; - for(var i = 0; i < dl;i++){ - key += '/' + object[distinctives[i]]; - } - var arrayForKey = distinctObjects[key]; - if(!arrayForKey){ - arrayForKey = distinctObjects[key] = []; + recurse(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); + } } - 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]; + var distinctObjects = {}; + var dl = distinctives.length; + each(this, function(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], + newObject = {}, + i; + for(i = 0; i < dl;i++){ + var property = distinctives[i]; + newObject[property] = arrayForKey[0][property]; + } + for(i = 0; i < al;i++){ + newObject[i] = aggregates[i].call(arrayForKey); + } + newResults.push(newObject); } - for(var i = 0; i < al;i++){ - var aggregate = aggregates[i]; - newObject[i] = aggregate.call(arrayForKey); + return newResults; + }, + between: filter(function(value, range){ + return value >= range[0] && value < range[1]; + }), + sum: reducer(function(a, b){ + return a + b; + }), + mean: function(property){ + return exports.operators.sum.call(this, property)/this.length; + }, + max: reducer(function(a, b){ + return Math.max(a, b); + }), + min: reducer(function(a, b){ + return Math.min(a, b); + }), + count: function(){ + return this.length; + }, + first: function(){ + return this[0]; + }, + one: function(){ + if(this.length > 1){ + throw new TypeError("More than one object found"); } - newResults.push(newObject); - } - return newResults; - }, - between: filter(function(value, range){ - return value >= range[0] && value < range[1]; - }), - sum: reducer(function(a, b){ - return a + b; - }), - mean: function(property){ - return exports.operators.sum.call(this, property)/this.length; - }, - max: reducer(function(a, b){ - return Math.max(a, b); - }), - min: reducer(function(a, b){ - return Math.min(a, b); - }), - count: function(){ - return this.length; - }, - first: function(){ - return this[0]; - }, - one: function(){ - if(this.length > 1){ - throw new TypeError("More than one object found"); + return this[0]; } - return this[0]; - } -}; -exports.filter = filter; -function filter(condition, not){ - // convert to boolean right now - var filter = function(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; }; - filter.condition = condition; - return filter; -}; -function reducer(func){ - return function(property){ - var result = this[0]; - if(property){ - result = result && result[property]; - for(var i = 1, l = this.length; i < l; i++) { - result = func(result, this[i][property]); - } - }else{ - for(var i = 1, l = this.length; i < l; i++) { - result = func(result, this[i]); + function reducer(func){ + return function(property){ + var result = this[0], i, l; + if(property){ + result = result && result[property]; + for(i = 1, l = this.length; i < l; i++) { + result = func(result, this[i][property]); + } + }else{ + for(i = 1, l = this.length; i < l; i++) { + result = func(result, this[i]); + } } - } - return result; + return result; + }; } } exports.evaluateProperty = evaluateProperty; diff --git a/parser.js b/parser.js old mode 100644 new mode 100755 index 4300842..f913e49 --- a/parser.js +++ b/parser.js @@ -1,294 +1,272 @@ +// Copyright (c) 2012-2015, Parallels Inc. All rights reserved. +// Available via the modified BSD license. See LICENSE for details. /** * This module provides RQL parsing. For example: * var parsed = require("./parser").parse("b=3&le(c,5)"); */ -({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./util/contains"));}}). -define(["exports", "./util/contains"], function(exports, contains){ +function exportFactory(exports, contains) { -var operatorMap = { - "=": "eq", - "==": "eq", - ">": "gt", - ">=": "ge", - "<": "lt", - "<=": "le", - "!=": "ne" -}; + var operatorMap = { + "=": "eq", + "==": "eq", + ">": "gt", + ">=": "ge", + "<": "lt", + "<=": "le", + "!=": "ne" + }; + var denialMap = { + "eq": "ne", + "ne": "eq", + "gt": "le", + "ge": "lt", + "lt": "ge", + "le": "gt", + "and": "or", + "or": "and", + "in": "out", + "out": "in", + "not": "not", + "like": "_like" + }; -exports.primaryKeyName = 'id'; -exports.lastSeen = ['sort', 'select', 'values', 'limit']; -exports.jsonQueryCompatible = true; + exports.primaryKeyName = "id"; + exports.lastSeen = ["sort", "select", "values", "limit"]; + exports.jsonQueryCompatible = true; -function parse(/*String|Object*/query, parameters){ - if (typeof query === "undefined" || query === null) - query = ''; - var term = new exports.Query(); - var topTerm = term; - topTerm.cache = {}; // room for lastSeen params - var topTermName = topTerm.name; - topTerm.name = ''; - if(typeof query === "object"){ - if(query instanceof exports.Query){ - return query; + function parse(/*String|Object*/query, parameters){ + if (typeof query === "undefined" || query === null) + query = ""; + var term = new exports.Query(); + var topTerm = term; + var not = false; + topTerm.cache = {}; // room for lastSeen params + if(typeof query === "object"){ + if(query instanceof exports.Query){ + return query; + } + for(var i in query){ + term = new exports.Query(); + topTerm.args.push(term); + term.name = "eq"; + term.args = [i, query[i]]; + } + return topTerm; } - for(var i in query){ - var term = new exports.Query(); - topTerm.args.push(term); - term.name = "eq"; - term.args = [i, query[i]]; + if(query.charAt(0) == "?"){ + throw new URIError("Query must not start with ?"); } - return topTerm; - } - if(query.charAt(0) == "?"){ - throw new URIError("Query must not start with ?"); - } - if(exports.jsonQueryCompatible){ - query = query.replace(/%3C=/g,"=le=").replace(/%3E=/g,"=ge=").replace(/%3C/g,"=lt=").replace(/%3E/g,"=gt="); - } - if(query.indexOf("/") > -1){ // performance guard - // convert slash delimited text to arrays - query = query.replace(/[\+\*\$\-:\w%\._]*\/[\+\*\$\-:\w%\._\/]*/g, function(slashed){ - return "(" + slashed.replace(/\//g, ",") + ")"; - }); - } - // convert FIQL to normalized call syntax form - query = query.replace(/(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)([<>!]?=(?:[\w]*=)?|>|<)(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)/g, - // <--------- property -----------><------ operator -----><---------------- value ------------------> - function(t, property, operator, value){ - if(operator.length < 3){ - if(!operatorMap[operator]){ - throw new URIError("Illegal operator " + operator); - } - operator = operatorMap[operator]; + if(exports.jsonQueryCompatible){ + query = query.replace(/%3C=/g,"=le=").replace(/%3E=/g,"=ge=").replace(/%3C/g,"=lt=").replace(/%3E/g,"=gt="); } - else{ - operator = operator.substring(1, operator.length - 1); + if(query.indexOf("/") > -1){ // performance guard + // convert slash delimited text to arrays + query = query.replace(/[\+\*\$\-:\w%\._]*\/[\+\*\$\-:\w%\._\/]*/g, function(slashed){ + return "(" + slashed.replace(/\//g, ",") + ")"; + }); } - return operator + '(' + property + "," + value + ")"; - }); - if(query.charAt(0)=="?"){ - query = query.substring(1); - } - var leftoverCharacters = query.replace(/(\))|([&\|,])?([\+\*\$\-:\w%\._]*)(\(?)/g, - // <-closedParan->|<-delim-- propertyOrValue -----(> | - function(t, closedParan, delim, propertyOrValue, openParan){ - if(delim){ - if(delim === "&"){ - setConjunction("and"); - } - if(delim === "|"){ - setConjunction("or"); + // convert FIQL to normalized call syntax form + query = query.replace(/(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)([<>!]?=(?:[\w]*=)?|>|<)(\([\+\*\$\-:\w%\._,]+(\(\))*\)|[\+\*\$\-:\w%\._]*(\(\))*|)/g, + //<--------- property -----------><------ operator -----><---------------- value ------------------> + function(t, property, operator, value){ + if(operator.length < 3){ + if(!operatorMap[operator]){ + throw new URIError("Illegal operator " + operator); } + operator = operatorMap[operator]; } - if(openParan){ - var newTerm = new exports.Query(); - newTerm.name = propertyOrValue; - newTerm.parent = term; - call(newTerm); + else{ + operator = operator.substring(1, operator.length - 1); } - else if(closedParan){ - var isArray = !term.name; - term = term.parent; - if(!term){ - throw new URIError("Closing paranthesis without an opening paranthesis"); - } - if(isArray){ - term.args.push(term.args.pop().args); + return operator + "(" + property + "," + value + ")"; + }); + if(query.charAt(0)=="?"){ + query = query.substring(1); + } + var leftoverCharacters = query.replace(/(\))|([&\|,;])?([\+\*\$\-:\w%\._]*)(\(?)/g, + // <-closedParan->|<-delim-- propertyOrValue -----(> | + function(t, closedParan, delim, propertyOrValue, openParan){ + if(delim){ + if(delim === "&"){ + setConjunction("and"); + } + if(delim === "|" || delim === ";"){ + setConjunction("or"); + } } - } - else if(propertyOrValue || delim === ','){ - term.args.push(stringToValue(propertyOrValue, parameters)); + if(openParan){ + var newTerm = new exports.Query(); + newTerm.name = not ? denialMap[propertyOrValue] : propertyOrValue; + newTerm.parent = term; + if(propertyOrValue === "not") + not = !not; - // cache the last seen sort(), select(), values() and limit() - if (contains(exports.lastSeen, term.name)) { - topTerm.cache[term.name] = term.args; + call(newTerm); } - // cache the last seen id equality - if (term.name === 'eq' && term.args[0] === exports.primaryKeyName) { - var id = term.args[1]; - if (id && !(id instanceof RegExp)) id = id.toString(); - topTerm.cache[exports.primaryKeyName] = id; + else if(closedParan){ + var isArray = !term.name; + if(term.name === "not") not = !not; + term = term.parent; + if(!term){ + throw new URIError("Closing paranthesis without an opening paranthesis"); + } + if(isArray){ + term.args.push(term.args.pop().args); + } } - } - return ""; - }); - if(term.parent){ - throw new URIError("Opening paranthesis without a closing paranthesis"); - } - if(leftoverCharacters){ - // any extra characters left over from the replace indicates invalid syntax - throw new URIError("Illegal character in query string encountered " + leftoverCharacters); - } + else if(propertyOrValue || delim === ","){ + term.args.push(stringToValue(propertyOrValue, parameters)); - function call(newTerm){ - term.args.push(newTerm); - term = newTerm; - // cache the last seen sort(), select(), values() and limit() - if (contains(exports.lastSeen, term.name)) { - topTerm.cache[term.name] = term.args; + // cache the last seen sort(), select(), values() and limit() + if (exports.lastSeen.indexOf(term.name) >= 0) { + topTerm.cache[term.name] = term.args; + } + // cache the last seen id equality + if (term.name === "eq" && term.args[0] === exports.primaryKeyName) { + var id = term.args[1]; + if (id && !(id instanceof RegExp)) id = id.toString(); + topTerm.cache[exports.primaryKeyName] = id; + } + } + return ""; + }); + if(term.parent){ + throw new URIError("Opening paranthesis without a closing paranthesis"); } - } - function setConjunction(operator){ - if(!term.name){ - term.name = operator; + if(leftoverCharacters){ + // any extra characters left over from the replace indicates invalid syntax + throw new URIError("Illegal character in query string encountered " + leftoverCharacters); } - else if(term.name !== operator){ - throw new Error("Can not mix conjunctions within a group, use paranthesis around each set of same conjuctions (& and |)"); + + function call(newTerm){ + term.args.push(newTerm); + term = newTerm; + // cache the last seen sort(), select(), values() and limit() + if (exports.lastSeen.indexOf(term.name) >= 0) { + topTerm.cache[term.name] = term.args; + } } - } - function removeParentProperty(obj) { - if(obj && obj.args){ - delete obj.parent; - var args = obj.args; - for(var i = 0, l = args.length; i < l; i++){ - removeParentProperty(args[i]); + function setConjunction(operator){ + if(!term.name){ + term.name = not ? denialMap[operator] : operator; + } + else if(term.name !== operator){ + throw new Error("Can not mix conjunctions within a group, use paranthesis around each set of same conjuctions (& and |)"); } } - return obj; - }; - removeParentProperty(topTerm); - if (!topTerm.name) { - topTerm.name = topTermName; + function removeParentProperty(obj) { + if(obj && obj.args){ + delete obj.parent; + obj.args.forEach(removeParentProperty); + } + return obj; + } + removeParentProperty(topTerm); + return topTerm; } - return topTerm; -}; -exports.parse = exports.parseQuery = parse; + exports.parse = exports.parseQuery = parse; -/* dumps undesirable exceptions to Query().error */ -exports.parseGently = function(){ - var terms; - try { - terms = parse.apply(this, arguments); - } catch(err) { - terms = new exports.Query(); - terms.error = err.message; - } - return terms; -} + /* dumps undesirable exceptions to Query().error */ + exports.parseGently = function(){ + var terms; + try { + terms = parse.apply(this, arguments); + } catch(err) { + terms = new exports.Query(); + terms.error = err.message; + } + return terms; + }; -exports.commonOperatorMap = { - "and" : "&", - "or" : "|", - "eq" : "=", - "ne" : "!=", - "le" : "<=", - "ge" : ">=", - "lt" : "<", - "gt" : ">" -} -function stringToValue(string, parameters){ - var converter = exports.converters['default']; - if(string.charAt(0) === "$"){ - var param_index = parseInt(string.substring(1)) - 1; - return param_index >= 0 && parameters ? parameters[param_index] : undefined; - } - if(string.indexOf(":") > -1){ - var parts = string.split(":"); - converter = exports.converters[parts[0]]; - if(!converter){ - throw new URIError("Unknown converter " + parts[0]); + exports.commonOperatorMap = { + "and" : "&", + "or" : "|", + "eq" : "=", + "ne" : "!=", + "le" : "<=", + "ge" : ">=", + "lt" : "<", + "gt" : ">" + }; + function stringToValue(string, parameters){ + var converter = exports.converters["default"]; + if(string.charAt(0) === "$"){ + var param_index = parseInt(string.substring(1), 10) - 1; + return param_index >= 0 && parameters ? parameters[param_index] : undefined; + } + if(string.indexOf(":") > -1){ + var parts = string.split(":"); + if(parts.length == 2) { + converter = exports.converters[parts[0]]; + string = parts[1]; + } + if(!converter){ + throw new URIError("Unknown converter " + parts[0]); + } } - string = parts.slice(1).join(':'); + if(/^\d{4}-[0-1]\d-[0-3][0-9](T([0-2]\d:){2}[0-2]\d([-+][0-2]\d:[0-2]\d)*)*$/.test(string)) + converter = exports.converters["date"]; + + return converter(string); } - return converter(string); -}; -var autoConverted = exports.autoConverted = { - "true": true, - "false": false, - "null": null, - "undefined": undefined, - "Infinity": Infinity, - "-Infinity": -Infinity -}; + var autoConverted = exports.autoConverted = { + "undefined": undefined, + "Infinity": Infinity, + "-Infinity": -Infinity + }; -exports.converters = { - auto: function(string){ - if(autoConverted.hasOwnProperty(string)){ - return autoConverted[string]; - } - var number = +string; - if(isNaN(number) || number.toString() !== string){ - /*var isoDate = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec(x); - if (isoDate) { - date = new Date(Date.UTC(+isoDate[1], +isoDate[2] - 1, +isoDate[3], +isoDate[4], +isoDate[5], +isoDate[6], +isoDate[7] || 0)); - }*/ - string = decodeURIComponent(string); - if(exports.jsonQueryCompatible){ - if(string.charAt(0) == "'" && string.charAt(string.length-1) == "'"){ - return JSON.parse('"' + string.substring(1,string.length-1) + '"'); + exports.converters = { + auto: function(string){ + if(autoConverted.hasOwnProperty(string)){ + return autoConverted[string]; + } + var number = +string; + if(isNaN(number) || number.toString() !== string){ + string = decodeURIComponent(string); + if(exports.jsonQueryCompatible){ + if(string.charAt(0) == "'" && string.charAt(string.length-1) == "'"){ + return JSON.parse('"' + string.substring(1,string.length-1) + '"'); + } } + return string; } - return string; - } - return number; - }, - number: function(x){ - var number = +x; - if(isNaN(number)){ - throw new URIError("Invalid number " + number); - } - return number; - }, - epoch: function(x){ - var date = new Date(+x); - if (isNaN(date.getTime())) { - throw new URIError("Invalid date " + x); - } - return date; - }, - isodate: function(x){ - // four-digit year - var date = '0000'.substr(0,4-x.length)+x; - // pattern for partial dates - date += '0000-01-01T00:00:00Z'.substring(date.length); - return exports.converters.date(date); - }, - date: function(x){ - var isoDate = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec(x); - var date; - if (isoDate) { - date = new Date(Date.UTC(+isoDate[1], +isoDate[2] - 1, +isoDate[3], +isoDate[4], +isoDate[5], +isoDate[6], +isoDate[7] || 0)); - }else{ - date = new Date(x); - } - if (isNaN(date.getTime())){ - throw new URIError("Invalid date " + x); + return number; + }, + date: function(x){ + var isoDate = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec(x); + var date; + if (isoDate) { + date = new Date(Date.UTC(+isoDate[1], +isoDate[2] - 1, +isoDate[3], +isoDate[4], +isoDate[5], +isoDate[6], +isoDate[7] || 0)); + }else{ + date = new Date(x); + } + if (isNaN(date.getTime())){ + throw new URIError("Invalid date " + x); + } + return date; } - return date; - }, - "boolean": function(x){ - return x === "true"; - }, - string: function(string){ - return decodeURIComponent(string); - }, - re: function(x){ - return new RegExp(decodeURIComponent(x), 'i'); - }, - RE: function(x){ - return new RegExp(decodeURIComponent(x)); - }, - glob: function(x){ - var s = decodeURIComponent(x).replace(/([\\|\||\(|\)|\[|\{|\^|\$|\*|\+|\?|\.|\<|\>])/g, function(x){return '\\'+x;}).replace(/\\\*/g,'.*').replace(/\\\?/g,'.?'); - if (s.substring(0,2) !== '.*') s = '^'+s; else s = s.substring(2); - if (s.substring(s.length-2) !== '.*') s = s+'$'; else s = s.substring(0, s.length-2); - return new RegExp(s, 'i'); - } -}; + }; -// exports.converters["default"] can be changed to a different converter if you want -// a different default converter, for example: -// RP = require("rql/parser"); -// RP.converters["default"] = RQ.converter.string; -exports.converters["default"] = exports.converters.auto; + exports.converters["default"] = exports.converters.auto; + + // this can get replaced by the chainable query if query.js is loaded + exports.Query = function(){ + this.name = "and"; + this.args = []; + }; + return exports; +} + +if(typeof define!="undefined") { + define(['exports', './util/contains'], function(exports, contains) { + return exportFactory(exports, contains); + }); +}else { + exportFactory( + module.exports, + require("./util/contains")); +} -// this can get replaced by the chainable query if query.js is loaded -exports.Query = function(){ - this.name = "and"; - this.args = []; -}; -return exports; -}); diff --git a/query.js b/query.js index cea7c5a..33ca30d 100644 --- a/query.js +++ b/query.js @@ -1,3 +1,5 @@ +// Copyright (c) 2012-2015, Parallels Inc. All rights reserved. +// Available via the modified BSD license. See LICENSE for details. /** * Provides a Query constructor with chainable capability. For example: * var Query = require("./query").Query; @@ -21,251 +23,263 @@ try{ when = function(value, callback){callback(value)}; } -parser.Query = function(seed, params){ - if (typeof seed === 'string') - return parseQuery(seed, params); - var q = new Query(); - if (seed && seed.name && seed.args) - q.name = seed.name, q.args = seed.args; - return q; -}; -exports.Query = parser.Query; -//TODO:THE RIGHT WAY IS:exports.knownOperators = Object.keys(jsarray.operators || {}).concat(Object.keys(jsarray.jsOperatorMap || {})); -exports.knownOperators = ["sort", "match", "in", "out", "or", "and", "select", "contains", "excludes", "values", "limit", "distinct", "recurse", "aggregate", "between", "sum", "mean", "max", "min", "count", "first", "one", "eq", "ne", "le", "ge", "lt", "gt"]; -exports.knownScalarOperators = ["mean", "sum", "min", "max", "count", "first", "one"]; -exports.arrayMethods = ["forEach", "reduce", "map", "filter", "indexOf", "some", "every"]; + parser.Query = function(seed, params){ + if (typeof seed === 'string') + return parseQuery(seed, params); + var q = new Query(); + if (seed && seed.name && seed.args) + q.name = seed.name, q.args = seed.args; + return q; + }; + exports.Query = parser.Query; + //TODO:THE RIGHT WAY IS:exports.knownOperators = Object.keys(jsarray.operators || {}).concat(Object.keys(jsarray.jsOperatorMap || {})); + exports.knownOperators = ["like", "sort", "match", "in", "out", "or", "and", "select", "contains", "excludes", "values", "limit", "distinct", "recurse", "aggregate", "between", "sum", "mean", "max", "min", "count", "first", "one", "eq", "ne", "le", "ge", "lt", "gt"]; + exports.knownScalarOperators = ["mean", "sum", "min", "max", "count", "first", "one"]; + exports.arrayMethods = ["forEach", "reduce", "map", "filter", "indexOf", "some", "every"]; -function Query(name){ - this.name = name || "and"; - this.args = []; -} -function serializeArgs(array, delimiter){ - var results = []; - for(var i = 0, l = array.length; i < l; i++){ - results.push(queryToString(array[i])); + function Query(name){ + this.name = name || "and"; + this.args = []; } - return results.join(delimiter); -} -exports.Query.prototype = Query.prototype; -Query.prototype.toString = function(){ - return this.name === "and" ? - serializeArgs(this.args, "&") : - queryToString(this); -}; - -function queryToString(part) { - if (part instanceof Array) { - return '(' + serializeArgs(part, ",")+')'; - } - if (part && part.name && part.args) { - return [ - part.name, - "(", - serializeArgs(part.args, ","), - ")" - ].join(""); + function serializeArgs(array, delimiter){ + var results = []; + for(var i = 0, l = array.length; i < l; i++){ + results.push(queryToString(array[i])); } - return exports.encodeValue(part); -}; + return results.join(delimiter); + } + exports.Query.prototype = Query.prototype; + Query.prototype.toString = function(){ + return this.name === "and" ? + serializeArgs(this.args, "&") : + queryToString(this); + }; -function encodeString(s) { - if (typeof s === "string") { - s = encodeURIComponent(s); - if (s.match(/[\(\)]/)) { - s = s.replace("(","%28").replace(")","%29"); - }; - } - return s; -} + function queryToString(part) { + if (part instanceof Array) { + return '(' + serializeArgs(part, ",")+')'; + } + if (part && part.name && part.args) { + return [ + part.name, + "(", + serializeArgs(part.args, ","), + ")" + ].join(""); + } + return exports.encodeValue(part); + } -exports.encodeValue = function(val) { - var encoded; - if (val === null) val = 'null'; - if (val !== parser.converters["default"]('' + ( - val.toISOString && val.toISOString() || val.toString() - ))) { - var type = typeof val; - if(val instanceof RegExp){ - // TODO: control whether to we want simpler glob() style - val = val.toString(); - var i = val.lastIndexOf('/'); - type = val.substring(i).indexOf('i') >= 0 ? "re" : "RE"; - val = encodeString(val.substring(1, i)); - encoded = true; - } - if(type === "object"){ - type = "epoch"; - val = val.getTime(); - encoded = true; - } - if(type === "string") { - val = encodeString(val); + function encodeString(s) { + if (typeof s === "string") { + s = encodeURIComponent(s); + if (s.match(/[\(\)]/)) { + s = s.replace("(","%28").replace(")","%29"); + }; + } + return s; + } + + exports.encodeValue = function(val) { + var encoded; + if (val === null) val = 'null'; + if (val !== parser.converters["default"]('' + ( + val.toISOString && val.toISOString() || val.toString() + ))) { + var type = typeof val; + if(val instanceof RegExp){ + // TODO: control whether to we want simpler glob() style + val = val.toString(); + var i = val.lastIndexOf('/'); + type = val.substring(i).indexOf('i') >= 0 ? "re" : "RE"; + val = encodeString(val.substring(1, i)); encoded = true; - } - val = [type, val].join(":"); - } - if (!encoded && typeof val === "string") val = encodeString(val); - return val; -}; + } + if(type === "object"){ + type = "epoch"; + val = val.getTime(); + encoded = true; + } + if(type === "string") { + val = encodeString(val); + encoded = true; + } + val = [type, val].join(":"); + } + if (!encoded && typeof val === "string") val = encodeString(val); + return val; + }; -exports.updateQueryMethods = function(){ - each(exports.knownOperators, function(name){ - Query.prototype[name] = function(){ - var newQuery = new Query(); - newQuery.executor = this.executor; - var newTerm = new Query(name); - newTerm.args = Array.prototype.slice.call(arguments); - newQuery.args = this.args.concat([newTerm]); - return newQuery; - }; - }); - each(exports.knownScalarOperators, function(name){ - Query.prototype[name] = function(){ - var newQuery = new Query(); - newQuery.executor = this.executor; - var newTerm = new Query(name); - newTerm.args = Array.prototype.slice.call(arguments); - newQuery.args = this.args.concat([newTerm]); - return newQuery.executor(newQuery); - }; - }); - each(exports.arrayMethods, function(name){ - // this makes no guarantee of ensuring that results supports these methods - Query.prototype[name] = function(){ - var args = arguments; - return when(this.executor(this), function(results){ - return results[name].apply(results, args); - }); - }; - }); + exports.updateQueryMethods = function(){ + each(exports.knownOperators, function(name){ + Query.prototype[name] = function(){ + var newQuery = new Query(); + newQuery.executor = this.executor; + var newTerm = new Query(name); + newTerm.args = Array.prototype.slice.call(arguments); + newQuery.args = this.args.concat([newTerm]); + return newQuery; + }; + }); + each(exports.knownScalarOperators, function(name){ + Query.prototype[name] = function(){ + var newQuery = new Query(); + newQuery.executor = this.executor; + var newTerm = new Query(name); + newTerm.args = Array.prototype.slice.call(arguments); + newQuery.args = this.args.concat([newTerm]); + return newQuery.executor(newQuery); + }; + }); + each(exports.arrayMethods, function(name){ + // this makes no guarantee of ensuring that results supports these methods + Query.prototype[name] = function(){ + var args = arguments; + return when(this.executor(this), function(results){ + return results[name].apply(results, args); + }); + }; + }); -}; + }; -exports.updateQueryMethods(); + exports.updateQueryMethods(); -/* recursively iterate over query terms calling 'fn' for each term */ -Query.prototype.walk = function(fn, options){ - options = options || {}; - function walk(name, terms){ - terms = terms || []; + /* recursively iterate over query terms calling 'fn' for each term */ + Query.prototype.walk = function(fn, options){ + options = options || {}; + function walk(name, terms){ + terms = terms || []; - var i = 0, - l = terms.length, - term, - args, - func, - newTerm; + var i = 0, + l = terms.length, + term, + args, + func, + newTerm; - for (; i < l; i++) { - term = terms[i]; - if (term == null) { - term = {}; - } - func = term.name; - args = term.args; - if (!func || !args) { - continue; - } - if (args[0] instanceof Query) { - walk.call(this, func, args); - } - else { - newTerm = fn.call(this, func, args); - if (newTerm && newTerm.name && newTerm.ags) { - terms[i] = newTerm; + for (; i < l; i++) { + term = terms[i]; + if (term == null) { + term = {}; + } + func = term.name; + args = term.args; + if (!func || !args) { + continue; + } + if (args[0] instanceof Query) { + walk.call(this, func, args); + } + else { + newTerm = fn.call(this, func, args); + if (newTerm && newTerm.name && newTerm.ags) { + terms[i] = newTerm; + } } } } - } - walk.call(this, this.name, this.args); -}; - -/* append a new term */ -Query.prototype.push = function(term){ - this.args.push(term); - return this; -}; - -/* disambiguate query */ -Query.prototype.normalize = function(options){ - options = options || {}; - options.primaryKey = options.primaryKey || 'id'; - options.map = options.map || {}; - var result = { - original: this, - sort: [], - limit: [Infinity, 0, Infinity], - skip: 0, - limit: Infinity, - select: [], - values: false + walk.call(this, this.name, this.args); }; - var plusMinus = { - // [plus, minus] - sort: [1, -1], - select: [1, 0] + + /* append a new term */ + Query.prototype.push = function(term){ + this.args.push(term); + return this; }; - function normal(func, args){ - // cache some parameters - if (func === 'sort' || func === 'select') { - result[func] = args; - var pm = plusMinus[func]; - result[func+'Arr'] = result[func].map(function(x){ - if (x instanceof Array) x = x.join('.'); - var o = {}; - var a = /([-+]*)(.+)/.exec(x); - o[a[2]] = pm[(a[1].charAt(0) === '-')*1]; - return o; - }); - result[func+'Obj'] = {}; - result[func].forEach(function(x){ - if (x instanceof Array) x = x.join('.'); - var a = /([-+]*)(.+)/.exec(x); - result[func+'Obj'][a[2]] = pm[(a[1].charAt(0) === '-')*1]; - }); - } else if (func === 'limit') { - // validate limit() args to be numbers, with sane defaults - var limit = args; - result.skip = +limit[1] || 0; - limit = +limit[0] || 0; - if (options.hardLimit && limit > options.hardLimit) - limit = options.hardLimit; - result.limit = limit; - result.needCount = true; - } else if (func === 'values') { - // N.B. values() just signals we want array of what we select() - result.values = true; - } else if (func === 'eq') { - // cache primary key equality -- useful to distinguish between .get(id) and .query(query) - var t = typeof args[1]; - //if ((args[0] instanceof Array ? args[0][args[0].length-1] : args[0]) === options.primaryKey && ['string','number'].indexOf(t) >= 0) { - if (args[0] === options.primaryKey && ('string' === t || 'number' === t)) { - result.pk = String(args[1]); + + /* disambiguate query */ + Query.prototype.normalize = function(options){ + options = options || {}; + options.primaryKey = options.primaryKey || 'id'; + options.map = options.map || {}; + var result = { + original: this, + sort: [], + limit: [Infinity, 0, Infinity], + skip: 0, + limit: Infinity, + select: [], + values: false + }; + var plusMinus = { + // [plus, minus] + sort: [1, -1], + select: [1, 0] + }; + function normal(func, args){ + // cache some parameters + if (func === 'sort' || func === 'select') { + result[func] = args; + var pm = plusMinus[func]; + result[func+'Arr'] = result[func].map(function(x){ + if (x instanceof Array) x = x.join('.'); + var o = {}; + var a = /([-+]*)(.+)/.exec(x); + o[a[2]] = pm[(a[1].charAt(0) === '-')*1]; + return o; + }); + result[func+'Obj'] = {}; + result[func].forEach(function(x){ + if (x instanceof Array) x = x.join('.'); + var a = /([-+]*)(.+)/.exec(x); + result[func+'Obj'][a[2]] = pm[(a[1].charAt(0) === '-')*1]; + }); + } else if (func === 'limit') { + // validate limit() args to be numbers, with sane defaults + var limit = args; + result.skip = +limit[1] || 0; + limit = +limit[0] || 0; + if (options.hardLimit && limit > options.hardLimit) + limit = options.hardLimit; + result.limit = limit; + result.needCount = true; + } else if (func === 'values') { + // N.B. values() just signals we want array of what we select() + result.values = true; + } else if (func === 'eq') { + // cache primary key equality -- useful to distinguish between .get(id) and .query(query) + var t = typeof args[1]; + //if ((args[0] instanceof Array ? args[0][args[0].length-1] : args[0]) === options.primaryKey && ['string','number'].indexOf(t) >= 0) { + if (args[0] === options.primaryKey && ('string' === t || 'number' === t)) { + result.pk = String(args[1]); + } } + // cache search conditions + //if (options.known[func]) + // map some functions + /*if (options.map[func]) { + func = options.map[func]; + }*/ } - // cache search conditions - //if (options.known[func]) - // map some functions - /*if (options.map[func]) { - func = options.map[func]; - }*/ - } - this.walk(normal); - return result; -}; + this.walk(normal); + return result; + }; -/* FIXME: an example will be welcome -Query.prototype.toMongo = function(options){ - return this.normalize({ - primaryKey: '_id', - map: { - ge: 'gte', - le: 'lte' - }, - known: ['lt','lte','gt','gte','ne','in','nin','not','mod','all','size','exists','type','elemMatch'] - }); -}; -*/ + /* FIXME: an example will be welcome + Query.prototype.toMongo = function(options){ + return this.normalize({ + primaryKey: '_id', + map: { + ge: 'gte', + le: 'lte' + }, + known: ['lt','lte','gt','gte','ne','in','nin','not','mod','all','size','exists','type','elemMatch'] + }); + }; + */ + + return exports; +} + +if(typeof define!="undefined") { + define(['exports', './parser', './util/each'], function(exports, parser, each) { + return exportFactory(exports, parser, each); + }); +}else { + exportFactory( + module.exports, + require("./parser"), + require("./util/each")); +} -return exports; -}); diff --git a/specification/draft-zyp-rql-00.html b/specification/draft-zyp-rql-00.html old mode 100644 new mode 100755 diff --git a/specification/draft-zyp-rql-00.xml b/specification/draft-zyp-rql-00.xml old mode 100644 new mode 100755 From 1b05876bedf461ecb66797713b5b1d2d5028cf7d Mon Sep 17 00:00:00 2001 From: michiel Date: Wed, 24 Oct 2018 08:40:22 +0200 Subject: [PATCH 2/4] Merge njust/rql master for "fixed webpack amd support" Merge burashka/rql master for "like support" --- js-array.d.ts | 6 ++ js-array.js | 127 +++++++----------------------- parser.d.ts | 5 ++ query.d.ts | 200 +++++++++++++++++++++++++++++++++++++++++++++++ query.js | 17 ++-- util/contains.js | 13 +-- util/each.js | 14 ++-- 7 files changed, 262 insertions(+), 120 deletions(-) create mode 100644 js-array.d.ts mode change 100644 => 100755 js-array.js create mode 100644 parser.d.ts create mode 100644 query.d.ts diff --git a/js-array.d.ts b/js-array.d.ts new file mode 100644 index 0000000..c9cd8f7 --- /dev/null +++ b/js-array.d.ts @@ -0,0 +1,6 @@ +import {Query} from './query' +declare namespace RqlArray { + export function executeQuery(query: string | Query, options: any, target: any[]); +} + +export = RqlArray; \ No newline at end of file diff --git a/js-array.js b/js-array.js old mode 100644 new mode 100755 index d09f683..1b9e56a --- a/js-array.js +++ b/js-array.js @@ -432,106 +432,33 @@ return result; }; } -} -exports.evaluateProperty = evaluateProperty; -function evaluateProperty(object, property){ - if(property instanceof Array){ - each(property, function(part){ - object = object[decodeURIComponent(part)]; - }); - return object; - }else if(typeof property == "undefined"){ - return object; - }else{ - return object[decodeURIComponent(property)]; - } -}; -var conditionEvaluator = exports.conditionEvaluator = function(condition){ - var jsOperator = exports.jsOperatorMap[term.name]; - if(jsOperator){ - js += "(function(item){return item." + term[0] + jsOperator + "parameters[" + (index -1) + "][1];});"; - } - else{ - js += "operators['" + term.name + "']"; + exports.filter = filter; + exports.evaluateProperty = evaluateProperty; + + exports.executeQuery = function(query, options, target){ + return exports.query(query, options, target); + }; + exports.query = query; + exports.missingOperator = function(operator){ + throw new Error("Operator " + operator + " is not defined"); + }; + function throwMaxIterations(){ + throw new Error("Query has taken too much computation, and the user is not allowed to execute resource-intense queries. Increase maxIterations in your config file to allow longer running non-indexed queries to be processed."); } - return eval(js); + exports.maxIterations = 10000; + return exports; }; -exports.executeQuery = function(query, options, target){ - return exports.query(query, options, target); -} -exports.query = query; -exports.missingOperator = function(operator){ - throw new Error("Operator " + operator + " is not defined"); -} -function query(query, options, target){ - options = options || {}; - query = parseQuery(query, options.parameters); - function t(){} - t.prototype = exports.operators; - var operators = new t; - // inherit from exports.operators - for(var i in options.operators){ - operators[i] = options.operators[i]; - } - function op(name){ - return operators[name]||exports.missingOperator(name); - } - var parameters = options.parameters || []; - var js = ""; - function queryToJS(value){ - if(value && typeof value === "object" && !(value instanceof RegExp)){ - if(value instanceof Array){ - return '[' + each(value, function(value, emit){ - emit(queryToJS(value)); - }) + ']'; - }else{ - var jsOperator = exports.jsOperatorMap[value.name]; - if(jsOperator){ - // item['foo.bar'] ==> (item && item.foo && item.foo.bar && ...) - var path = value.args[0]; - var target = value.args[1]; - if (typeof target == "undefined"){ - var item = "item"; - target = path; - }else if(path instanceof Array){ - var item = "item"; - var escaped = []; - for(var i = 0;i < path.length; i++){ - escaped.push(stringify(path[i])); - item +="&&item[" + escaped.join("][") + ']'; - } - }else{ - var item = "item&&item[" + stringify(path) + "]"; - } - // use native Array.prototype.filter if available - var condition = item + jsOperator + queryToJS(target); - if (typeof Array.prototype.filter === 'function') { - return "(function(){return this.filter(function(item){return " + condition + "})})"; - //???return "this.filter(function(item){return " + condition + "})"; - } else { - return "(function(){var filtered = []; for(var i = 0, length = this.length; i < length; i++){var item = this[i];if(" + condition + "){filtered.push(item);}} return filtered;})"; - } - }else{ - if (value instanceof Date){ - return value.valueOf(); - } - return "(function(){return op('" + value.name + "').call(this" + - (value && value.args && value.args.length > 0 ? (", " + each(value.args, function(value, emit){ - emit(queryToJS(value)); - }).join(",")) : "") + - ")})"; - } - } - }else{ - return typeof value === "string" ? stringify(value) : value; - } - } - var evaluator = eval("(1&&function(target){return " + queryToJS(query) + ".call(target);})"); - return target ? evaluator(target) : evaluator; -} -function throwMaxIterations(){ - throw new Error("Query has taken too much computation, and the user is not allowed to execute resource-intense queries. Increase maxIterations in your config file to allow longer running non-indexed queries to be processed."); + +if(typeof define != "undefined") { + define(["exports", "./parser", "./query", "./util/each", "./util/contains"], function(exports, parser, QUERY, each, contains){ + return exportFactory(exports, parser, QUERY, each, contains); + }); +}else { + exportFactory( + module.exports, + require("./parser"), + require("./query"), + require("./util/each"), + require("./util/contains") + ); } -exports.maxIterations = 10000; -return exports; -}); diff --git a/parser.d.ts b/parser.d.ts new file mode 100644 index 0000000..6e8962f --- /dev/null +++ b/parser.d.ts @@ -0,0 +1,5 @@ +declare namespace RqlParser { + export function parseQuery(query: string); +} + +export = RqlParser; diff --git a/query.d.ts b/query.d.ts new file mode 100644 index 0000000..d29cd90 --- /dev/null +++ b/query.d.ts @@ -0,0 +1,200 @@ +declare namespace RqlQuery { + function updateQueryMethods(); + export type defaultOperator = (...params: any[]) => any; + const knownOperators: string[]; + class Query { + /** + * Filters for objects where the specified property's value is equal to the provided value + * + * @param {string} property + * @param {*} value + * @returns {Query} + * @memberof Query + */ + eq(property: string, value: any): Query; + + /** + * Filters for objects where the specified property's value is not equal to the provided value + * + * @param {string} property + * @param {*} value + * @returns {Query} + * @memberof Query + */ + ne(property: string, value: any): Query; + + /** + * Filters for objects where the specified property's value is greater than the provided value + * + * @param {string} property + * @param {*} value + * @returns {Query} + * @memberof Query + */ + gt(property: string, value: any): Query; + + /** + * Filters for objects where the specified property's value is less than the provided value + * + * @param {string} property + * @param {*} value + * @returns {Query} + * @memberof Query + */ + lt(property: string, value: any): Query; + + /** + * Filters for objects where the specified property's value is greater than or equal to the provided value + * + * @param {string} property + * @param {*} value + * @returns {Query} + * @memberof Query + */ + ge(property: string, value: any): Query; + + /** + * Filters for objects where the specified property's value is less than or equal to the provided value + * + * @param {string} property + * @param {*} value + * @returns {Query} + * @memberof Query + */ + le(property: string, value: any): Query; + + /** + * Filters for objects where the specified property's value is an array and the array contains any value that equals the provided value or satisfies the provided expression. + * + * @param {string} property + * @param {*} value + * @returns {Query} + * @memberof Query + */ + contains(property: string, value: any): Query; + + /** + * Trims each object down to the set of properties defined in the arguments + * + * @param {...string[]} properties + * @returns {Query} + * @memberof Query + */ + select(...properties: string[]): Query; + + /** + * Returns the given range of objects from the result set + * + * @param {number} count + * @param {number} [top] + * @returns {Query} + * @memberof Query + */ + limit(count: number, top?: number): Query; + /** + * Sorts by the given property in order specified by the prefix (+ for ascending, - for descending) + * + * @param {string[]} property + * @returns {Query} + * @memberof Query + */ + sort(...property: string[]): Query; + + /** + * The union of the given queries + * + * @param {...Query[]} queries + * @returns {Query} + * @memberof Query + */ + or(...queries: Query[]): Query; + + /** + * Applies all the given queries + * + * @param {...Query[]} queries + * @returns {Query} + * @memberof Query + */ + and(...queries: Query[]): Query; + + /** + * Convert the query to its string representation + * + * @returns {string} + * @memberof Query + */ + toString(): string; + + /** + * Add a query + * + * @param {Query} query + * @returns {Query} + * @memberof Query + */ + push(query: Query): Query; + + /** + * Returns the first record of the query's result set + * + * @returns {Query} + * @memberof Query + */ + first(): Query; + + /** + * Returns the count of the number of records in the query's result set + * + * @returns {Query} + * @memberof Query + */ + count(): Query; + + /** + * Returns the first and only record of the query's result set, or produces an error if the query's result set has more or less than one record in it. + * + * @returns {Query} + * @memberof Query + */ + one(): Query; + + /** + * Finds the sum of every value in the array or if the property argument is provided, returns the sum of the value of property for every object in the array + * + * @param {string} property + * @memberof Query + */ + sum(property: string); + + /** + * Finds the minimum of every value in the array or if the property argument is provided, returns the minimum of the value of property for every object in the array + * + * @param {string} property + * @memberof Query + */ + min(property: string); + + /** + * Finds the maximum of every value in the array or if the property argument is provided, returns the maximum of the value of property for every object in the array + * + * @param {string} property + * @memberof Query + */ + max(property: string); + + executor: (query: Query) => any; + match: defaultOperator; + in: defaultOperator; + out: defaultOperator; + excludes: defaultOperator; + values: defaultOperator; + distinct: defaultOperator; + recurse: defaultOperator; + aggregate: defaultOperator; + between: defaultOperator; + mean: defaultOperator; + } +} + +export = RqlQuery; \ No newline at end of file diff --git a/query.js b/query.js index 33ca30d..2cebdba 100644 --- a/query.js +++ b/query.js @@ -11,17 +11,14 @@ * // for each object that matches the query * }); */ -//({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"), require("./js-array"));}}). -//define(["exports", "./parser", "./js-array"], function(exports, parser, jsarray){ -({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"), require("./util/each"));}}). -define(["exports", "./parser", "./util/each"], function(exports, parser, each){ +function exportFactory(exports, parser, each) { -var parseQuery = parser.parseQuery; -try{ - var when = require("promised-io/promise").when; -}catch(e){ - when = function(value, callback){callback(value)}; -} + var parseQuery = parser.parseQuery, when; + try { + when = require("promised-io/promise").when; + } catch(e){ + when = function(value, callback){callback(value)}; + } parser.Query = function(seed, params){ if (typeof seed === 'string') diff --git a/util/contains.js b/util/contains.js index f707893..3508080 100644 --- a/util/contains.js +++ b/util/contains.js @@ -1,7 +1,3 @@ -({define:typeof define!=='undefined'?define:function(deps, factory){module.exports = factory(exports);}}). -define([], function(){ -return contains; - function contains(array, item){ for(var i = 0, l = array.length; i < l; i++){ if(array[i] === item){ @@ -9,4 +5,11 @@ function contains(array, item){ } } } -}); + +if(typeof define != "undefined") { + define([], function() { + return contains; + }); + }else { + module.exports = contains; +} diff --git a/util/each.js b/util/each.js index c1a1a75..e5f2091 100644 --- a/util/each.js +++ b/util/each.js @@ -1,7 +1,3 @@ -({define:typeof define!=='undefined'?define:function(deps, factory){module.exports = factory(exports);}}). -define([], function(){ -return each; - function each(array, callback){ var emit, result; if (callback.length > 1) { @@ -18,4 +14,12 @@ function each(array, callback){ } return result; } -}); + +if(typeof define != "undefined") { + define([], function() { + return each; + }); + }else { + module.exports = each; +} + \ No newline at end of file From 00b5421fa9750b18d4e47f5f589482ed2379454f Mon Sep 17 00:00:00 2001 From: michiel Date: Wed, 24 Oct 2018 08:53:21 +0200 Subject: [PATCH 3/4] Update readme and added like function to query.d.ts --- README.md | 20 ++++++++++++++++++++ query.d.ts | 12 +++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 092009c..539ee6a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ +# Fork information +This fork merges various other forks to include all relevant fixes and changes. + +Merged forks: +- https://github.com/njust/rql - for webpack compatibility +- https://github.com/burashka/rql - for like() support (among other fixes) + + +Other forks that might contain relevant code (i.e. are ahead of the main branch): +- https://github.com/greatcare/rql +- https://github.com/odin-public/rql +- https://github.com/rayros/rql +- https://github.com/zrlay/rql +- https://github.com/wshager/rql + +# RQL Resource Query Language (RQL) is a query language designed for use in URIs with object style data structures. This project includes the RQL specification and provides a JavaScript implementation of query @@ -127,6 +143,7 @@ for more less operators): * or(<query>,<query>,...) - The union of the given queries * not(query) - negation * eq(<property>,<value>) - Filters for objects where the specified property's value is equal to the provided value +* like(<property>,<value>) - Filters for objects where the specified property's value is like to the provided value in [glob format][1] * lt(<property>,<value>) - Filters for objects where the specified property's value is less than the provided value * le(<property>,<value>) - Filters for objects where the specified property's value is less than or equal to the provided value * gt(<property>,<value>) - Filters for objects where the specified property's value is greater than the provided value @@ -215,3 +232,6 @@ See the main APS Standard for more information: ### RQL Specification: * [https://github.com/persvr/rql/tree/master/specification](https://github.com/persvr/rql/tree/master/specification) + + +[1]: https://github.com/xiag-ag/rql-parser/#operators \ No newline at end of file diff --git a/query.d.ts b/query.d.ts index d29cd90..8fed06b 100644 --- a/query.d.ts +++ b/query.d.ts @@ -12,7 +12,17 @@ declare namespace RqlQuery { * @memberof Query */ eq(property: string, value: any): Query; - + + /** + * Filters for objects where the specified property's value is like to the provided value. Supports * and ? + * + * @param {string} property + * @param {*} value + * @returns {Query} + * @memberof Query + */ + like(property: string, value: any): Query; + /** * Filters for objects where the specified property's value is not equal to the provided value * From 794a0048e1f02a9f6193e4c9005b15524604ec6c Mon Sep 17 00:00:00 2001 From: michiel Date: Wed, 24 Oct 2018 08:56:24 +0200 Subject: [PATCH 4/4] Revert readme for PR branch --- README.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/README.md b/README.md index 539ee6a..31ed646 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,3 @@ -# Fork information -This fork merges various other forks to include all relevant fixes and changes. - -Merged forks: -- https://github.com/njust/rql - for webpack compatibility -- https://github.com/burashka/rql - for like() support (among other fixes) - - -Other forks that might contain relevant code (i.e. are ahead of the main branch): -- https://github.com/greatcare/rql -- https://github.com/odin-public/rql -- https://github.com/rayros/rql -- https://github.com/zrlay/rql -- https://github.com/wshager/rql - -# RQL Resource Query Language (RQL) is a query language designed for use in URIs with object style data structures. This project includes the RQL specification and provides a JavaScript implementation of query