Skip to content
Browse files

Updated documentation

Made AMD compliant (thanks @Rob--W)
Added tests for new features
Updated CAKE file
  • Loading branch information...
1 parent bfb0ca5 commit 141d491e912d07b2b74c00c83bc298322e6eb758 Dave Tonge committed Sep 17, 2012
Showing with 940 additions and 767 deletions.
  1. +2 −1 .gitignore
  2. +2 −5 Cakefile
  3. +1 −1 LICENSE
  4. +21 −5 README.md
  5. +0 −352 js/backbone-query.js
  6. +0 −1 js/backbone-query.min.js
  7. +459 −0 lib/backbone-query.js
  8. +1 −0 lib/backbone-query.min.js
  9. +1 −1 package.json
  10. +304 −299 src/backbone-query.coffee
  11. +149 −102 test/bq-test.coffee
View
3 .gitignore
@@ -1,3 +1,4 @@
# Idea IDE files
.idea/
-node_modules/
+node_modules/
+backbone_query.iml
View
7 Cakefile
@@ -213,9 +213,6 @@ mocha = (options, callback) ->
if typeof options is 'function'
callback = options
options = []
- # add coffee directive
- options.push '--coffee'
- options.push 'coffee:coffee-script'
launch 'mocha', options, callback
@@ -232,11 +229,11 @@ task 'uglify', 'Minify and obfuscate', ->
jsp = uglify.parser
pro = uglify.uglify
- contents = fs.readFileSync "js/backbone-query.js", 'utf8'
+ contents = fs.readFileSync "lib/backbone-query.js", 'utf8'
ast = jsp.parse contents # parse code and get the initial AST
ast = pro.ast_mangle ast # get a new AST with mangled names
ast = pro.ast_squeeze ast # get an AST with compression optimizations
final_code = pro.gen_code ast # compressed code here
- fs.writeFile 'js/backbone-query.min.js', final_code
+ fs.writeFile 'lib/backbone-query.min.js', final_code
View
2 LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011 Dave Tonge
+Copyright (c) 2012 Dave Tonge
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
View
26 README.md
@@ -21,10 +21,14 @@ You can install with NPM: `npm install backbone-query`
Then simply require in your project: `QueryCollection = require("backbone-query").QueryCollection`
-Your collections will now have two new methods: `query` and `where`. Both methods accept 2 arguments -
-a query object and an options object. The `query` method returns an array of models, but the `where` method
+Your collections will now have two new methods: `query` and `whereBy`. Both methods accept 2 arguments -
+a query object and an options object. The `query` method returns an array of models, but the `whereBy` method
returns a new collection and is therefore useful where you would like to chain multiple collection
-methods / where queries (thanks to @cezary). The following are some basic examples:
+methods / whereBy queries (thanks to @cezary).
+
+The library also supports nested compound queries and is AMD compatible (thanks to @Rob--W).
+
+The following are some basic examples:
```js
MyCollection.query({
@@ -71,14 +75,14 @@ MyCollection.query
colors: $contains: "yellow"
```
-Another CoffeeScript example, this time using `where` rather than `query`
+Another CoffeeScript example, this time using `whereBy` rather than `query`
```coffeescript
query =
$likes: $lt: 10
$downloads: $gt: 20
-MyCollection.where(query).my_custom_collection_method()
+MyCollection.whereBy(query).my_custom_collection_method()
```
@@ -351,6 +355,18 @@ MyCollection.query({ $not: { title: {$like: "News"}, likes: {$gt: 10}}});
// Returns all models that don't contain "News" in the title AND DON'T have more than 10 likes.
```
+If you need to perform multiple queries on the same key, then you can supply the query as an array:
+```js
+MyCollection.query({
+ $or:[
+ {title:"News"},
+ {title:"About"}
+ ]
+});
+// Returns all models with the title "News" or "About".
+```
+
+
Compound Queries
================
View
352 js/backbone-query.js
@@ -1,352 +0,0 @@
-// Generated by CoffeeScript 1.3.1
-
-/*
-Backbone Query - A lightweight query API for Backbone Collections
-(c)2012 - Dave Tonge
-May be freely distributed according to MIT license.
-*/
-
-
-(function() {
- var Backbone, detect, filter, get_cache, get_models, get_sorted_models, iterator, page_models, parse_query, perform_query, process_query, reject, sort_models, test_model_attribute, test_query_value, _,
- __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
-
- parse_query = function(raw_query) {
- var key, o, q, query_param, type, value, _results;
- _results = [];
- for (key in raw_query) {
- query_param = raw_query[key];
- o = {
- key: key
- };
- if (_.isRegExp(query_param)) {
- o.type = "$regex";
- o.value = query_param;
- } else if (_(query_param).isObject() && !_(query_param).isArray()) {
- for (type in query_param) {
- value = query_param[type];
- if (test_query_value(type, value)) {
- o.type = type;
- switch (type) {
- case "$elemMatch":
- case "$relationMatch":
- o.value = parse_query(value);
- break;
- case "$computed":
- q = {};
- q[key] = value;
- o.value = parse_query(q);
- break;
- default:
- o.value = value;
- }
- }
- }
- } else {
- o.type = "$equal";
- o.value = query_param;
- }
- if (o.type === "$equal" && _(o.value).isObject()) {
- o.type = "$oEqual";
- }
- _results.push(o);
- }
- return _results;
- };
-
- test_query_value = function(type, value) {
- switch (type) {
- case "$in":
- case "$nin":
- case "$all":
- case "$any":
- return _(value).isArray();
- case "$size":
- return _(value).isNumber();
- case "$regex":
- return _(value).isRegExp();
- case "$like":
- case "$likeI":
- return _(value).isString();
- case "$between":
- return _(value).isArray() && (value.length === 2);
- case "$cb":
- return _(value).isFunction();
- default:
- return true;
- }
- };
-
- test_model_attribute = function(type, value) {
- switch (type) {
- case "$like":
- case "$likeI":
- case "$regex":
- return _(value).isString();
- case "$contains":
- case "$all":
- case "$any":
- case "$elemMatch":
- return _(value).isArray();
- case "$size":
- return _(value).isArray() || _(value).isString();
- case "$in":
- case "$nin":
- return value != null;
- case "$relationMatch":
- return (value != null) && value.models;
- default:
- return true;
- }
- };
-
- perform_query = function(type, value, attr, model, key) {
- switch (type) {
- case "$equal":
- if (_(attr).isArray()) {
- return __indexOf.call(attr, value) >= 0;
- } else {
- return attr === value;
- }
- break;
- case "$oEqual":
- return _(attr).isEqual(value);
- case "$contains":
- return __indexOf.call(attr, value) >= 0;
- case "$ne":
- return attr !== value;
- case "$lt":
- return attr < value;
- case "$gt":
- return attr > value;
- case "$lte":
- return attr <= value;
- case "$gte":
- return attr >= value;
- case "$between":
- return (value[0] < attr && attr < value[1]);
- case "$in":
- return __indexOf.call(value, attr) >= 0;
- case "$nin":
- return __indexOf.call(value, attr) < 0;
- case "$all":
- return _(value).all(function(item) {
- return __indexOf.call(attr, item) >= 0;
- });
- case "$any":
- return _(attr).any(function(item) {
- return __indexOf.call(value, item) >= 0;
- });
- case "$size":
- return attr.length === value;
- case "$exists":
- case "$has":
- return (attr != null) === value;
- case "$like":
- return attr.indexOf(value) !== -1;
- case "$likeI":
- return attr.toLowerCase().indexOf(value.toLowerCase()) !== -1;
- case "$regex":
- return value.test(attr);
- case "$cb":
- return value.call(model, attr);
- case "$elemMatch":
- return iterator(attr, value, false, detect, "elemMatch");
- case "$relationMatch":
- return iterator(attr.models, value, false, detect, "relationMatch");
- case "$computed":
- return iterator([model], value, false, detect, "computed");
- default:
- return false;
- }
- };
-
- iterator = function(models, query, andOr, filterFunction, subQuery) {
- var parsed_query;
- if (subQuery == null) {
- subQuery = false;
- }
- parsed_query = subQuery ? query : parse_query(query);
- return filterFunction(models, function(model) {
- var attr, q, test, _i, _len;
- for (_i = 0, _len = parsed_query.length; _i < _len; _i++) {
- q = parsed_query[_i];
- attr = (function() {
- switch (subQuery) {
- case "elemMatch":
- return model[q.key];
- case "computed":
- return model[q.key]();
- default:
- return model.get(q.key);
- }
- })();
- test = test_model_attribute(q.type, attr);
- if (test) {
- test = perform_query(q.type, q.value, attr, model, q.key);
- }
- if (andOr === test) {
- return andOr;
- }
- }
- return !andOr;
- });
- };
-
- filter = function(array, test) {
- var val, _i, _len, _results;
- _results = [];
- for (_i = 0, _len = array.length; _i < _len; _i++) {
- val = array[_i];
- if (test(val)) {
- _results.push(val);
- }
- }
- return _results;
- };
-
- reject = function(array, test) {
- var val, _i, _len, _results;
- _results = [];
- for (_i = 0, _len = array.length; _i < _len; _i++) {
- val = array[_i];
- if (!test(val)) {
- _results.push(val);
- }
- }
- return _results;
- };
-
- detect = function(array, test) {
- var val, _i, _len;
- for (_i = 0, _len = array.length; _i < _len; _i++) {
- val = array[_i];
- if (test(val)) {
- return true;
- }
- }
- return false;
- };
-
- process_query = {
- $and: function(models, query) {
- return iterator(models, query, false, filter);
- },
- $or: function(models, query) {
- return iterator(models, query, true, filter);
- },
- $nor: function(models, query) {
- return iterator(models, query, true, reject);
- },
- $not: function(models, query) {
- return iterator(models, query, false, reject);
- }
- };
-
- get_cache = function(collection, query, options) {
- var cache, models, query_string, _ref;
- query_string = JSON.stringify(query);
- cache = (_ref = collection._query_cache) != null ? _ref : collection._query_cache = {};
- models = cache[query_string];
- if (!models) {
- models = get_sorted_models(collection, query, options);
- cache[query_string] = models;
- }
- return models;
- };
-
- get_models = function(collection, query) {
- var compound_query, models, reduce_iterator;
- compound_query = _.intersection(["$and", "$not", "$or", "$nor"], _(query).keys());
- models = collection.models;
- if (compound_query.length === 0) {
- return process_query.$and(models, query);
- } else {
- reduce_iterator = function(memo, query_type) {
- return process_query[query_type](memo, query[query_type]);
- };
- return _.reduce(compound_query, reduce_iterator, models);
- }
- };
-
- get_sorted_models = function(collection, query, options) {
- var models;
- models = get_models(collection, query);
- if (options.sortBy) {
- models = sort_models(models, options);
- }
- return models;
- };
-
- sort_models = function(models, options) {
- if (_(options.sortBy).isString()) {
- models = _(models).sortBy(function(model) {
- return model.get(options.sortBy);
- });
- } else if (_(options.sortBy).isFunction()) {
- models = _(models).sortBy(options.sortBy);
- }
- if (options.order === "desc") {
- models = models.reverse();
- }
- return models;
- };
-
- page_models = function(models, options) {
- var end, sliced_models, start, total_pages;
- if (options.offset) {
- start = options.offset;
- } else if (options.page) {
- start = (options.page - 1) * options.limit;
- } else {
- start = 0;
- }
- end = start + options.limit;
- sliced_models = models.slice(start, end);
- if (options.pager && _.isFunction(options.pager)) {
- total_pages = Math.ceil(models.length / options.limit);
- options.pager(total_pages, sliced_models);
- }
- return sliced_models;
- };
-
- if (typeof require !== 'undefined') {
- _ = require('underscore');
- Backbone = require('backbone');
- }
-
- Backbone.QueryCollection = Backbone.Collection.extend({
- query: function(query, options) {
- var models;
- if (options == null) {
- options = {};
- }
- if (options.cache) {
- models = get_cache(this, query, options);
- } else {
- models = get_sorted_models(this, query, options);
- }
- if (options.limit) {
- models = page_models(models, options);
- }
- return models;
- },
- find_one: function(query) {
- return this.query(query)[0];
- },
- where_by: function(params, options) {
- if (options == null) {
- options = {};
- }
- return new this.constructor(this.query(params, options));
- },
- reset_query_cache: function() {
- return this._query_cache = {};
- }
- });
-
- if (typeof exports !== "undefined") {
- exports.QueryCollection = Backbone.QueryCollection;
- }
-
-}).call(this);
View
1 js/backbone-query.min.js
@@ -1 +0,0 @@
-((function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o=Array.prototype.indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(b in this&&this[b]===a)return b;return-1};h=function(a){var b,c,d,e,f,g,i;i=[];for(b in a){e=a[b],c={key:b};if(_.isRegExp(e))c.type="$regex",c.value=e;else if(_(e).isObject()&&!_(e).isArray())for(f in e){g=e[f];if(n(f,g)){c.type=f;switch(f){case"$elemMatch":case"$relationMatch":c.value=h(g);break;case"$computed":d={},d[b]=g,c.value=h(d);break;default:c.value=g}}}else c.type="$equal",c.value=e;c.type==="$equal"&&_(c.value).isObject()&&(c.type="$oEqual"),i.push(c)}return i},n=function(a,b){switch(a){case"$in":case"$nin":case"$all":case"$any":return _(b).isArray();case"$size":return _(b).isNumber();case"$regex":return _(b).isRegExp();case"$like":case"$likeI":return _(b).isString();case"$between":return _(b).isArray()&&b.length===2;case"$cb":return _(b).isFunction();default:return!0}},m=function(a,b){switch(a){case"$like":case"$likeI":case"$regex":return _(b).isString();case"$contains":case"$all":case"$any":case"$elemMatch":return _(b).isArray();case"$size":return _(b).isArray()||_(b).isString();case"$in":case"$nin":return b!=null;case"$relationMatch":return b!=null&&b.models;default:return!0}},i=function(b,c,d,e,g){switch(b){case"$equal":return _(d).isArray()?o.call(d,c)>=0:d===c;case"$oEqual":return _(d).isEqual(c);case"$contains":return o.call(d,c)>=0;case"$ne":return d!==c;case"$lt":return d<c;case"$gt":return d>c;case"$lte":return d<=c;case"$gte":return d>=c;case"$between":return c[0]<d&&d<c[1];case"$in":return o.call(c,d)>=0;case"$nin":return o.call(c,d)<0;case"$all":return _(c).all(function(a){return o.call(d,a)>=0});case"$any":return _(d).any(function(a){return o.call(c,a)>=0});case"$size":return d.length===c;case"$exists":case"$has":return d!=null===c;case"$like":return d.indexOf(c)!==-1;case"$likeI":return d.toLowerCase().indexOf(c.toLowerCase())!==-1;case"$regex":return c.test(d);case"$cb":return c.call(e,d);case"$elemMatch":return f(d,c,!1,a,"elemMatch");case"$relationMatch":return f(d.models,c,!1,a,"relationMatch");case"$computed":return f([e],c,!1,a,"computed");default:return!1}},f=function(a,b,c,d,e){var f;return e==null&&(e=!1),f=e?b:h(b),d(a,function(a){var b,d,g,h,j;for(h=0,j=f.length;h<j;h++){d=f[h],b=function(){switch(e){case"elemMatch":return a[d.key];case"computed":return a[d.key]();default:return a.get(d.key)}}(),g=m(d.type,b),g&&(g=i(d.type,d.value,b,a,d.key));if(c===g)return c}return!c})},b=function(a,b){var c,d,e,f;f=[];for(d=0,e=a.length;d<e;d++)c=a[d],b(c)&&f.push(c);return f},k=function(a,b){var c,d,e,f;f=[];for(d=0,e=a.length;d<e;d++)c=a[d],b(c)||f.push(c);return f},a=function(a,b){var c,d,e;for(d=0,e=a.length;d<e;d++){c=a[d];if(b(c))return!0}return!1},j={$and:function(a,c){return f(a,c,!1,b)},$or:function(a,c){return f(a,c,!0,b)},$nor:function(a,b){return f(a,b,!0,k)},$not:function(a,b){return f(a,b,!1,k)}},c=function(a,b,c){var d,f,g,h;return g=JSON.stringify(b),d=(h=a._query_cache)!=null?h:a._query_cache={},f=d[g],f||(f=e(a,b,c),d[g]=f),f},d=function(a,b){var c,d,e;return c=_.intersection(["$and","$not","$or","$nor"],_(b).keys()),d=a.models,c.length===0?j.$and(d,b):(e=function(a,c){return j[c](a,b[c])},_.reduce(c,e,d))},e=function(a,b,c){var e;return e=d(a,b),c.sortBy&&(e=l(e,c)),e},l=function(a,b){return _(b.sortBy).isString()?a=_(a).sortBy(function(a){return a.get(b.sortBy)}):_(b.sortBy).isFunction()&&(a=_(a).sortBy(b.sortBy)),b.order==="desc"&&(a=a.reverse()),a},g=function(a,b){var c,d,e,f;return b.offset?e=b.offset:b.page?e=(b.page-1)*b.limit:e=0,c=e+b.limit,d=a.slice(e,c),b.pager&&_.isFunction(b.pager)&&(f=Math.ceil(a.length/b.limit),b.pager(f,d)),d};if(typeof require!="undefined"){if(typeof _=="undefined"||_===null)_=require("underscore");if(typeof Backbone=="undefined"||Backbone===null)Backbone=require("backbone")}Backbone.QueryCollection=Backbone.Collection.extend({query:function(a,b){var d;return b==null&&(b={}),b.cache?d=c(this,a,b):d=e(this,a,b),b.limit&&(d=g(d,b)),d},where:function(a,b){return b==null&&(b={}),new this.constructor(this.query(a,b))},reset_query_cache:function(){return this._query_cache={}}}),typeof exports!="undefined"&&(exports.QueryCollection=Backbone.QueryCollection)})).call(this)
View
459 lib/backbone-query.js
@@ -0,0 +1,459 @@
+// Generated by CoffeeScript 1.3.3
+/*
+Backbone Query - A lightweight query API for Backbone Collections
+(c)2012 - Dave Tonge
+May be freely distributed according to MIT license.
+*/
+
+/* UTILS
+*/
+
+var Backbone, detect, filter, getCache, getModels, getSortedModels, getType, iterator, makeObj, pageModels, parseQuery, performQuery, processQuery, reject, sortModels, testModelAttribute, testQueryValue, _,
+ __slice = [].slice,
+ __hasProp = {}.hasOwnProperty,
+ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+filter = function(array, test) {
+ var val, _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = array.length; _i < _len; _i++) {
+ val = array[_i];
+ if (test(val)) {
+ _results.push(val);
+ }
+ }
+ return _results;
+};
+
+reject = function(array, test) {
+ var val, _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = array.length; _i < _len; _i++) {
+ val = array[_i];
+ if (!test(val)) {
+ _results.push(val);
+ }
+ }
+ return _results;
+};
+
+detect = function(array, test) {
+ var val, _i, _len;
+ for (_i = 0, _len = array.length; _i < _len; _i++) {
+ val = array[_i];
+ if (test(val)) {
+ return true;
+ }
+ }
+ return false;
+};
+
+makeObj = function() {
+ var args, current, key, o, val;
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+ o = {};
+ current = o;
+ while (args.length) {
+ key = args.shift();
+ val = (args.length === 1 ? args.shift() : {});
+ current = current[key] = val;
+ }
+ return o;
+};
+
+getType = function(item) {
+ if (_.isRegExp(item)) {
+ return "$regex";
+ }
+ if (_.isDate(item)) {
+ return "$date";
+ }
+ if (_.isObject(item) && !_.isArray(item)) {
+ return "object";
+ }
+ if (_.isArray(item)) {
+ return "array";
+ }
+ if (_.isString(item)) {
+ return "string";
+ }
+ if (_.isNumber(item)) {
+ return "number";
+ }
+ if (_.isBoolean(item)) {
+ return "boolean";
+ }
+ if (_.isFunction(item)) {
+ return "function";
+ }
+ return false;
+};
+
+/*
+Function to parse raw queries
+@param {mixed} raw query
+@return {array} parsed query
+
+Allows queries of the following forms:
+query
+ name: "test"
+ id: $gte: 10
+
+query [
+ {name:"test"}
+ {id:$gte:10}
+]
+*/
+
+
+parseQuery = function(rawQuery) {
+ var key, o, paramType, q, query, queryArray, queryParam, type, val, value, _i, _len, _results;
+ if (_.isArray(rawQuery)) {
+ queryArray = rawQuery;
+ } else {
+ queryArray = (function() {
+ var _results;
+ _results = [];
+ for (key in rawQuery) {
+ if (!__hasProp.call(rawQuery, key)) continue;
+ val = rawQuery[key];
+ _results.push(makeObj(key, val));
+ }
+ return _results;
+ })();
+ }
+ _results = [];
+ for (_i = 0, _len = queryArray.length; _i < _len; _i++) {
+ query = queryArray[_i];
+ for (key in query) {
+ if (!__hasProp.call(query, key)) continue;
+ queryParam = query[key];
+ o = {
+ key: key
+ };
+ paramType = getType(queryParam);
+ switch (paramType) {
+ case "$regex":
+ case "$date":
+ o.type = paramType;
+ o.value = queryParam;
+ break;
+ case "object":
+ if (key === "$and" || key === "$or" || key === "$nor" || key === "$not") {
+ o.value = queryParam;
+ o.type = key;
+ o.key = null;
+ } else {
+ for (type in queryParam) {
+ value = queryParam[type];
+ if (testQueryValue(type, value)) {
+ o.type = type;
+ switch (type) {
+ case "$elemMatch":
+ case "$relationMatch":
+ o.value = parseQuery(value);
+ break;
+ case "$computed":
+ q = makeObj(key, value);
+ o.value = parseQuery(q);
+ break;
+ default:
+ o.value = value;
+ }
+ }
+ }
+ }
+ break;
+ default:
+ o.type = "$equal";
+ o.value = queryParam;
+ }
+ if ((o.type === "$equal") && (paramType === "object" || paramType === "array")) {
+ o.type = "$oEqual";
+ }
+ }
+ _results.push(o);
+ }
+ return _results;
+};
+
+testQueryValue = function(type, value) {
+ switch (type) {
+ case "$in":
+ case "$nin":
+ case "$all":
+ case "$any":
+ return _(value).isArray();
+ case "$size":
+ return _(value).isNumber();
+ case "$regex":
+ return _(value).isRegExp();
+ case "$like":
+ case "$likeI":
+ return _(value).isString();
+ case "$between":
+ return _(value).isArray() && (value.length === 2);
+ case "$cb":
+ return _(value).isFunction();
+ default:
+ return true;
+ }
+};
+
+testModelAttribute = function(type, value) {
+ switch (type) {
+ case "$like":
+ case "$likeI":
+ case "$regex":
+ return _(value).isString();
+ case "$contains":
+ case "$all":
+ case "$any":
+ case "$elemMatch":
+ return _(value).isArray();
+ case "$size":
+ return _(value).isArray() || _(value).isString();
+ case "$in":
+ case "$nin":
+ return value != null;
+ case "$relationMatch":
+ return (value != null) && value.models;
+ default:
+ return true;
+ }
+};
+
+performQuery = function(type, value, attr, model, key) {
+ switch (type) {
+ case "$equal":
+ if (_(attr).isArray()) {
+ return __indexOf.call(attr, value) >= 0;
+ } else {
+ return attr === value;
+ }
+ break;
+ case "$oEqual":
+ return _(attr).isEqual(value);
+ case "$contains":
+ return __indexOf.call(attr, value) >= 0;
+ case "$ne":
+ return attr !== value;
+ case "$lt":
+ return attr < value;
+ case "$gt":
+ return attr > value;
+ case "$lte":
+ return attr <= value;
+ case "$gte":
+ return attr >= value;
+ case "$between":
+ return (value[0] < attr && attr < value[1]);
+ case "$in":
+ return __indexOf.call(value, attr) >= 0;
+ case "$nin":
+ return __indexOf.call(value, attr) < 0;
+ case "$all":
+ return _(value).all(function(item) {
+ return __indexOf.call(attr, item) >= 0;
+ });
+ case "$any":
+ return _(attr).any(function(item) {
+ return __indexOf.call(value, item) >= 0;
+ });
+ case "$size":
+ return attr.length === value;
+ case "$exists":
+ case "$has":
+ return (attr != null) === value;
+ case "$like":
+ return attr.indexOf(value) !== -1;
+ case "$likeI":
+ return attr.toLowerCase().indexOf(value.toLowerCase()) !== -1;
+ case "$regex":
+ return value.test(attr);
+ case "$cb":
+ return value.call(model, attr);
+ case "$elemMatch":
+ return iterator(attr, value, false, detect, "elemMatch");
+ case "$relationMatch":
+ return iterator(attr.models, value, false, detect, "relationMatch");
+ case "$computed":
+ return iterator([model], value, false, detect, "computed");
+ case "$and":
+ case "$or":
+ case "$nor":
+ case "$not":
+ return (processQuery[type]([model], value)).length === 1;
+ default:
+ return false;
+ }
+};
+
+iterator = function(models, query, andOr, filterFunction, subQuery) {
+ var parsedQuery;
+ if (subQuery == null) {
+ subQuery = false;
+ }
+ parsedQuery = subQuery ? query : parseQuery(query);
+ return filterFunction(models, function(model) {
+ var attr, q, test, _i, _len;
+ for (_i = 0, _len = parsedQuery.length; _i < _len; _i++) {
+ q = parsedQuery[_i];
+ attr = (function() {
+ switch (subQuery) {
+ case "elemMatch":
+ return model[q.key];
+ case "computed":
+ return model[q.key]();
+ default:
+ return model.get(q.key);
+ }
+ })();
+ test = testModelAttribute(q.type, attr);
+ if (test) {
+ test = performQuery(q.type, q.value, attr, model, q.key);
+ }
+ if (andOr === test) {
+ return andOr;
+ }
+ }
+ return !andOr;
+ });
+};
+
+processQuery = {
+ $and: function(models, query) {
+ return iterator(models, query, false, filter);
+ },
+ $or: function(models, query) {
+ return iterator(models, query, true, filter);
+ },
+ $nor: function(models, query) {
+ return iterator(models, query, true, reject);
+ },
+ $not: function(models, query) {
+ return iterator(models, query, false, reject);
+ }
+};
+
+getCache = function(collection, query, options) {
+ var cache, models, queryString, _ref;
+ queryString = JSON.stringify(query);
+ cache = (_ref = collection._queryCache) != null ? _ref : collection._queryCache = {};
+ models = cache[queryString];
+ if (!models) {
+ models = getSortedModels(collection, query, options);
+ cache[queryString] = models;
+ }
+ return models;
+};
+
+getModels = function(collection, query) {
+ var compoundKeys, compoundQuery, key, queryKeys, reduceIterator, val;
+ queryKeys = _(query).keys();
+ compoundKeys = ["$and", "$not", "$or", "$nor"];
+ compoundQuery = _.intersection(compoundKeys, queryKeys);
+ if (compoundQuery.length === 0) {
+ return processQuery.$and(collection.models, query);
+ } else {
+ if (compoundQuery.length !== queryKeys.length) {
+ if (__indexOf.call(compoundQuery, "$and") < 0) {
+ query.$and = {};
+ compoundQuery.unshift("$and");
+ }
+ for (key in query) {
+ if (!__hasProp.call(query, key)) continue;
+ val = query[key];
+ if (!(__indexOf.call(compoundKeys, key) < 0)) {
+ continue;
+ }
+ query.$and[key] = val;
+ delete query[key];
+ }
+ }
+ reduceIterator = function(memo, queryType) {
+ return processQuery[queryType](memo, query[queryType]);
+ };
+ return _.reduce(compoundQuery, reduceIterator, collection.models);
+ }
+};
+
+getSortedModels = function(collection, query, options) {
+ var models;
+ models = getModels(collection, query);
+ if (options.sortBy) {
+ models = sortModels(models, options);
+ }
+ return models;
+};
+
+sortModels = function(models, options) {
+ if (_(options.sortBy).isString()) {
+ models = _(models).sortBy(function(model) {
+ return model.get(options.sortBy);
+ });
+ } else if (_(options.sortBy).isFunction()) {
+ models = _(models).sortBy(options.sortBy);
+ }
+ if (options.order === "desc") {
+ models = models.reverse();
+ }
+ return models;
+};
+
+pageModels = function(models, options) {
+ var end, sliced_models, start, total_pages;
+ if (options.offset) {
+ start = options.offset;
+ } else if (options.page) {
+ start = (options.page - 1) * options.limit;
+ } else {
+ start = 0;
+ }
+ end = start + options.limit;
+ sliced_models = models.slice(start, end);
+ if (options.pager && _.isFunction(options.pager)) {
+ total_pages = Math.ceil(models.length / options.limit);
+ options.pager(total_pages, sliced_models);
+ }
+ return sliced_models;
+};
+
+if (typeof require !== 'undefined') {
+ _ = require('underscore');
+ Backbone = require('backbone');
+}
+
+Backbone.QueryCollection = Backbone.Collection.extend({
+ query: function(query, options) {
+ var models;
+ if (options == null) {
+ options = {};
+ }
+ if (options.cache) {
+ models = getCache(this, query, options);
+ } else {
+ models = getSortedModels(this, query, options);
+ }
+ if (options.limit) {
+ models = pageModels(models, options);
+ }
+ return models;
+ },
+ findOne: function(query) {
+ return this.query(query)[0];
+ },
+ whereBy: function(params, options) {
+ if (options == null) {
+ options = {};
+ }
+ return new this.constructor(this.query(params, options));
+ },
+ resetQueryCache: function() {
+ return this._queryCache = {};
+ }
+});
+
+if (typeof exports !== "undefined") {
+ exports.QueryCollection = Backbone.QueryCollection;
+}
View
1 lib/backbone-query.min.js
@@ -0,0 +1 @@
+var Backbone,detect,filter,getCache,getModels,getSortedModels,getType,iterator,makeObj,pageModels,parseQuery,performQuery,processQuery,reject,sortModels,testModelAttribute,testQueryValue,_,__slice=[].slice,__hasProp={}.hasOwnProperty,__indexOf=[].indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(b in this&&this[b]===a)return b;return-1};filter=function(a,b){var c,d,e,f;f=[];for(d=0,e=a.length;d<e;d++)c=a[d],b(c)&&f.push(c);return f},reject=function(a,b){var c,d,e,f;f=[];for(d=0,e=a.length;d<e;d++)c=a[d],b(c)||f.push(c);return f},detect=function(a,b){var c,d,e;for(d=0,e=a.length;d<e;d++){c=a[d];if(b(c))return!0}return!1},makeObj=function(){var a,b,c,d,e;a=1<=arguments.length?__slice.call(arguments,0):[],d={},b=d;while(a.length)c=a.shift(),e=a.length===1?a.shift():{},b=b[c]=e;return d},getType=function(a){return _.isRegExp(a)?"$regex":_.isDate(a)?"$date":_.isObject(a)&&!_.isArray(a)?"object":_.isArray(a)?"array":_.isString(a)?"string":_.isNumber(a)?"number":_.isBoolean(a)?"boolean":_.isFunction(a)?"function":!1},parseQuery=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n;_.isArray(a)?g=a:g=function(){var c;c=[];for(b in a){if(!__hasProp.call(a,b))continue;j=a[b],c.push(makeObj(b,j))}return c}(),n=[];for(l=0,m=g.length;l<m;l++){f=g[l];for(b in f){if(!__hasProp.call(f,b))continue;h=f[b],c={key:b},d=getType(h);switch(d){case"$regex":case"$date":c.type=d,c.value=h;break;case"object":if(b==="$and"||b==="$or"||b==="$nor"||b==="$not")c.value=h,c.type=b,c.key=null;else for(i in h){k=h[i];if(testQueryValue(i,k)){c.type=i;switch(i){case"$elemMatch":case"$relationMatch":c.value=parseQuery(k);break;case"$computed":e=makeObj(b,k),c.value=parseQuery(e);break;default:c.value=k}}}break;default:c.type="$equal",c.value=h}c.type==="$equal"&&(d==="object"||d==="array")&&(c.type="$oEqual")}n.push(c)}return n},testQueryValue=function(a,b){switch(a){case"$in":case"$nin":case"$all":case"$any":return _(b).isArray();case"$size":return _(b).isNumber();case"$regex":return _(b).isRegExp();case"$like":case"$likeI":return _(b).isString();case"$between":return _(b).isArray()&&b.length===2;case"$cb":return _(b).isFunction();default:return!0}},testModelAttribute=function(a,b){switch(a){case"$like":case"$likeI":case"$regex":return _(b).isString();case"$contains":case"$all":case"$any":case"$elemMatch":return _(b).isArray();case"$size":return _(b).isArray()||_(b).isString();case"$in":case"$nin":return b!=null;case"$relationMatch":return b!=null&&b.models;default:return!0}},performQuery=function(a,b,c,d,e){switch(a){case"$equal":return _(c).isArray()?__indexOf.call(c,b)>=0:c===b;case"$oEqual":return _(c).isEqual(b);case"$contains":return __indexOf.call(c,b)>=0;case"$ne":return c!==b;case"$lt":return c<b;case"$gt":return c>b;case"$lte":return c<=b;case"$gte":return c>=b;case"$between":return b[0]<c&&c<b[1];case"$in":return __indexOf.call(b,c)>=0;case"$nin":return __indexOf.call(b,c)<0;case"$all":return _(b).all(function(a){return __indexOf.call(c,a)>=0});case"$any":return _(c).any(function(a){return __indexOf.call(b,a)>=0});case"$size":return c.length===b;case"$exists":case"$has":return c!=null===b;case"$like":return c.indexOf(b)!==-1;case"$likeI":return c.toLowerCase().indexOf(b.toLowerCase())!==-1;case"$regex":return b.test(c);case"$cb":return b.call(d,c);case"$elemMatch":return iterator(c,b,!1,detect,"elemMatch");case"$relationMatch":return iterator(c.models,b,!1,detect,"relationMatch");case"$computed":return iterator([d],b,!1,detect,"computed");case"$and":case"$or":case"$nor":case"$not":return processQuery[a]([d],b).length===1;default:return!1}},iterator=function(a,b,c,d,e){var f;return e==null&&(e=!1),f=e?b:parseQuery(b),d(a,function(a){var b,d,g,h,i;for(h=0,i=f.length;h<i;h++){d=f[h],b=function(){switch(e){case"elemMatch":return a[d.key];case"computed":return a[d.key]();default:return a.get(d.key)}}(),g=testModelAttribute(d.type,b),g&&(g=performQuery(d.type,d.value,b,a,d.key));if(c===g)return c}return!c})},processQuery={$and:function(a,b){return iterator(a,b,!1,filter)},$or:function(a,b){return iterator(a,b,!0,filter)},$nor:function(a,b){return iterator(a,b,!0,reject)},$not:function(a,b){return iterator(a,b,!1,reject)}},getCache=function(a,b,c){var d,e,f,g;return f=JSON.stringify(b),d=(g=a._queryCache)!=null?g:a._queryCache={},e=d[f],e||(e=getSortedModels(a,b,c),d[f]=e),e},getModels=function(a,b){var c,d,e,f,g,h;f=_(b).keys(),c=["$and","$not","$or","$nor"],d=_.intersection(c,f);if(d.length===0)return processQuery.$and(a.models,b);if(d.length!==f.length){__indexOf.call(d,"$and")<0&&(b.$and={},d.unshift("$and"));for(e in b){if(!__hasProp.call(b,e))continue;h=b[e];if(__indexOf.call(c,e)<0)b.$and[e]=h,delete b[e];else continue}}return g=function(a,c){return processQuery[c](a,b[c])},_.reduce(d,g,a.models)},getSortedModels=function(a,b,c){var d;return d=getModels(a,b),c.sortBy&&(d=sortModels(d,c)),d},sortModels=function(a,b){return _(b.sortBy).isString()?a=_(a).sortBy(function(a){return a.get(b.sortBy)}):_(b.sortBy).isFunction()&&(a=_(a).sortBy(b.sortBy)),b.order==="desc"&&(a=a.reverse()),a},pageModels=function(a,b){var c,d,e,f;return b.offset?e=b.offset:b.page?e=(b.page-1)*b.limit:e=0,c=e+b.limit,d=a.slice(e,c),b.pager&&_.isFunction(b.pager)&&(f=Math.ceil(a.length/b.limit),b.pager(f,d)),d},typeof require!="undefined"&&(_=require("underscore"),Backbone=require("backbone")),Backbone.QueryCollection=Backbone.Collection.extend({query:function(a,b){var c;return b==null&&(b={}),b.cache?c=getCache(this,a,b):c=getSortedModels(this,a,b),b.limit&&(c=pageModels(c,b)),c},findOne:function(a){return this.query(a)[0]},whereBy:function(a,b){return b==null&&(b={}),new this.constructor(this.query(a,b))},resetQueryCache:function(){return this._queryCache={}}}),typeof exports!="undefined"&&(exports.QueryCollection=Backbone.QueryCollection)
View
2 package.json
@@ -15,7 +15,7 @@
}
, "devDependencies": {
"coffee-script" : ">=1.x.x"
- ,"moacha": ""
+ ,"mocha": ""
,"uglify-js": "1.2.2"
}
, "scripts": {
View
603 src/backbone-query.coffee
@@ -3,313 +3,318 @@ Backbone Query - A lightweight query API for Backbone Collections
(c)2012 - Dave Tonge
May be freely distributed according to MIT license.
###
+((define) -> define 'backbone-query', (require, exports) ->
+ _ = require('underscore')
+ Backbone = require('backbone')
+
+ ### UTILS ###
+
+ # Custom Filter / Reject methods faster than underscore methods as use for loops
+ # http://jsperf.com/filter-vs-for-loop2
+ filter = (array, test) -> (val for val in array when test val)
+ reject = (array, test) -> (val for val in array when not test val)
+ detect = (array, test) ->
+ for val in array
+ return true if test val
+ false
+
+ # Utility Function to turn a list of values into an object
+ makeObj = (args...)->
+ o = {}
+ current = o
+ while args.length
+ key = args.shift()
+ val = (if args.length is 1 then args.shift() else {})
+ current = current[key] = val
+ o
+
+ # Get the type as a string
+ getType = (item) ->
+ return "$regex" if _.isRegExp(item)
+ return "$date" if _.isDate(item)
+ return "object" if _.isObject(item) and not _.isArray(item)
+ return "array" if _.isArray(item)
+ return "string" if _.isString(item)
+ return "number" if _.isNumber(item)
+ return "boolean" if _.isBoolean(item)
+ return "function" if _.isFunction(item)
+ false
+
+ ###
+ Function to parse raw queries
+ @param {mixed} raw query
+ @return {array} parsed query
+
+ Allows queries of the following forms:
+ query
+ name: "test"
+ id: $gte: 10
+
+ query [
+ {name:"test"}
+ {id:$gte:10}
+ ]
+ ###
+ parseQuery = (rawQuery) ->
+
+ if _.isArray(rawQuery)
+ queryArray = rawQuery
+ else
+ queryArray = (makeObj(key, val) for own key, val of rawQuery)
+
+ (for query in queryArray
+ for own key, queryParam of query
+ o = {key}
+ paramType = getType(queryParam)
+ switch paramType
+ # Test for Regexs and Dates as they can be supplied without an operator
+ when "$regex", "$date"
+ o.type = paramType
+ o.value = queryParam
+
+ # If the query paramater is an object then extract the key and value
+ when "object"
+ if key in ["$and", "$or", "$nor", "$not"]
+ o.value = queryParam
+ o.type = key
+ o.key = null
+ else
+ for type, value of queryParam
+ # Before adding the query, its value is checked to make sure it is the right type
+ if testQueryValue type, value
+ o.type = type
+ switch type
+ when "$elemMatch", "$relationMatch"
+ o.value = parseQuery value
+ when "$computed"
+ q = makeObj(key,value)
+ o.value = parseQuery q
+ else
+ o.value = value
+
+ # If the query_param is not an object or a regexp then revert to the default operator: $equal
+ else
+ o.type = "$equal"
+ o.value = queryParam
+
+ # For "$equal" queries with arrays or objects we need to perform a deep equal
+ if (o.type is "$equal") and (paramType in ["object","array"])
+ o.type = "$oEqual"
+ o)
+
+
+
+ # Tests query value, to ensure that it is of the correct type
+ testQueryValue = (type, value) ->
+ switch type
+ when "$in","$nin","$all", "$any" then _(value).isArray()
+ when "$size" then _(value).isNumber()
+ when "$regex" then _(value).isRegExp()
+ when "$like", "$likeI" then _(value).isString()
+ when "$between" then _(value).isArray() and (value.length is 2)
+ when "$cb" then _(value).isFunction()
+ else true
+
+ # Test each attribute that is being tested to ensure that is of the correct type
+ testModelAttribute = (type, value) ->
+ switch type
+ when "$like", "$likeI", "$regex" then _(value).isString()
+ when "$contains", "$all", "$any", "$elemMatch" then _(value).isArray()
+ when "$size" then _(value).isArray() or _(value).isString()
+ when "$in", "$nin" then value?
+ when "$relationMatch" then value? and value.models
+ else true
+
+ # Perform the actual query logic for each query and each model/attribute
+ performQuery = (type, value, attr, model, key) ->
+ switch type
+ when "$equal"
+ # If the attrubute is an array then search for the query value in the array the same as Mongo
+ if _(attr).isArray() then value in attr else attr is value
+ when "$oEqual" then _(attr).isEqual value
+ when "$contains" then value in attr
+ when "$ne" then attr isnt value
+ when "$lt" then attr < value
+ when "$gt" then attr > value
+ when "$lte" then attr <= value
+ when "$gte" then attr >= value
+ when "$between" then value[0] < attr < value[1]
+ when "$in" then attr in value
+ when "$nin" then attr not in value
+ when "$all" then _(value).all (item) -> item in attr
+ when "$any" then _(attr).any (item) -> item in value
+ when "$size" then attr.length is value
+ when "$exists", "$has" then attr? is value
+ when "$like" then attr.indexOf(value) isnt -1
+ when "$likeI" then attr.toLowerCase().indexOf(value.toLowerCase()) isnt -1
+ when "$regex" then value.test attr
+ when "$cb" then value.call model, attr
+ when "$elemMatch" then iterator attr, value, false, detect, "elemMatch"
+ when "$relationMatch" then iterator attr.models, value, false, detect, "relationMatch"
+ when "$computed" then iterator [model], value, false, detect, "computed"
+ when "$and", "$or", "$nor", "$not"
+ (processQuery[type]([model], value)).length is 1
+ else false
+
+
+ # The main iterator that actually applies the query
+ iterator = (models, query, andOr, filterFunction, subQuery = false) ->
+ parsedQuery = if subQuery then query else parseQuery query
+ # The collections filter or reject method is used to iterate through each model in the collection
+ filterFunction models, (model) ->
+ # For each model in the collection, iterate through the supplied queries
+ for q in parsedQuery
+ # Retrieve the attribute value from the model
+ attr = switch subQuery
+ when "elemMatch" then model[q.key]
+ when "computed" then model[q.key]()
+ else model.get(q.key)
+ # Check if the attribute value is the right type (some operators need a string, or an array)
+ test = testModelAttribute(q.type, attr)
+ # If the attribute test is true, perform the query
+ if test then test = performQuery q.type, q.value, attr, model, q.key
+ # If the query is an "or" query than as soon as a match is found we return "true"
+ # Whereas if the query is an "and" query then we return "false" as soon as a match isn't found.
+ return andOr if andOr is test
+
+ # For an "or" query, if all the queries are false, then we return false
+ # For an "and" query, if all the queries are true, then we return true
+ not andOr
+
+
+
+ # An object with or, and, nor and not methods
+ processQuery =
+ $and: (models, query) -> iterator models, query, false, filter
+ $or: (models, query) -> iterator models, query, true, filter
+ $nor: (models, query) -> iterator models, query, true, reject
+ $not: (models, query) -> iterator models, query, false, reject
+
+
+ # This method attempts to retrieve the result from the cache.
+ # If no match is found in the cache, then the query is run and
+ # the results are saved in the cache
+ getCache = (collection, query, options) ->
+ # Convert the query to a string to use as a key in the cache
+ queryString = JSON.stringify query
+ # Create cache if doesn't exist
+ cache = collection._queryCache ?= {}
+ # Retrieve cached results
+ models = cache[queryString]
+ # If no results are retrieved then use the get_models method and cache the result
+ unless models
+ models = getSortedModels collection, query, options
+ cache[queryString] = models
+ # Return the results
+ models
+ # This method get the unsorted results
+ getModels = (collection, query) ->
-### UTILS ###
-
-# Custom Filter / Reject methods faster than underscore methods as use for loops
-# http://jsperf.com/filter-vs-for-loop2
-filter = (array, test) -> (val for val in array when test val)
-reject = (array, test) -> (val for val in array when not test val)
-detect = (array, test) ->
- for val in array
- return true if test val
- false
-
-# Utility Function to turn a list of values into an object
-makeObj = (args...)->
- o = {}
- current = o
- while args.length
- key = args.shift()
- val = (if args.length is 1 then args.shift() else {})
- current = current[key] = val
- o
-
-# Get the type as a string
-getType = (item) ->
- return "$regex" if _.isRegExp(item)
- return "$date" if _.isDate(item)
- return "object" if _.isObject(item) and not _.isArray(item)
- return "array" if _.isArray(item)
- return "string" if _.isString(item)
- return "number" if _.isNumber(item)
- return "boolean" if _.isBoolean(item)
- return "function" if _.isFunction(item)
- false
-
-###
-Function to parse raw queries
-@param {mixed} raw query
-@return {array} parsed query
-
-Allows queries of the following forms:
-query
- name: "test"
- id: $gte: 10
-
-query [
- {name:"test"}
- {id:$gte:10}
-]
-###
-parseQuery = (rawQuery) ->
+ # Iterate through the query keys to check for any of the compound methods
+ # The resulting array will have "$and" and "$not" first as it is better to use these
+ # operators first when performing a compound query as they are likely to return less results
+ queryKeys = _(query).keys()
+ compoundKeys = ["$and", "$not", "$or", "$nor"]
+ compoundQuery = _.intersection compoundKeys, queryKeys
- if _.isArray(rawQuery)
- queryArray = rawQuery
- else
- queryArray = (makeObj(key, val) for own key, val of rawQuery)
-
- (for query in queryArray
- for own key, queryParam of query
- o = {key}
- paramType = getType(queryParam)
- switch paramType
- # Test for Regexs and Dates as they can be supplied without an operator
- when "$regex", "$date"
- o.type = paramType
- o.value = queryParam
-
- # If the query paramater is an object then extract the key and value
- when "object"
- if key in ["$and", "$or", "$nor", "$not"]
- o.value = parseQuery(queryParam)
- o.type = key
- o.key = null
- else
- for type, value of queryParam
- # Before adding the query, its value is checked to make sure it is the right type
- if testQueryValue type, value
- o.type = type
- switch type
- when "$elemMatch", "$relationMatch"
- o.value = parseQuery value
- when "$computed"
- q = makeObj(key,value)
- o.value = parseQuery q
- else
- o.value = value
-
- # If the query_param is not an object or a regexp then revert to the default operator: $equal
- else
- o.type = "$equal"
- o.value = queryParam
-
- # For "$equal" queries with arrays or objects we need to perform a deep equal
- if (o.type is "$equal") and (paramType in ["object","array"])
- o.type = "$oEqual"
- o)
-
-
-
-# Tests query value, to ensure that it is of the correct type
-testQueryValue = (type, value) ->
- switch type
- when "$in","$nin","$all", "$any" then _(value).isArray()
- when "$size" then _(value).isNumber()
- when "$regex" then _(value).isRegExp()
- when "$like", "$likeI" then _(value).isString()
- when "$between" then _(value).isArray() and (value.length is 2)
- when "$cb" then _(value).isFunction()
- else true
-
-# Test each attribute that is being tested to ensure that is of the correct type
-testModelAttribute = (type, value) ->
- switch type
- when "$like", "$likeI", "$regex" then _(value).isString()
- when "$contains", "$all", "$any", "$elemMatch" then _(value).isArray()
- when "$size" then _(value).isArray() or _(value).isString()
- when "$in", "$nin" then value?
- when "$relationMatch" then value? and value.models
- else true
-
-# Perform the actual query logic for each query and each model/attribute
-performQuery = (type, value, attr, model, key) ->
- switch type
- when "$equal"
- # If the attrubute is an array then search for the query value in the array the same as Mongo
- if _(attr).isArray() then value in attr else attr is value
- when "$oEqual" then _(attr).isEqual value
- when "$contains" then value in attr
- when "$ne" then attr isnt value
- when "$lt" then attr < value
- when "$gt" then attr > value
- when "$lte" then attr <= value
- when "$gte" then attr >= value
- when "$between" then value[0] < attr < value[1]
- when "$in" then attr in value
- when "$nin" then attr not in value
- when "$all" then _(value).all (item) -> item in attr
- when "$any" then _(attr).any (item) -> item in value
- when "$size" then attr.length is value
- when "$exists", "$has" then attr? is value
- when "$like" then attr.indexOf(value) isnt -1
- when "$likeI" then attr.toLowerCase().indexOf(value.toLowerCase()) isnt -1
- when "$regex" then value.test attr
- when "$cb" then value.call model, attr
- when "$elemMatch" then iterator attr, value, false, detect, "elemMatch"
- when "$relationMatch" then iterator attr.models, value, false, detect, "relationMatch"
- when "$computed" then iterator [model], value, false, detect, "computed"
- when "$and", "$or", "$nor", "$not"
- (processQuery[type]([model], value)).length
- else false
-
-
-# The main iterator that actually applies the query
-iterator = (models, query, andOr, filterFunction, subQuery = false) ->
- parsedQuery = if subQuery then query else parseQuery query
- # The collections filter or reject method is used to iterate through each model in the collection
- filterFunction models, (model) ->
- # For each model in the collection, iterate through the supplied queries
- for q in parsedQuery
- # Retrieve the attribute value from the model
- attr = switch subQuery
- when "elemMatch" then model[q.key]
- when "computed" then model[q.key]()
- else model.get(q.key)
- # Check if the attribute value is the right type (some operators need a string, or an array)
- test = testModelAttribute(q.type, attr)
- # If the attribute test is true, perform the query
- if test then test = performQuery q.type, q.value, attr, model, q.key
- # If the query is an "or" query than as soon as a match is found we return "true"
- # Whereas if the query is an "and" query then we return "false" as soon as a match isn't found.
- return andOr if andOr is test
-
- # For an "or" query, if all the queries are false, then we return false
- # For an "and" query, if all the queries are true, then we return true
- not andOr
-
-
-
-# An object with or, and, nor and not methods
-processQuery =
- $and: (models, query) -> iterator models, query, false, filter
- $or: (models, query) -> iterator models, query, true, filter
- $nor: (models, query) -> iterator models, query, true, reject
- $not: (models, query) -> iterator models, query, false, reject
-
-
-# This method attempts to retrieve the result from the cache.
-# If no match is found in the cache, then the query is run and
-# the results are saved in the cache
-getCache = (collection, query, options) ->
- # Convert the query to a string to use as a key in the cache
- queryString = JSON.stringify query
- # Create cache if doesn't exist
- cache = collection._queryCache ?= {}
- # Retrieve cached results
- models = cache[queryString]
- # If no results are retrieved then use the get_models method and cache the result
- unless models
- models = getSortedModels collection, query, options
- cache[queryString] = models
- # Return the results
- models
-
-# This method get the unsorted results
-getModels = (collection, query) ->
-
- # Iterate through the query keys to check for any of the compound methods
- # The resulting array will have "$and" and "$not" first as it is better to use these
- # operators first when performing a compound query as they are likely to return less results
- queryKeys = _(query).keys()
- compoundKeys = ["$and", "$not", "$or", "$nor"]
- compoundQuery = _.intersection compoundKeys, queryKeys
-
- if compoundQuery.length is 0
- # If no compound methods are found then use the "and" iterator
- processQuery.$and collection.models, query
- else
- # Detect if there is an implicit $and compundQuery operator
- if compoundQuery.length isnt queryKeys.length
- # Add the and compund query operator (with a sanity check that it doesn't exist)
- if "$and" not in compoundQuery
- query.$and = {}
- compoundQuery.unshift "$and"
- for own key, val of query when key not in compoundKeys
- query.$and[key] = val
- delete query[key]
-
-
- # Iterate through the compound methods using underscore reduce
- # The reduce iterator takes an array of models, performs the query and returns
- # the matched models for the next query
- reduceIterator = (memo, queryType) ->
- processQuery[queryType] memo, query[queryType]
-
- _.reduce compoundQuery, reduceIterator, collection.models
-
-# Gets the results and optionally sorts them
-getSortedModels = (collection, query, options) ->
- models = getModels collection, query
- if options.sortBy then models = sortModels models, options
- models
-
-# Sorts models either be a model attribute or with a callback
-sortModels = (models, options) ->
- # If the sortBy param is a string then we sort according to the model attribute with that string as a key
- if _(options.sortBy).isString()
- models = _(models).sortBy (model) -> model.get(options.sortBy)
- # If a function is supplied then it is passed directly to the sortBy iterator
- else if _(options.sortBy).isFunction()
- models = _(models).sortBy(options.sortBy)
-
- # If there is an order property of "desc" then the results can be reversed
- # (sortBy provides result in ascending order by default)
- if options.order is "desc" then models = models.reverse()
- # The sorted models are returned
- models
-
-# Slices the results set according to the supplied options
-pageModels = (models, options) ->
- # Expects object in the form: {limit: num, offset: num, page: num, pager:callback}
- if options.offset then start = options.offset
- else if options.page then start = (options.page - 1) * options.limit
- else start = 0
-
- end = start + options.limit
-
- # The results are sliced according to the calculated start and end params
- sliced_models = models[start...end]
-
- if options.pager and _.isFunction(options.pager)
- total_pages = Math.ceil (models.length / options.limit)
- options.pager total_pages, sliced_models
-
- sliced_models
-
-# If used on the server, then Backbone and Underscore are loaded as modules
-unless typeof require is 'undefined'
- _ = require 'underscore'
- Backbone = require 'backbone'
-
-Backbone.QueryCollection = Backbone.Collection.extend
-
- # The main query method
- query: (query, options = {}) ->
-
- # Retrieve matching models using the supplied query
- if options.cache
- models = getCache @, query, options
+ if compoundQuery.length is 0
+ # If no compound methods are found then use the "and" iterator
+ processQuery.$and collection.models, query
else
- models = getSortedModels @, query, options
-
- # If a limit param is specified than slice the results
- if options.limit then models = pageModels models, options
+ # Detect if there is an implicit $and compundQuery operator
+ if compoundQuery.length isnt queryKeys.length
+ # Add the and compund query operator (with a sanity check that it doesn't exist)
+ if "$and" not in compoundQuery
+ query.$and = {}
+ compoundQuery.unshift "$and"
+ for own key, val of query when key not in compoundKeys
+ query.$and[key] = val
+ delete query[key]
+
+
+ # Iterate through the compound methods using underscore reduce
+ # The reduce iterator takes an array of models, performs the query and returns
+ # the matched models for the next query
+ reduceIterator = (memo, queryType) ->
+ processQuery[queryType] memo, query[queryType]
+
+ _.reduce compoundQuery, reduceIterator, collection.models
+
+ # Gets the results and optionally sorts them
+ getSortedModels = (collection, query, options) ->
+ models = getModels collection, query
+ if options.sortBy then models = sortModels models, options
+ models
- # Return the results
+ # Sorts models either be a model attribute or with a callback
+ sortModels = (models, options) ->
+ # If the sortBy param is a string then we sort according to the model attribute with that string as a key
+ if _(options.sortBy).isString()
+ models = _(models).sortBy (model) -> model.get(options.sortBy)
+ # If a function is supplied then it is passed directly to the sortBy iterator
+ else if _(options.sortBy).isFunction()
+ models = _(models).sortBy(options.sortBy)
+
+ # If there is an order property of "desc" then the results can be reversed
+ # (sortBy provides result in ascending order by default)
+ if options.order is "desc" then models = models.reverse()
+ # The sorted models are returned
models
- findOne: (query) -> @query(query)[0]
+ # Slices the results set according to the supplied options
+ pageModels = (models, options) ->
+ # Expects object in the form: {limit: num, offset: num, page: num, pager:callback}
+ if options.offset then start = options.offset
+ else if options.page then start = (options.page - 1) * options.limit
+ else start = 0
- # Where method wraps query and returns a new collection
- whereBy: (params, options = {})->
- new @constructor @query params, options
+ end = start + options.limit
- # Helper method to reset the query cache
- # Defined as a separate method to make it easy to bind to collection's change/add/remove events
- resetQueryCache: -> @_queryCache = {}
+ # The results are sliced according to the calculated start and end params
+ sliced_models = models[start...end]
-# On the server the new Query Collection is added to exports
-unless typeof exports is "undefined"
+ if options.pager and _.isFunction(options.pager)
+ total_pages = Math.ceil (models.length / options.limit)
+ options.pager total_pages, sliced_models
+
+ sliced_models
+
+
+ Backbone.QueryCollection = Backbone.Collection.extend
+
+ # The main query method
+ query: (query, options = {}) ->
+
+ # Retrieve matching models using the supplied query
+ if options.cache
+ models = getCache @, query, options
+ else
+ models = getSortedModels @, query, options
+
+ # If a limit param is specified than slice the results
+ if options.limit then models = pageModels models, options
+
+ # Return the results
+ models
+
+ findOne: (query) -> @query(query)[0]
+
+ # Where method wraps query and returns a new collection
+ whereBy: (params, options = {})->
+ new @constructor @query params, options
+
+ # Helper method to reset the query cache
+ # Defined as a separate method to make it easy to bind to collection's change/add/remove events
+ resetQueryCache: -> @_queryCache = {}
+
+ # On the server the new Query Collection is added to exports
exports.QueryCollection = Backbone.QueryCollection
+).call this, if typeof define == 'function' and define.amd then define else (id, factory) ->
+ unless typeof exports is 'undefined'
+ factory ((id) -> require id), exports
+ else
+ # Load Underscore and backbone. No need to export QueryCollection in an module-less environment
+ factory ((id) -> this[if id == 'underscore' then '_' else 'Backbone']), {}
+ return
+
View
251 test/bq-test.coffee
@@ -3,8 +3,6 @@ assert = require('assert')
{QueryCollection} = require "../src/backbone-query"
Backbone = require('backbone')
-QueryCollection::findAll = QueryCollection::where_by
-
create = ->
new QueryCollection [
{title:"Home", colors:["red","yellow","blue"], likes:12, featured:true, content: "Dummy content about coffeescript"}
@@ -13,211 +11,212 @@ create = ->
]
+
+
+
describe "Backbone Query Tests", ->
-
+
it "Equals query", ->
a = create()
result = a.query title:"Home"
assert.equal result.length, 1
assert.equal result[0].get("title"), "Home"
- #result = a.findAll colors: "blue"
- #assert.equal result.length, 2
-
- #result = a.findAll colors: ["red", "blue"]
- #assert.equal result.length, 1
-
-
+ result = a.whereBy colors: "blue"
+ assert.equal result.length, 2
+ result = a.whereBy colors: ["red", "blue"]
+ assert.equal result.length, 1
+
it "Simple equals query (no results)", ->
a = create()
- result = a.findAll title:"Homes"
+ result = a.whereBy title:"Homes"
assert.equal result.length, 0
it "Simple equals query with explicit $equal", ->
a = create()
- result = a.findAll title: {$equal: "About"}
+ result = a.whereBy title: {$equal: "About"}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "About"
it "$contains operator", ->
a = create()
- result = a.findAll colors: {$contains: "blue"}
+ result = a.whereBy colors: {$contains: "blue"}
assert.equal result.length, 2
it "$ne operator", ->
a = create()
- result = a.findAll title: {$ne: "Home"}
+ result = a.whereBy title: {$ne: "Home"}
assert.equal result.length, 2
it "$lt operator", ->
a = create()
- result = a.findAll likes: {$lt: 12}
+ result = a.whereBy likes: {$lt: 12}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "About"
it "$lte operator", ->
a = create()
- result = a.findAll likes: {$lte: 12}
+ result = a.whereBy likes: {$lte: 12}
assert.equal result.length, 2
it "$gt operator", ->
a = create()
- result = a.findAll likes: {$gt: 12}
+ result = a.whereBy likes: {$gt: 12}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "Contact"
it "$gte operator", ->
a = create()
- result = a.findAll likes: {$gte: 12}
+ result = a.whereBy likes: {$gte: 12}
assert.equal result.length, 2
it "$between operator", ->
a = create()
- result = a.findAll likes: {$between: [1,5]}
+ result = a.whereBy likes: {$between: [1,5]}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "About"
it "$in operator", ->
a = create()
- result = a.findAll title: {$in: ["Home","About"]}
+ result = a.whereBy title: {$in: ["Home","About"]}
assert.equal result.length, 2
it "$in operator with wrong query value", ->
a = create()
- result = a.findAll title: {$in: "Home"}
+ result = a.whereBy title: {$in: "Home"}
assert.equal result.length, 0
it "$nin operator", ->
a = create()
- result = a.findAll title: {$nin: ["Home","About"]}
+ result = a.whereBy title: {$nin: ["Home","About"]}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "Contact"
it "$all operator", ->
a = create()
- result = a.findAll colors: {$all: ["red","blue"]}
+ result = a.whereBy colors: {$all: ["red","blue"]}
assert.equal result.length, 2
it "$all operator (wrong values)", ->
a = create()
- result = a.findAll title: {$all: ["red","blue"]}
+ result = a.whereBy title: {$all: ["red","blue"]}
assert.equal result.length, 0
- result = a.findAll colors: {$all: "red"}
+ result = a.whereBy colors: {$all: "red"}
assert.equal result.length, 0
it "$any operator", ->
a = create()
- result = a.findAll colors: {$any: ["red","blue"]}
+ result = a.whereBy colors: {$any: ["red","blue"]}
assert.equal result.length, 3
- result = a.findAll colors: {$any: ["yellow","blue"]}
+ result = a.whereBy colors: {$any: ["yellow","blue"]}
assert.equal result.length, 2
it "$size operator", ->
a = create()
- result = a.findAll colors: {$size: 3}
+ result = a.whereBy colors: {$size: 3}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "Home"
it "$exists operator", ->
a = create()
- result = a.findAll featured: {$exists: true}
+ result = a.whereBy featured: {$exists: true}
assert.equal result.length, 2
it "$has operator", ->
a = create()
- result = a.findAll featured: {$exists: false}
+ result = a.whereBy featured: {$exists: false}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "Contact"
it "$like operator", ->
a = create()
- result = a.findAll content: {$like: "javascript"}
+ result = a.whereBy content: {$like: "javascript"}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "About"
it "$like operator 2", ->
a = create()
- result = a.findAll content: {$like: "content"}
+ result = a.whereBy content: {$like: "content"}
assert.equal result.length, 3
it "$likeI operator", ->
a = create()
- result = a.findAll content: {$likeI: "dummy"}
+ result = a.whereBy content: {$likeI: "dummy"}
assert.equal result.length, 3
- result = a.findAll content: {$like: "dummy"}
+ result = a.whereBy content: {$like: "dummy"}
assert.equal result.length, 1
it "$regex", ->
a = create()
- result = a.findAll content: {$regex: /javascript/gi}
+ result = a.whereBy content: {$regex: /javascript/gi}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "About"
it "$regex2", ->
a = create()
- result = a.findAll content: {$regex: /dummy/}
+ result = a.whereBy content: {$regex: /dummy/}
assert.equal result.length, 1
it "$regex3", ->
a = create()
- result = a.findAll content: {$regex: /dummy/i}
+ result = a.whereBy content: {$regex: /dummy/i}
assert.equal result.length, 3
it "$regex4", ->
a = create()
- result = a.findAll content: /javascript/i
+ result = a.whereBy content: /javascript/i
assert.equal result.length, 1
it "$cb - callback", ->
a = create()
- result = a.findAll title: {$cb: (attr) -> attr.charAt(0).toLowerCase() is "c"}
+ result = a.whereBy title: {$cb: (attr) -> attr.charAt(0).toLowerCase() is "c"}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "Contact"
it "$cb - callback - checking 'this' is the model", ->
a = create()
- result = a.findAll title:
+ result = a.whereBy title:
$cb: (attr) -> @get("title") is "Home"
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "Home"
it "$and operator", ->
a = create()
- result = a.findAll likes: {$gt: 5}, colors: {$contains: "yellow"}
+ result = a.whereBy likes: {$gt: 5}, colors: {$contains: "yellow"}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "Home"
it "$and operator (explicit)", ->
a = create()
- result = a.findAll $and: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
+ result = a.whereBy $and: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "Home"
it "$or operator", ->
a = create()
- result = a.findAll $or: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
+ result = a.whereBy $or: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
assert.equal result.length, 2
it "$or2 operator", ->
a = create()
- result = a.findAll $or: {likes: {$gt: 5}, featured: true}
+ result = a.whereBy $or: {likes: {$gt: 5}, featured: true}
assert.equal result.length, 3
it "$nor operator", ->
a = create()
- result = a.findAll $nor: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
+ result = a.whereBy $nor: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
assert.equal result.length, 1
assert.equal result.at(0).get("title"), "About"
it "Compound Queries", ->
a = create()
- result = a.findAll $and: {likes: {$gt: 5}}, $or: {content: {$like: "PHP"}, colors: {$contains: "yellow"}}
+ result = a.whereBy $and: {likes: {$gt: 5}}, $or: {content: {$like: "PHP"}, colors: {$contains: "yellow"}}
assert.equal result.length, 2
- result = a.findAll
+ result = a.whereBy
$and:
likes: $lt: 15
$or:
@@ -234,114 +233,117 @@ describe "Backbone Query Tests", ->
it "Limit", ->
a = create()
- result = a.findAll {likes: {$gt: 1}}, {limit:2}
+ result = a.whereBy {likes: {$gt: 1}}, {limit:2}
assert.equal result.length, 2
it "Offset", ->
a = create()
- result = a.findAll {likes: {$gt: 1}}, {limit:2, offset:2}
+ result = a.whereBy {likes: {$gt: 1}}, {limit:2, offset:2}
assert.equal result.length, 1
it "Page", ->
a = create()
- result = a.findAll {likes: {$gt: 1}}, {limit:3, page:2}
+ result = a.whereBy {likes: {$gt: 1}}, {limit:3, page:2}
assert.equal result.length, 0
it "Sorder by model key", ->
a = create()
- result = a.findAll {likes: {$gt: 1}}, {sortBy:"likes"}
+ result = a.query {likes: {$gt: 1}}, {sortBy:"likes"}
assert.equal result.length, 3
- #assert.equal result.at(0).get("title"), "About"
- #assert.equal result[1].get("title"), "Home"
- #assert.equal result[2].get("title"), "Contact"
+ assert.equal result[0].get("title"), "About"
+ assert.equal result[1].get("title"), "Home"
+ assert.equal result[2].get("title"), "Contact"
it "Sorder by model key with descending order", ->
a = create()
- result = a.findAll {likes: {$gt: 1}}, {sortBy:"likes", order:"desc"}
+ result = a.query {likes: {$gt: 1}}, {sortBy:"likes", order:"desc"}
assert.equal result.length, 3
- #assert.equal result[2].get("title"), "About"
- #assert.equal result[1].get("title"), "Home"
- #assert.equal result.at(0).get("title"), "Contact"
+ assert.equal result[2].get("title"), "About"
+ assert.equal result[1].get("title"), "Home"
+ assert.equal result[0].get("title"), "Contact"
it "Sorder by function", ->
a = create()
- result = a.findAll {likes: {$gt: 1}}, {sortBy: (model) -> model.get("title").charAt(2) }
+ result = a.query {likes: {$gt: 1}}, {sortBy: (model) -> model.get("title").charAt(2) }
assert.equal result.length, 3
- #assert.equal result[2].get("title"), "About"
- #assert.equal result.at(0).get("title"), "Home"
- #assert.equal result[1].get("title"), "Contact"
+ assert.equal result[2].get("title"), "About"
+ assert.equal result[0].get("title"), "Home"
+ assert.equal result[1].get("title"), "Contact"
it "cache", ->
a = create()
- result = a.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+ result = a.whereBy {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal result.length, 3
- result = a.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+ result = a.whereBy {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal result.length, 3
a.remove result.at(0)
- result = a.findAll {likes: {$gt: 1}}, {sortBy: (model) -> model.get("title").charAt(2) }
+ result = a.whereBy {likes: {$gt: 1}}, {sortBy: (model) -> model.get("title").charAt(2) }
assert.equal result.length, 2
- result = a.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+ result = a.whereBy {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal result.length, 3
- ###
+
it "cache with multiple collections", ->
a = create()
b = create()
b.remove b.at(0)
assert.equal b.length, 2
assert.equal a.length, 3
-
- a_result = a.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+
+
+ a_result = a.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal a_result.length, 3
- b_result = b.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+ b_result = b.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal b_result.length, 2
-
+
a.remove a_result[0]
b.remove b_result[0]
- a_result = a.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+ a_result = a.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal a_result.length, 3
assert.equal a.length, 2
- b_result = b.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+
+
+ b_result = b.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal b_result.length, 2
assert.equal b.length, 1
-
- a.reset_query_cache()
- a_result = a.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+
+ a.resetQueryCache()
+ a_result = a.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal a_result.length, 2
assert.equal a.length, 2
- b_result = b.findAll {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
+ b_result = b.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
assert.equal b_result.length, 2
assert.equal b.length, 1
- ###
+
it "null attribute with various operators", ->
a = create()
- result = a.findAll wrong_key: {$like: "test"}
+ result = a.whereBy wrong_key: {$like: "test"}
assert.equal result.length, 0
- result = a.findAll wrong_key: {$regex: /test/}
+ result = a.whereBy wrong_key: {$regex: /test/}
assert.equal result.length, 0
- result = a.findAll wrong_key: {$contains: "test"}
+ result = a.whereBy wrong_key: {$contains: "test"}
assert.equal result.length, 0
- result = a.findAll wrong_key: {$all: [12,23]}
+ result = a.whereBy wrong_key: {$all: [12,23]}
assert.equal result.length, 0
- result = a.findAll wrong_key: {$any: [12,23]}
+ result = a.whereBy wrong_key: {$any: [12,23]}
assert.equal result.length, 0
- result = a.findAll wrong_key: {$size: 10}
+ result = a.whereBy wrong_key: {$size: 10}
assert.equal result.length, 0
- result = a.findAll wrong_key: {$in: [12,23]}
+ result = a.whereBy wrong_key: {$in: [12,23]}
assert.equal result.length, 0
- result = a.findAll wrong_key: {$nin: [12,23]}
+ result = a.whereBy wrong_key: {$nin: [12,23]}
assert.equal result.length, 0
it "Where method", ->
a = create()
- result = a.where_by likes: $gt: 5
+ result = a.whereBy likes: $gt: 5
assert.equal result.length, 2
assert.equal result.models.length, result.length
- ###
+
it "$computed", ->
class testModel extends Backbone.Model
full_name: -> "#{@get 'first_name'} #{@get 'last_name'}"
@@ -354,17 +356,17 @@ describe "Backbone Query Tests", ->
last_name: "Smith"
c = new QueryCollection [a,b]
- result = c.findAll
+ result = c.query
full_name: $computed: "Dave Tonge"
assert.equal result.length, 1
assert.equal result[0].get("first_name"), "Dave"
- result = c.findAll
+ result = c.query
full_name: $computed: $likeI: "n sm"
assert.equal result.length, 1
assert.equal result[0].get("first_name"), "John"
- ###
+
it "$elemMatch", ->
a = new QueryCollection [
@@ -393,59 +395,60 @@ describe "Backbone Query Tests", ->
text_search = {$likeI: "love"}
- result = a.findAll $or:
+ result = a.query $or:
comments:
$elemMatch:
text: text_search
title: text_search
assert.equal result.length, 2
- result = a.findAll $or:
+ result = a.query $or:
comments:
$elemMatch:
text: /post/
assert.equal result.length, 1
- result = a.findAll $or:
+ result = a.query $or:
comments:
$elemMatch:
text: /post/
title: /about/i
assert.equal result.length, 2
- result = a.findAll $or:
+ result = a.query $or:
comments:
$elemMatch:
text: /really/
assert.equal result.length, 1
- result = b.findAll
+ result = b.query
foo:
$elemMatch:
shape:"square"
color:"purple"
assert.equal result.length, 1
- #assert.equal result[0].get("foo")[0].shape, "square"
- #assert.equal result[0].get("foo")[0].color, "purple"
- #assert.equal result[0].get("foo")[0].thick, false
+ assert.equal result[0].get("foo")[0].shape, "square"
+ assert.equal result[0].get("foo")[0].color, "purple"
+ assert.equal result[0].get("foo")[0].thick, false
+
- ###
it "$any and $all", ->
a = name: "test", tags1: ["red","yellow"], tags2: ["orange", "green", "red", "blue"]
b = name: "test1", tags1: ["purple","blue"], tags2: ["orange", "red", "blue"]
c = name: "test2", tags1: ["black","yellow"], tags2: ["green", "orange", "blue"]
d = name: "test3", tags1: ["red","yellow","blue"], tags2: ["green"]
e = new QueryCollection [a,b,c,d]
- result = e.findAll
+ result = e.query
tags1: $any: ["red","purple"] # should match a, b, d
tags2: $all: ["orange","green"] # should match a, c
assert.equal result.length, 1
assert.equal result[0].get("name"), "test"
- ###
+
+ # Test from RobW - https://github.com/Rob--W
it "Explicit $and combined with matching $or must return the correct number of items", ->
Col = new QueryCollection [
{equ:'ok', same: 'ok'},
@@ -458,6 +461,7 @@ describe "Backbone Query Tests", ->
same: 'ok' # Matches both items
assert.equal result.length, 2
+ # Test from RobW - https://github.com/Rob--W
it "Implicit $and consisting of non-matching subquery and $or must return empty list", ->
Col = new QueryCollection [
{equ:'ok', same: 'ok'},
@@ -468,4 +472,47 @@ describe "Backbone Query Tests", ->
$or:
same: 'ok' # Matches all items, but due to implicit $and, this subquery should not affect the result
assert.equal result.length, 0
+
+ it "Testing nested compound operators", ->
+ a = create()
+ result = a.whereBy
+ $and: