Skip to content
This repository
Browse code

Merge git://github.com/dvv/rql

Conflicts:
	lib/js-array.js
	test/js-array.js
  • Loading branch information...
commit 3676ed1d0572a958f1f9d9064b33b1bf8ff87e80 2 parents dffb85e + c48cd9d
Kris Zyp kriszyp authored
59 lib/js-array.js
@@ -4,8 +4,10 @@
4 4 *
5 5 */
6 6
7   -({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"));}}).
8   -define(["exports", "./parser"], function(exports, parser){
  7 +({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"), require("./query"));}}).
  8 +define(["exports", "./parser", "./query"], function(exports, parser, QUERY){
  9 +//({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"));}}).
  10 +//define(["exports", "./parser"], function(exports, parser){
9 11
10 12 var parseQuery = parser.parseQuery;
11 13 var stringify = JSON.stringify || function(str){
@@ -52,12 +54,12 @@ exports.operators = {
52 54 }),
53 55 contains: filter(function(array, value){
54 56 if(typeof value == "function"){
55   - return array.some(function(i){
  57 + return array instanceof Array && array.some(function(i){
56 58 return value([i]).length;
57 59 });
58 60 }
59 61 else{
60   - return array.indexOf(value) > -1;
  62 + return array instanceof Array && array.indexOf(value) > -1;
61 63 }
62 64 }),
63 65 excludes: filter(function(array, value){
@@ -269,6 +271,10 @@ exports.filter = filter;
269 271 function filter(condition, not){
270 272 // convert to boolean right now
271 273 var filter = function(property, second){
  274 + if(typeof second == "undefined"){
  275 + second = property;
  276 + property = undefined;
  277 + }
272 278 var args = arguments;
273 279 var filtered = [];
274 280 for(var i = 0, length = this.length; i < length; i++){
@@ -300,8 +306,9 @@ function evaluateProperty(object, property){
300 306 object = object[decodeURIComponent(part)];
301 307 });
302 308 return object;
303   - }
304   - else{
  309 + }else if(typeof property == "undefined"){
  310 + return object;
  311 + }else{
305 312 return object[decodeURIComponent(property)];
306 313 }
307 314 };
@@ -332,6 +339,9 @@ function query(query, options, target){
332 339 for(var i in options.operators){
333 340 operators[i] = options.operators[i];
334 341 }
  342 + function op(name){
  343 + return operators[name]||exports.missingOperator(name);
  344 + }
335 345 var parameters = options.parameters || [];
336 346 var js = "";
337 347 function queryToJS(value){
@@ -340,6 +350,7 @@ function query(query, options, target){
340 350 if(jsOperator){
341 351 // item['foo.bar'] ==> (item && item.foo && item.foo.bar && ...)
342 352 var path = value.args[0];
  353 + var target = value.args[1];
343 354 if(path instanceof Array){
344 355 var item = "";
345 356 var escaped = [];
@@ -347,19 +358,39 @@ function query(query, options, target){
347 358 escaped.push(stringify(path[i]));
348 359 item +="&&item[" + escaped.join("][") + ']';
349 360 }
  361 + }else if (typeof path == "undefined"){
  362 + var item = "";
  363 + target = path;
350 364 }else{
351 365 var item = "&&item[" + stringify(path) + "]";
352 366 }
353   - item = "item" + item;
354   - if (jsOperator === '===' && value.args[1] instanceof RegExp){
355   - // N.B. matching requires String
356   - item = 'String('+item + '||"")';
357   - jsOperator = '.match('+value.args[1]+')&&""===';
358   - value.args[1] = '';
  367 + // comparison to regexps are special: we cope with eq(), ne(), the others coerce regexp to string
  368 + if (value.args[1] instanceof RegExp) {
  369 + if (jsOperator === '===' || jsOperator === '!==') {
  370 + // N.B. matching requires String
  371 + item = 'String('+item + '||"")';
  372 + if (jsOperator === '!==') item = '!'+item;
  373 + jsOperator = '.match('+value.args[1]+')&&""===';
  374 + target = '';
  375 + } else {
  376 + // TODO: more elegant?
  377 + // FIXME: require()ing ./query at top level makes circular dep and ./query.knownOperators becomes undefined
  378 + target = QUERY.encodeValue(target).split(':').slice(1).join(':');
  379 + // TODO: RECONSIDER, as under nodules require() is 10 times slower
  380 + // PLUS is breaks client-side require()
  381 + //value.args[1] = require('./query').encodeValue(value.args[1]).split(':').slice(1).join(':');
  382 + }
  383 + }
  384 + // use native Array.prototype.filter if available
  385 + var condition = item + jsOperator + queryToJS(target);
  386 + if (typeof Array.prototype.filter === 'function') {
  387 + return "(function(){return this.filter(function(item){return " + condition + "})})";
  388 + //???return "this.filter(function(item){return " + condition + "})";
  389 + } else {
  390 + 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;})";
359 391 }
360   - return "(function(){var filtered = []; for(var i = 0, length = this.length; i < length; i++){var item = this[i];if(" + item + jsOperator + queryToJS(value.args[1]) + "){filtered.push(item);}} return filtered;})";
361 392 }else{
362   - return "(function(){return (operators['" + value.name + "']||exports.missingOperator('" + value.name + "')).call(this" +
  393 + return "(function(){return op('" + value.name + "').call(this" +
363 394 (value && value.args && value.args.length > 0 ? (", " + value.args.map(queryToJS).join(",")) : "") +
364 395 ")})";
365 396 }
220 lib/query.js
@@ -3,12 +3,14 @@
3 3 * var Query = require("./query").Query;
4 4 * query = Query();
5 5 * query.executor = function(query){
6   - * require("./js-array").query(query, params, data); // if we want to operate on an array
  6 + * require("./js-array").query(query, params, data); // if we want to operate on an array
7 7 * };
8 8 * query.eq("a", 3).le("b", 4).forEach(function(object){
9   - * // for each object that matches the query
  9 + * // for each object that matches the query
10 10 * });
11 11 */
  12 +//({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"), require("./js-array"));}}).
  13 +//define(["exports", "./parser", "./js-array"], function(exports, parser, jsarray){
12 14 ({define:typeof define!="undefined"?define:function(deps, factory){module.exports = factory(exports, require("./parser"));}}).
13 15 define(["exports", "./parser"], function(exports, parser){
14 16
@@ -28,7 +30,8 @@ parser.Query = function(seed, params){
28 30 return q;
29 31 };
30 32 exports.Query = parser.Query;
31   -exports.knownOperators = ["and", "or", "eq", "ne", "le", "lt", "gt", "ge", "sort", "in", "notin", "select", "exclude", "contains", "notcontains", "values","aggregate","distinct","limit","recurse"];
  33 +//TODO:THE RIGHT WAY IS:exports.knownOperators = Object.keys(jsarray.operators || {}).concat(Object.keys(jsarray.jsOperatorMap || {}));
  34 +exports.knownOperators = ["sort", "in", "not", "any", "all", "or", "and", "select", "exclude", "values", "limit", "distinct", "recurse", "aggregate", "between", "sum", "mean", "max", "min", "count", "first", "one", "eq", "ne", "le", "ge", "lt", "gt"];
32 35 exports.knownScalarOperators = ["mean", "sum", "min", "max", "count", "first", "one"];
33 36 exports.arrayMethods = ["forEach", "reduce", "map", "filter", "indexOf", "some", "every"];
34 37
@@ -44,62 +47,62 @@ Query.prototype.toString = function(){
44 47 };
45 48
46 49 function queryToString(part) {
47   - if (part instanceof Array) {
48   - return '('+part.map(function(arg) {
49   - return queryToString(arg);
50   - }).join(",")+')';
51   - }
52   - if (part && part.name && part.args) {
53   - return [
54   - part.name,
55   - "(",
56   - part.args.map(function(arg) {
57   - return queryToString(arg);
58   - }).join(","),
59   - ")"
60   - ].join("");
61   - }
62   - return exports.encodeValue(part);
  50 + if (part instanceof Array) {
  51 + return '('+part.map(function(arg) {
  52 + return queryToString(arg);
  53 + }).join(",")+')';
  54 + }
  55 + if (part && part.name && part.args) {
  56 + return [
  57 + part.name,
  58 + "(",
  59 + part.args.map(function(arg, pos) {
  60 + return queryToString(arg);
  61 + }).join(","),
  62 + ")"
  63 + ].join("");
  64 + }
  65 + return exports.encodeValue(part);
63 66 };
64 67
65 68 function encodeString(s) {
66   - if (typeof s === "string") {
67   - s = encodeURIComponent(s);
68   - if (s.match(/[\(\)]/)) {
69   - s = s.replace("(","%28").replace(")","%29");
70   - };
71   - }
72   - return s;
  69 + if (typeof s === "string") {
  70 + s = encodeURIComponent(s);
  71 + if (s.match(/[\(\)]/)) {
  72 + s = s.replace("(","%28").replace(")","%29");
  73 + };
  74 + }
  75 + return s;
73 76 }
74 77
75 78 exports.encodeValue = function(val) {
76   - var encoded;
77   - if (val === null) val = 'null';
78   - if (val !== parser.converters["default"]('' + (
79   - val.toISOString && val.toISOString() || val.toString()
80   - ))) {
81   - var type = typeof val;
82   - if(val instanceof RegExp){
83   - // TODO: control whether to we want simpler glob() style
84   - val = val.toString();
85   - var i = val.lastIndexOf('/');
86   - type = val.substring(i).indexOf('i') >= 0 ? "re" : "RE";
87   - val = encodeString(val.substring(1, i));
88   - encoded = true;
89   - }
90   - if(type === "object"){
91   - type = "epoch";
92   - val = val.getTime();
93   - encoded = true;
94   - }
95   - if(type === "string") {
96   - val = encodeString(val);
97   - encoded = true;
98   - }
99   - val = [type, val].join(":");
100   - }
101   - if (!encoded && typeof val === "string") val = encodeString(val);
102   - return val;
  79 + var encoded;
  80 + if (val === null) val = 'null';
  81 + if (val !== parser.converters["default"]('' + (
  82 + val.toISOString && val.toISOString() || val.toString()
  83 + ))) {
  84 + var type = typeof val;
  85 + if(val instanceof RegExp){
  86 + // TODO: control whether to we want simpler glob() style
  87 + val = val.toString();
  88 + var i = val.lastIndexOf('/');
  89 + type = val.substring(i).indexOf('i') >= 0 ? "re" : "RE";
  90 + val = encodeString(val.substring(1, i));
  91 + encoded = true;
  92 + }
  93 + if(type === "object"){
  94 + type = "epoch";
  95 + val = val.getTime();
  96 + encoded = true;
  97 + }
  98 + if(type === "string") {
  99 + val = encodeString(val);
  100 + encoded = true;
  101 + }
  102 + val = [type, val].join(":");
  103 + }
  104 + if (!encoded && typeof val === "string") val = encodeString(val);
  105 + return val;
103 106 };
104 107
105 108 exports.updateQueryMethods = function(){
@@ -136,5 +139,116 @@ exports.updateQueryMethods = function(){
136 139
137 140 exports.updateQueryMethods();
138 141
  142 +/* recursively iterate over query terms calling 'fn' for each term */
  143 +Query.prototype.walk = function(fn, options){
  144 + options = options || {};
  145 + function walk(name, terms){
  146 + (terms || []).forEach(function(term, i, arr) {
  147 + var args, func, key, x;
  148 + term != null ? term : term = {};
  149 + func = term.name;
  150 + args = term.args;
  151 + if (!func || !args) {
  152 + return;
  153 + }
  154 + if (args[0] instanceof Query) {
  155 + walk.call(this, func, args);
  156 + } else {
  157 + var newTerm = fn.call(this, func, args);
  158 + if (newTerm && newTerm.name && newTerm.args)
  159 + arr[i] = newTerm;
  160 + }
  161 + });
  162 + }
  163 + walk.call(this, this.name, this.args);
  164 +};
  165 +
  166 +/* append a new term */
  167 +Query.prototype.push = function(term){
  168 + this.args.push(term);
  169 + return this;
  170 +};
  171 +
  172 +/* disambiguate query */
  173 +Query.prototype.normalize = function(options){
  174 + options = options || {};
  175 + options.primaryKey = options.primaryKey || 'id';
  176 + options.map = options.map || {};
  177 + var result = {
  178 + original: this,
  179 + sort: [],
  180 + limit: [Infinity, 0, Infinity],
  181 + skip: 0,
  182 + limit: Infinity,
  183 + select: [],
  184 + values: false
  185 + };
  186 + var plusMinus = {
  187 + // [plus, minus]
  188 + sort: [1, -1],
  189 + select: [1, 0]
  190 + };
  191 + function normal(func, args){
  192 + // cache some parameters
  193 + if (func === 'sort' || func === 'select') {
  194 + result[func] = args;
  195 + var pm = plusMinus[func];
  196 + result[func+'Arr'] = result[func].map(function(x){
  197 + if (x instanceof Array) x = x.join('.');
  198 + var o = {};
  199 + var a = /([-+]*)(.+)/.exec(x);
  200 + o[a[2]] = pm[(a[1].charAt(0) === '-')*1];
  201 + return o;
  202 + });
  203 + result[func+'Obj'] = {};
  204 + result[func].forEach(function(x){
  205 + if (x instanceof Array) x = x.join('.');
  206 + var a = /([-+]*)(.+)/.exec(x);
  207 + result[func+'Obj'][a[2]] = pm[(a[1].charAt(0) === '-')*1];
  208 + });
  209 + } else if (func === 'limit') {
  210 + // validate limit() args to be numbers, with sane defaults
  211 + var limit = args;
  212 + result.skip = +limit[1] || 0;
  213 + limit = +limit[0] || 0;
  214 + if (options.hardLimit && limit > options.hardLimit)
  215 + limit = options.hardLimit;
  216 + result.limit = limit;
  217 + result.needCount = true;
  218 + } else if (func === 'values') {
  219 + // N.B. values() just signals we want array of what we select()
  220 + result.values = true;
  221 + } else if (func === 'eq') {
  222 + // cache primary key equality -- useful to distinguish between .get(id) and .query(query)
  223 + var t = typeof args[1];
  224 + //if ((args[0] instanceof Array ? args[0][args[0].length-1] : args[0]) === options.primaryKey && ['string','number'].indexOf(t) >= 0) {
  225 + if (args[0] === options.primaryKey && ['string','number'].indexOf(t) >= 0) {
  226 + result.pk = String(args[1]);
  227 + }
  228 + }
  229 + // cache search conditions
  230 + //if (options.known[func])
  231 + // map some functions
  232 + /*if (options.map[func]) {
  233 + func = options.map[func];
  234 + }*/
  235 + }
  236 + this.walk(normal);
  237 + return result;
  238 +};
  239 +
  240 +/* FIXME: an example will be welcome
  241 +Query.prototype.toMongo = function(options){
  242 + return this.normalize({
  243 + primaryKey: '_id',
  244 + map: {
  245 + ge: 'gte',
  246 + le: 'lte'
  247 + },
  248 + known: ['lt','lte','gt','gte','ne','in','nin','not','mod','all','size','exists','type','elemMatch']
  249 + });
  250 +};
  251 +*/
  252 +
139 253 return exports;
140 254 });
14 test/js-array.js
@@ -31,8 +31,22 @@ exports.testFiltering = function() {
31 31 assert.equal(executeQuery("excludes(tags,ne(fun))", {}, data).length, 1);
32 32 assert.equal(executeQuery("excludes(tags,ne(even))", {}, data).length, 0);
33 33 assert.equal(executeQuery("excludes(tags,ne(even))", {}, data).length, 2);
  34 + // eq() on re: should trigger .match()
  35 + assert.deepEqual(executeQuery("price=re:10", {}, data), [data[0]]);
  36 + // ne() on re: should trigger .not(.match())
  37 + assert.deepEqual(executeQuery("ne(price,re:10)", {}, data), [data[1]]);
  38 + // other comparisons to re: should treat regexp as string
  39 + assert.deepEqual(executeQuery("lt(price,re:10)", {}, data), [data[1]]);
  40 + assert.deepEqual(executeQuery("gt(price,re:10)", {}, data), []);
34 41 };
35 42
  43 +exports.testFiltering1 = function() {
  44 + var data = [{"path.1":[1,2,3]}, {"path.1":[9,3,7]}];
  45 + assert.deepEqual(executeQuery("contains(path,3)&sort()", {}, data), []); // path is undefined
  46 + assert.deepEqual(executeQuery("contains(path.1,3)&sort()", {}, data), data); // 3 found in both
  47 + assert.deepEqual(executeQuery("excludes(path.1,3)&sort()", {}, data), []); // 3 found in both
  48 + assert.deepEqual(executeQuery("excludes(path.1,7)&sort()", {}, data), [data[0]]); // 7 found in second
  49 +};
36 50
37 51 if (require.main === module)
38 52 require("patr/runner").run(exports);
4 test/query.js
@@ -172,6 +172,10 @@ exports.testStringification = function() {
172 172 // string to array and back
173 173 var str = 'somefunc(and(1),(a,b),(10,(10,1)),(a,b.c))';
174 174 assert.equal(parseQuery(str)+'', str);
  175 + // quirky arguments
  176 + var name = ['a/b','c.d'];
  177 + assert.equal(parseQuery(Query().eq(name,1)+'')+'', 'eq((a%2Fb,c.d),1)');
  178 + assert.deepEqual(parseQuery(Query().eq(name,1)+'').args[0].args[0], name);
175 179 };
176 180
177 181 if (require.main === module)

0 comments on commit 3676ed1

Please sign in to comment.
Something went wrong with that request. Please try again.