Skip to content

Commit

Permalink
functional refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
davidgtonge committed Jun 10, 2013
1 parent e4d19e2 commit e17db71
Show file tree
Hide file tree
Showing 7 changed files with 509 additions and 69 deletions.
131 changes: 131 additions & 0 deletions backbone-query.coffee
@@ -0,0 +1,131 @@
###
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')
require('underscore-query')
Backbone = require('backbone')

# 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

# The default Backbone Collection is extended with our query methods
Backbone.QueryCollection = Backbone.Collection.extend

# Main Query method
query: (params, options) ->
if params
# If a query is provided, then the query is run immediately
models = _.query @models, params, "get"
if options
# Caching is depreciated
if options.cache then throw new Error "Query cache is depreciated in version 0.3.0. Use live collections."
# Optional sorting performed
if options.sortBy then models = sortModels(models, options)
# Options paging performed
if options.limit then models = pageModels(models, options)
# Return the results
models

else
# If no query is provided then we return a query builder object
_.query.build @models, "get"


# Helper method to return a new collection with the filtered models
whereBy: -> throw new Error "Whereby is depreciated in version 0.3.0, please use live collections or chain"

# This method assists in creating live collections that remain updated
setFilter: (parent, query) ->
# Need a reference to the parent in case the filter is updated
@_query_parent = parent

# A checking function is created to test models against
# The function is added to the collection instance so that it can later be updated
if query
@_query = _.query.tester(query, "get")
# Any existing models on the parent are filtered and added to this collection
@set _.query(parent.models, @_query, "get")

else
# No models to be added by default until filter is set
@_query = -> false
# To allow chaining form
# col.setFilter(parent).add(a,b).not(c,d).set()
builder = _.query().getter("get")
builder.set = =>
@_query = builder.tester()
# In case the filter is set later we need to ensure any existing models are updated
@set _.query(parent.models, @_query, "get")

# Listeners are added to the parent collection
@listenTo parent,
# Any model added to the parent, will be added to this collection if it passes the test
add: (model) -> if @._query(model) then @add(model)
# Any model removed from the parent will be removed from this collection
remove: @remove
# Any model that is changed on the parent will be re-tested
change: (model) ->
if @_query(model) then @add(model) else @remove(model)


updateFilter: (query) ->
throw new Error "setFiler must be called before updateFilter" unless @_query
if query
@_query = _.query.tester(query, "get")
@set _.query(@_parent.models, @_query, "get")
else
# To allow the form col.updateFilter().and(a,v).set()
builder = _.query().getter("get")
builder.set = =>
@_query = builder.tester()
@set _.query(@_parent.models, @_query, "get")


# Helper method to return the first filtered model
findOne: (query) -> _.findOne @models, query, "get"

resetQueryCache: -> throw new Error "Query cache is depreciated in version 0.3.0"

# 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

138 changes: 75 additions & 63 deletions lib/underscore-query.js
Expand Up @@ -9,9 +9,9 @@ This is small library that provides a query api for JavaScript arrays similar to
The aim of the project is to provide a simple, well tested, way of filtering data in JavaScript.
*/

var buildQuery, iterator, key, makeTest, parseQuery, parseSubQuery, performQuery, runQuery, testModelAttribute, testQueryValue, utils, _, _i, _len, _ref,
__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; };
var buildQuery, iterator, key, makeTest, parseParamType, parseQuery, parseSubQuery, performQuery, runQuery, testModelAttribute, testQueryValue, utils, _, _i, _len, _ref,
__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; },
__hasProp = {}.hasOwnProperty;

_ = require('underscore');

Expand Down Expand Up @@ -64,8 +64,74 @@ utils.compoundKeys = ["$and", "$not", "$or", "$nor"];

utils.seperator = ".";

utils.$computedGetter = function(model, key) {
return typeof model[key] === "function" ? model[key]() : void 0;
};

