Skip to content
Browse files

Added a "$computed" operator that allows queries to be performed on c…

…omputed properties
  • Loading branch information...
1 parent 6fd8361 commit 618509eae7468c774bbfb52a74ae23e9c4841114 @davidgtonge committed
Showing with 183 additions and 30 deletions.
  1. +36 −0 README.md
  2. +43 −16 js/backbone-query.js
  3. +1 −1 js/backbone-query.min.js
  4. +23 −10 src/backbone-query.coffee
  5. +28 −1 test/backbone-query-test.coffee
  6. +52 −2 test/backbone-query-test.js
View
36 README.md
@@ -277,6 +277,42 @@ Posts.query({
All of the operators above can be performed on `$elemMatch` queries, e.g. `$all`, `$size` or `$lt`.
+### $computed
+This operator allows you to perform queries on computed properties. For example you may want to perform a query
+for a persons full name, even though the first and last name are stored separately in your db / model.
+For example
+
+```js
+testModel = Backbone.Model.extend({
+ full_name: function() {
+ return (this.get('first_name')) + " " + (this.get('last_name'));
+ }
+});
+
+a = new testModel({
+ first_name: "Dave",
+ last_name: "Tonge"
+});
+
+b = new testModel({
+ first_name: "John",
+ last_name: "Smith"
+});
+
+MyCollection = new QueryCollection([a, b]);
+
+MyCollection.query({
+ full_name: { $computed: "Dave Tonge" }
+});
+// Returns the model with the computed `full_name` equal to Dave Tonge
+
+MyCollection.query({
+ full_name: { $computed: { $likeI: "john smi" } }
+});
+// Any of the previous operators can be used (including elemMatch is required)
+```
+
+
Combined Queries
================
View
59 js/backbone-query.js
@@ -6,11 +6,11 @@ May be freely distributed according to MIT license.
*/
(function() {
- var 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,
+ var 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 = Array.prototype.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, query_param, type, value, _results;
+ var key, o, q, query_param, type, value, _results;
_results = [];
for (key in raw_query) {
query_param = raw_query[key];
@@ -25,10 +25,17 @@ May be freely distributed according to MIT license.
value = query_param[type];
if (test_query_value(type, value)) {
o.type = type;
- if (type === "$elemMatch") {
- o.value = parse_query(value);
- } else {
- o.value = value;
+ switch (type) {
+ case "$elemMatch":
+ o.value = parse_query(value);
+ break;
+ case "$computed":
+ q = {};
+ q[key] = value;
+ o.value = parse_query(q);
+ break;
+ default:
+ o.value = value;
}
}
}
@@ -86,7 +93,7 @@ May be freely distributed according to MIT license.
}
};
- perform_query = function(type, value, attr, model) {
+ perform_query = function(type, value, attr, model, key) {
switch (type) {
case "$equal":
if (_(attr).isArray()) {
@@ -137,7 +144,9 @@ May be freely distributed according to MIT license.
case "$cb":
return value.call(model, attr);
case "$elemMatch":
- return (iterator(attr, value, false, filter, true)).length > 0;
+ return iterator(attr, value, false, detect, "elemMatch");
+ case "$computed":
+ return iterator([model], value, false, detect, "computed");
default:
return false;
}
@@ -151,9 +160,18 @@ May be freely distributed according to MIT license.
var attr, q, test, _i, _len;
for (_i = 0, _len = parsed_query.length; _i < _len; _i++) {
q = parsed_query[_i];
- attr = subQuery ? model[q.key] : model.get(q.key);
+ 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);
+ if (test) test = perform_query(q.type, q.value, attr, model, q.key);
if (andOr === test) return andOr;
}
return !andOr;
@@ -161,25 +179,34 @@ May be freely distributed according to MIT license.
};
filter = function(array, test) {
- var index, val, _i, _len, _results;
+ var val, _i, _len, _results;
_results = [];
- for (index = _i = 0, _len = array.length; _i < _len; index = ++_i) {
- val = array[index];
+ 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 index, val, _i, _len, _results;
+ var val, _i, _len, _results;
_results = [];
- for (index = _i = 0, _len = array.length; _i < _len; index = ++_i) {
- val = array[index];
+ 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);
View
2 js/backbone-query.min.js
@@ -1 +1 @@
-((function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n=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};g=function(a){var b,c,d,e,f,h;h=[];for(b in a){d=a[b],c={key:b};if(_.isRegExp(d))c.type="$regex",c.value=d;else if(_(d).isObject()&&!_(d).isArray())for(e in d)f=d[e],m(e,f)&&(c.type=e,e==="$elemMatch"?c.value=g(f):c.value=f);else c.type="$equal",c.value=d;c.type==="$equal"&&_(c.value).isObject()&&(c.type="$oEqual"),h.push(c)}return h},m=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}},l=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;default:return!0}},h=function(b,c,d,f){switch(b){case"$equal":return _(d).isArray()?n.call(d,c)>=0:d===c;case"$oEqual":return _(d).isEqual(c);case"$contains":return n.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 n.call(c,d)>=0;case"$nin":return n.call(c,d)<0;case"$all":return _(d).all(function(a){return n.call(c,a)>=0});case"$any":return _(d).any(function(a){return n.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(f,d);case"$elemMatch":return e(d,c,!1,a,!0).length>0;default:return!1}},e=function(a,b,c,d,e){var f;return e==null&&(e=!1),f=e?b:g(b),d(a,function(a){var b,d,g,i,j;for(i=0,j=f.length;i<j;i++){d=f[i],b=e?a[d.key]:a.get(d.key),g=l(d.type,b),g&&(g=h(d.type,d.value,b,a));if(c===g)return c}return!c})},a=function(a,b){var c,d,e,f,g;g=[];for(c=e=0,f=a.length;e<f;c=++e)d=a[c],b(d)&&g.push(d);return g},j=function(a,b){var c,d,e,f,g;g=[];for(c=e=0,f=a.length;e<f;c=++e)d=a[c],b(d)||g.push(d);return g},i={$and:function(b,c){return e(b,c,!1,a)},$or:function(b,c){return e(b,c,!0,a)},$nor:function(a,b){return e(a,b,!0,j)},$not:function(a,b){return e(a,b,!1,j)}},b=function(a,b,c){var e,f,g,h;return g=JSON.stringify(b),e=(h=a._query_cache)!=null?h:a._query_cache={},f=e[g],f||(f=d(a,b,c),e[g]=f),f},c=function(a,b){var c,d,e;return c=_.intersection(["$and","$not","$or","$nor"],_(b).keys()),d=a.models,c.length===0?i.$and(d,b):(e=function(a,c){return i[c](a,b[c])},_.reduce(c,e,d))},d=function(a,b,d){var e;return e=c(a,b),d.sortBy&&(e=k(e,d)),e},k=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},f=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,c){var e;return c==null&&(c={}),c.cache?e=b(this,a,c):e=d(this,a,c),c.limit&&(e=f(e,c)),e},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)
+((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":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;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 _(d).all(function(a){return o.call(c,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"$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
33 src/backbone-query.coffee
@@ -19,10 +19,15 @@ parse_query = (raw_query) ->
# Before adding the query, its value is checked to make sure it is the right type
if test_query_value type, value
o.type = type
- if type is "$elemMatch"
- o.value = parse_query value
- else
- o.value = value
+ switch type
+ when "$elemMatch"
+ o.value = parse_query value
+ when "$computed"
+ q = {}
+ q[key] = value
+ o.value = parse_query 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"
@@ -53,7 +58,7 @@ test_model_attribute = (type, value) ->
else true
# Perform the actual query logic for each query and each model/attribute
-perform_query = (type, value, attr, model) ->
+perform_query = (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
@@ -76,7 +81,8 @@ perform_query = (type, value, attr, model) ->
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, filter, true).length > 0
+ when "$elemMatch" then iterator attr, value, false, detect, "elemMatch"
+ when "$computed" then iterator [model], value, false, detect, "computed"
else false
@@ -88,11 +94,14 @@ iterator = (models, query, andOr, filterReject, subQuery = false) ->
# For each model in the collection, iterate through the supplied queries
for q in parsed_query
# Retrieve the attribute value from the model
- attr = if subQuery then model[q.key] else model.get(q.key)
+ 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 = test_model_attribute(q.type, attr)
# If the attribute test is true, perform the query
- if test then test = perform_query q.type, q.value, attr, model
+ if test then test = perform_query 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
@@ -103,8 +112,12 @@ iterator = (models, query, andOr, filterReject, subQuery = false) ->
# 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, index in array when test val)
-reject = (array, test) -> (val for val, index in array when not test val)
+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
# An object with or, and, nor and not methods
process_query =
View
29 test/backbone-query-test.coffee
@@ -1,7 +1,10 @@
+
if typeof require isnt "undefined"
{QueryCollection} = require "../js/backbone-query.js"
+ Backbone = require "backbone"
else
- QueryCollection = Backbone.QueryCollection
+ QueryCollection = window.Backbone.QueryCollection
+ Backbone = window.Backbone
# Helper functions that turn Qunit tests into nodeunit tests
equals = []
@@ -345,6 +348,30 @@ test "Where method", ->
equal result.models.length, result.length
+test "$computed", ->
+ class testModel extends Backbone.Model
+ full_name: -> "#{@get 'first_name'} #{@get 'last_name'}"
+
+ a = new testModel
+ first_name: "Dave"
+ last_name: "Tonge"
+ b = new testModel
+ first_name: "John"
+ last_name: "Smith"
+ c = new QueryCollection [a,b]
+
+ result = c.query
+ full_name: $computed: "Dave Tonge"
+
+ equal result.length, 1
+ equal result[0].get("first_name"), "Dave"
+
+ result = c.query
+ full_name: $computed: $likeI: "n sm"
+ equal result.length, 1
+ equal result[0].get("first_name"), "John"
+
+
test "$elemMatch", ->
a = new QueryCollection [
{title: "Home", comments:[
View
54 test/backbone-query-test.js
@@ -1,10 +1,14 @@
(function() {
- var QueryCollection, create, equals;
+ var Backbone, QueryCollection, create, equals,
+ __hasProp = Object.prototype.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
if (typeof require !== "undefined") {
QueryCollection = require("../js/backbone-query.js").QueryCollection;
+ Backbone = require("backbone");
} else {
- QueryCollection = Backbone.QueryCollection;
+ QueryCollection = window.Backbone.QueryCollection;
+ Backbone = window.Backbone;
}
equals = [];
@@ -793,6 +797,52 @@
return equal(result.models.length, result.length);
});
+ test("$computed", function() {
+ var a, b, c, result, testModel;
+ testModel = (function(_super) {
+
+ __extends(testModel, _super);
+
+ testModel.name = 'testModel';
+
+ function testModel() {
+ return testModel.__super__.constructor.apply(this, arguments);
+ }
+
+ testModel.prototype.full_name = function() {
+ return "" + (this.get('first_name')) + " " + (this.get('last_name'));
+ };
+
+ return testModel;
+
+ })(Backbone.Model);
+ a = new testModel({
+ first_name: "Dave",
+ last_name: "Tonge"
+ });
+ b = new testModel({
+ first_name: "John",
+ last_name: "Smith"
+ });
+ c = new QueryCollection([a, b]);
+ result = c.query({
+ full_name: {
+ $computed: "Dave Tonge"
+ }
+ });
+ equal(result.length, 1);
+ equal(result[0].get("first_name"), "Dave");
+ result = c.query({
+ full_name: {
+ $computed: {
+ $likeI: "n sm"
+ }
+ }
+ });
+ equal(result.length, 1);
+ return equal(result[0].get("first_name"), "John");
+ });
+
test("$elemMatch", function() {
var a, b, result, text_search;
a = new QueryCollection([

0 comments on commit 618509e

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