Skip to content

Commit

Permalink
Added a "$computed" operator that allows queries to be performed on c…
Browse files Browse the repository at this point in the history
…omputed properties
  • Loading branch information
davidgtonge committed Mar 2, 2012
1 parent 6fd8361 commit 618509e
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 30 deletions.
36 changes: 36 additions & 0 deletions README.md
Expand Up @@ -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
================

Expand Down
59 changes: 43 additions & 16 deletions js/backbone-query.js
Expand Up @@ -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];
Expand All @@ -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;
}
}
}
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -151,35 +160,53 @@ 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;
});
};

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);
Expand Down
2 changes: 1 addition & 1 deletion js/backbone-query.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 23 additions & 10 deletions src/backbone-query.coffee
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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


Expand All @@ -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
Expand All @@ -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 =
Expand Down
29 changes: 28 additions & 1 deletion 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 = []
Expand Down Expand Up @@ -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:[
Expand Down
54 changes: 52 additions & 2 deletions test/backbone-query-test.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 618509e

Please sign in to comment.