parseParamType = function(query) {
var o, paramType, queryParam, type, value;

key = utils.keys(query)[0];
queryParam = query[key];
o = {
key: key
};
if (key.indexOf(utils.seperator) !== -1) {
o.getter = utils.getNested(key.split(utils.seperator));
}
paramType = utils.getType(queryParam);
switch (paramType) {
case "RegExp":
case "Date":
o.type = "$" + (paramType.toLowerCase());
o.value = queryParam;
break;
case "Object":
if (__indexOf.call(utils.compoundKeys, key) >= 0) {
o.type = key;
o.value = parseSubQuery(queryParam);
o.key = null;
} else {
for (type in queryParam) {
value = queryParam[type];
if (testQueryValue(type, value)) {
o.type = type;
switch (type) {
case "$elemMatch":
o.value = parseQuery(value);
break;
case "$endsWith":
o.value = utils.reverseString(value);
break;
case "$likeI":
case "$startsWith":
o.value = value.toLowerCase();
break;
case "$computed":
o = parseParamType(utils.makeObj(key, value));
o.getter = utils.$computedGetter;
break;
default:
o.value = value;
}
} else {
throw new Error("Query value doesn't match query type: " + type + ": " + value);
}
}
}
break;
default:
o.type = "$equal";
o.value = queryParam;
}
if ((o.type === "$equal") && (paramType === "Object" || paramType === "Array")) {
o.type = "$deepEqual";
}
return o;
};

parseSubQuery = function(rawQuery) {
var o, paramType, query, queryArray, queryParam, type, val, value, _j, _len1, _results;
var query, queryArray, val, _j, _len1, _results;

if (utils.isArray(rawQuery)) {
queryArray = rawQuery;
Expand All @@ -85,61 +151,7 @@ parseSubQuery = function(rawQuery) {
_results = [];
for (_j = 0, _len1 = queryArray.length; _j < _len1; _j++) {
query = queryArray[_j];
for (key in query) {
if (!__hasProp.call(query, key)) continue;
queryParam = query[key];
o = {
key: key
};
if (key.indexOf(utils.seperator) !== -1) {
o.getter = utils.getNested(key.split(utils.seperator));
}
paramType = utils.getType(queryParam);
switch (paramType) {
case "RegExp":
case "Date":
o.type = "$" + (paramType.toLowerCase());
o.value = queryParam;
break;
case "Object":
if (__indexOf.call(utils.compoundKeys, key) >= 0) {
o.type = key;
o.value = parseSubQuery(queryParam);
o.key = null;
} else {
for (type in queryParam) {
value = queryParam[type];
if (testQueryValue(type, value)) {
o.type = type;
switch (type) {
case "$elemMatch":
o.value = parseQuery(value);
break;
case "$endsWith":
o.value = utils.reverseString(value);
break;
case "$likeI":
case "$startsWith":
o.value = value.toLowerCase();
break;
default:
o.value = value;
}
} else {
throw new Error("Query value doesn't match query type: " + type + ": " + value);
}
}
}
break;
default:
o.type = "$equal";
o.value = queryParam;
}
if ((o.type === "$equal") && (paramType === "Object" || paramType === "Array")) {
o.type = "$deepEqual";
}
}
_results.push(o);
_results.push(parseParamType(query));
}
return _results;
};
Expand Down Expand Up @@ -198,7 +210,7 @@ testModelAttribute = function(queryType, value) {
}
};

performQuery = function(type, value, attr, model) {
performQuery = function(type, value, attr, model, getter) {
switch (type) {
case "$equal":
if (utils.isArray(attr)) {
Expand Down Expand Up @@ -265,7 +277,7 @@ performQuery = function(type, value, attr, model) {
case "$or":
case "$nor":
case "$not":
return iterator([model], value, type).length === 1;
return iterator([model], value, type, getter).length === 1;
default:
return false;
}
Expand All @@ -282,15 +294,15 @@ iterator = function(models, query, type, getter) {
for (_j = 0, _len1 = query.length; _j < _len1; _j++) {
q = query[_j];
if (q.getter) {
attr = q.getter(model);
attr = q.getter(model, q.key);
} else if (getter) {
attr = getter(model, q.key);
} else {
attr = model[q.key];
}
test = testModelAttribute(q.type, attr);
if (test) {
test = performQuery(q.type, q.value, attr, model);
test = performQuery(q.type, q.value, attr, model, getter);
}
if (andOr === test) {
return andOr;
Expand Down

0 comments on commit e17db71

Please sign in to comment.