├── .gitignore
├── .npmignore
├── .travis.yml
├── Cakefile
├── LICENSE
├── README.md
├── backbone-query.js
├── backbone-query.min.js
├── package.json
├── src
└── backbone-query.coffee
└── test
├── bq-test.coffee
└── mocha.opts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Idea IDE files
2 | .idea/
3 | node_modules/
4 | backbone_query.iml
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .git/
3 | node_modules/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.6
4 |
5 | before_script:
6 | - "npm install"
7 |
8 | script:
9 | - "node_modules/.bin/mocha"
10 |
--------------------------------------------------------------------------------
/Cakefile:
--------------------------------------------------------------------------------
1 | # ** Cakefile Template ** is a Template for a common Cakefile that you may use in a coffeescript nodejs project.
2 | #
3 | # It comes baked in with 5 tasks:
4 | #
5 | # * build - compiles your src directory to your lib directory
6 | # * watch - watches any changes in your src directory and automatically compiles to the lib directory
7 | # * test - runs mocha test framework, you can edit this task to use your favorite test framework
8 | # * docs - generates annotated documentation using docco
9 | # * clean - clean generated .js files
10 | files = [
11 | '.'
12 | 'src'
13 | ]
14 |
15 | fs = require 'fs'
16 | {print} = require 'util'
17 | {spawn, exec} = require 'child_process'
18 |
19 | try
20 | which = require('which').sync
21 | catch err
22 | if process.platform.match(/^win/)?
23 | console.log 'WARNING: the which module is required for windows\ntry: npm install which'
24 | which = null
25 |
26 | # ANSI Terminal Colors
27 | bold = '\x1b[0;1m'
28 | green = '\x1b[0;32m'
29 | reset = '\x1b[0m'
30 | red = '\x1b[0;31m'
31 |
32 | # Cakefile Tasks
33 | #
34 | # ## *docs*
35 | #
36 | # Generate Annotated Documentation
37 | #
38 | # Usage
39 | #
40 | # ```
41 | # cake docs
42 | # ```
43 | task 'docs', 'generate documentation', -> docco()
44 |
45 | # ## *build*
46 | #
47 | # Builds Source
48 | #
49 | # Usage
50 | #
51 | # ```
52 | # cake build
53 | # ```
54 | task 'build', 'compile source', -> build -> log ":)", green
55 |
56 | # ## *watch*
57 | #
58 | # Builds your source whenever it changes
59 | #
60 | # Usage
61 | #
62 | # ```
63 | # cake watch
64 | # ```
65 | task 'watch', 'compile and watch', -> build true, -> log ":-)", green
66 |
67 | # ## *test*
68 | #
69 | # Runs your test suite.
70 | #
71 | # Usage
72 | #
73 | # ```
74 | # cake test
75 | # ```
76 | task 'test', 'run tests', -> build -> mocha -> log ":)", green
77 |
78 | # ## *clean*
79 | #
80 | # Cleans up generated js files
81 | #
82 | # Usage
83 | #
84 | # ```
85 | # cake clean
86 | # ```
87 | task 'clean', 'clean generated files', -> clean -> log ";)", green
88 |
89 |
90 | # Internal Functions
91 | #
92 | # ## *walk*
93 | #
94 | # **given** string as dir which represents a directory in relation to local directory
95 | # **and** callback as done in the form of (err, results)
96 | # **then** recurse through directory returning an array of files
97 | #
98 | # Examples
99 | #
100 | # ``` coffeescript
101 | # walk 'src', (err, results) -> console.log results
102 | # ```
103 | walk = (dir, done) ->
104 | results = []
105 | fs.readdir dir, (err, list) ->
106 | return done(err, []) if err
107 | pending = list.length
108 | return done(null, results) unless pending
109 | for name in list
110 | file = "#{dir}/#{name}"
111 | try
112 | stat = fs.statSync file
113 | catch err
114 | stat = null
115 | if stat?.isDirectory()
116 | walk file, (err, res) ->
117 | results.push name for name in res
118 | done(null, results) unless --pending
119 | else
120 | results.push file
121 | done(null, results) unless --pending
122 |
123 | # ## *log*
124 | #
125 | # **given** string as a message
126 | # **and** string as a color
127 | # **and** optional string as an explanation
128 | # **then** builds a statement and logs to console.
129 | #
130 | log = (message, color, explanation) -> console.log color + message + reset + ' ' + (explanation or '')
131 |
132 | # ## *launch*
133 | #
134 | # **given** string as a cmd
135 | # **and** optional array and option flags
136 | # **and** optional callback
137 | # **then** spawn cmd with options
138 | # **and** pipe to process stdout and stderr respectively
139 | # **and** on child process exit emit callback if set and status is 0
140 | launch = (cmd, options=[], callback) ->
141 | cmd = which(cmd) if which
142 | app = spawn cmd, options
143 | app.stdout.pipe(process.stdout)
144 | app.stderr.pipe(process.stderr)
145 | app.on 'exit', (status) -> callback?() if status is 0
146 |
147 | # ## *build*
148 | #
149 | # **given** optional boolean as watch
150 | # **and** optional function as callback
151 | # **then** invoke launch passing coffee command
152 | # **and** defaulted options to compile src to lib
153 | build = (watch, callback) ->
154 | if typeof watch is 'function'
155 | callback = watch
156 | watch = false
157 |
158 | options = ['-c', '-b', '-o' ]
159 | options = options.concat files
160 | options.unshift '-w' if watch
161 | launch 'coffee', options, callback
162 |
163 | # ## *unlinkIfCoffeeFile*
164 | #
165 | # **given** string as file
166 | # **and** file ends in '.coffee'
167 | # **then** convert '.coffee' to '.js'
168 | # **and** remove the result
169 | unlinkIfCoffeeFile = (file) ->
170 | if file.match /\.coffee$/
171 | fs.unlink file.replace(/\.coffee$/, '.js')
172 | true
173 | else false
174 |
175 | # ## *clean*
176 | #
177 | # **given** optional function as callback
178 | # **then** loop through files variable
179 | # **and** call unlinkIfCoffeeFile on each
180 | clean = (callback) ->
181 | try
182 | for file in files
183 | unless unlinkIfCoffeeFile file
184 | walk file, (err, results) ->
185 | for f in results
186 | unlinkIfCoffeeFile f
187 |
188 | callback?()
189 | catch err
190 |
191 | # ## *moduleExists*
192 | #
193 | # **given** name for module
194 | # **when** trying to require module
195 | # **and** not found
196 | # **then* print not found message with install helper in red
197 | # **and* return false if not found
198 | moduleExists = (name) ->
199 | try
200 | require name
201 | catch err
202 | log "#{name} required: npm install #{name}", red
203 | false
204 |
205 |
206 | # ## *mocha*
207 | #
208 | # **given** optional array of option flags
209 | # **and** optional function as callback
210 | # **then** invoke launch passing mocha command
211 | mocha = (options, callback) ->
212 | #if moduleExists('mocha')
213 | if typeof options is 'function'
214 | callback = options
215 | options = []
216 |
217 | launch 'mocha', options, callback
218 |
219 | # ## *docco*
220 | #
221 | # **given** optional function as callback
222 | # **then** invoke launch passing docco command
223 | docco = (callback) ->
224 | #if moduleExists('docco')
225 | walk 'src', (err, files) -> launch 'docco', files, callback
226 |
227 | task 'uglify', 'Minify and obfuscate', ->
228 | uglify = require 'uglify-js'
229 | jsp = uglify.parser
230 | pro = uglify.uglify
231 |
232 | contents = fs.readFileSync "backbone-query.js", 'utf8'
233 |
234 | ast = jsp.parse contents # parse code and get the initial AST
235 | ast = pro.ast_mangle ast # get a new AST with mangled names
236 | ast = pro.ast_squeeze ast # get an AST with compression optimizations
237 | final_code = pro.gen_code ast # compressed code here
238 |
239 | fs.writeFile 'backbone-query.min.js', final_code
240 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Dave Tonge
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | backbone-query
2 | ===================
3 |
4 | [](http://travis-ci.org/davidgtonge/backbone_query)
5 |
6 | A lightweight (3KB minified) utility for Backbone projects, that works in the Browser and on the Server.
7 | Adds the ability to search for models with a Query API similar to
8 | [MongoDB](http://www.mongodb.org/display/DOCS/Advanced+Queries)
9 | Please report any bugs, feature requests in the issue tracker.
10 | Pull requests are welcome!
11 |
12 | Compatible with Backbone 0.5 to 0.99
13 |
14 | Usage
15 | =====
16 |
17 | #### Client Side Installation:
18 | To install, include the `js/backbone-query.min.js` file in your HTML page, after Backbone and it's dependencies.
19 | Then extend your collections from Backbone.QueryCollection rather than from Backbone.Collection.
20 |
21 | Backbone Query is also available via [Jam](http://jamjs.org/). Jam is a package manager for
22 | browser js packages and uses require.js. This is the recommended method of you want to use
23 | this library with require.js. To install, simply run `jam install backbone-query`.
24 |
25 | #### Server side (node.js) installation
26 | You can install with NPM: `npm install backbone-query`
27 | Then simply require in your project: `QueryCollection = require("backbone-query").QueryCollection`
28 |
29 |
30 | Your collections will now have two new methods: `query` and `whereBy`. Both methods accept 2 arguments -
31 | a query object and an options object. The `query` method returns an array of models, but the `whereBy` method
32 | returns a new collection and is therefore useful where you would like to chain multiple collection
33 | methods / whereBy queries (thanks to [Cezary Wojtkowski](https://github.com/cezary) ).
34 |
35 | The library also supports nested compound queries and is AMD compatible (thanks to [Rob W](https://github.com/Rob--W) ).
36 |
37 | The following are some basic examples:
38 |
39 | ```js
40 | MyCollection.query({
41 | featured:true,
42 | likes: {$gt:10}
43 | });
44 | // Returns all models where the featured attribute is true and there are
45 | // more than 10 likes
46 |
47 | MyCollection.query(
48 | {tags: { $any: ["coffeescript", "backbone", "mvc"]}},
49 | {sortBy: "likes", order: "desc", limit:10, page:2, cache:true}
50 | );
51 | // Finds models that have either "coffeescript", "backbone", "mvc" in their "tags" attribute
52 | // Sorts these models by the "likes" attribute in descending order
53 | // Caches the results and returns only 10 models, starting from the 11th model (page 2)
54 |
55 | MyCollection.query({
56 | // Models must match all these queries
57 | $and:{
58 | title: {$like: "news"}, // Title attribute contains the string "news"
59 | likes: {$gt: 10}
60 | }, // Likes attribute is greater than 10
61 |
62 | // Models must match one of these queries
63 | $or:{
64 | featured: true, // Featured attribute is true
65 | category:{$in:["code","programming","javascript"]}
66 | }
67 | //Category attribute is either "code", "programming", or "javascript"
68 | });
69 | ```
70 |
71 | Or if CoffeeScript is your thing (the source is written in CoffeeScript), try this:
72 |
73 | ```coffeescript
74 | MyCollection.query
75 | $and:
76 | likes: $lt: 15
77 | $or:
78 | content: $like: "news"
79 | featured: $exists: true
80 | $not:
81 | colors: $contains: "yellow"
82 | ```
83 |
84 | Another CoffeeScript example, this time using `whereBy` rather than `query`
85 |
86 | ```coffeescript
87 | query =
88 | $likes: $lt: 10
89 | $downloads: $gt: 20
90 |
91 | MyCollection.whereBy(query).my_custom_collection_method()
92 | ```
93 |
94 |
95 | Query API
96 | ===
97 |
98 | ### $equal
99 | Performs a strict equality test using `===`. If no operator is provided and the query value isn't a regex then `$equal` is assumed.
100 |
101 | If the attribute in the model is an array then the query value is searched for in the array in the same way as `$contains`
102 |
103 | If the query value is an object (including array) then a deep comparison is performed using underscores `_.isEqual`
104 |
105 | ```javascript
106 | MyCollection.query({ title:"Test" });
107 | // Returns all models which have a "title" attribute of "Test"
108 |
109 | MyCollection.query({ title: {$equal:"Test"} }); // Same as above
110 |
111 | MyCollection.query({ colors: "red" });
112 | // Returns models which contain the value "red" in a "colors" attribute that is an array.
113 |
114 | MyCollection.query ({ colors: ["red", "yellow"] });
115 | // Returns models which contain a colors attribute with the array ["red", "yellow"]
116 | ```
117 |
118 | ### $contains
119 | Assumes that the model property is an array and searches for the query value in the array
120 |
121 | ```js
122 | MyCollection.query({ colors: {$contains: "red"} });
123 | // Returns models which contain the value "red" in a "colors" attribute that is an array.
124 | // e.g. a model with this attribute colors:["red","yellow","blue"] would be returned
125 | ```
126 |
127 | ### $ne
128 | "Not equal", the opposite of $equal, returns all models which don't have the query value
129 |
130 | ```js
131 | MyCollection.query({ title: {$ne:"Test"} });
132 | // Returns all models which don't have a "title" attribute of "Test"
133 | ```
134 |
135 | ### $lt, $lte, $gt, $gte
136 | These conditional operators can be used for greater than and less than comparisons in queries
137 |
138 | ```js
139 | MyCollection.query({ likes: {$lt:10} });
140 | // Returns all models which have a "likes" attribute of less than 10
141 | MyCollection.query({ likes: {$lte:10} });
142 | // Returns all models which have a "likes" attribute of less than or equal to 10
143 | MyCollection.query({ likes: {$gt:10} });
144 | // Returns all models which have a "likes" attribute of greater than 10
145 | MyCollection.query({ likes: {$gte:10} });
146 | // Returns all models which have a "likes" attribute of greater than or equal to 10
147 | ```
148 |
149 | ### $between
150 | To check if a value is in-between 2 query values use the $between operator and supply an array with the min and max value
151 |
152 | ```js
153 | MyCollection.query({ likes: {$between:[5,15] } });
154 | // Returns all models which have a "likes" attribute of greater than 5 and less then 15
155 | ```
156 |
157 | ### $in
158 | An array of possible values can be supplied using $in, a model will be returned if any of the supplied values is matched
159 |
160 | ```js
161 | MyCollection.query({ title: {$in:["About", "Home", "Contact"] } });
162 | // Returns all models which have a title attribute of either "About", "Home", or "Contact"
163 | ```
164 |
165 | ### $nin
166 | "Not in", the opposite of $in. A model will be returned if none of the supplied values is matched
167 |
168 | ```js
169 | MyCollection.query({ title: {$nin:["About", "Home", "Contact"] } });
170 | // Returns all models which don't have a title attribute of either
171 | // "About", "Home", or "Contact"
172 | ```
173 |
174 | ### $all
175 | Assumes the model property is an array and only returns models where all supplied values are matched.
176 |
177 | ```js
178 | MyCollection.query({ colors: {$all:["red", "yellow"] } });
179 | // Returns all models which have "red" and "yellow" in their colors attribute.
180 | // A model with the attribute colors:["red","yellow","blue"] would be returned
181 | // But a model with the attribute colors:["red","blue"] would not be returned
182 | ```
183 |
184 | ### $any
185 | Assumes the model property is an array and returns models where any of the supplied values are matched.
186 |
187 | ```js
188 | MyCollection.query({ colors: {$any:["red", "yellow"] } });
189 | // Returns models which have either "red" or "yellow" in their colors attribute.
190 | ```
191 |
192 | ### $size
193 | Assumes the model property has a length (i.e. is either an array or a string).
194 | Only returns models the model property's length matches the supplied values
195 |
196 | ```js
197 | MyCollection.query({ colors: {$size:2 } });
198 | // Returns all models which 2 values in the colors attribute
199 | ```
200 |
201 | ### $exists or $has
202 | Checks for the existence of an attribute. Can be supplied either true or false.
203 |
204 | ```js
205 | MyCollection.query({ title: {$exists: true } });
206 | // Returns all models which have a "title" attribute
207 | MyCollection.query({ title: {$has: false } });
208 | // Returns all models which don't have a "title" attribute
209 | ```
210 |
211 | ### $like
212 | Assumes the model attribute is a string and checks if the supplied query value is a substring of the property.
213 | Uses indexOf rather than regex for performance reasons
214 |
215 | ```js
216 | MyCollection.query({ title: {$like: "Test" } });
217 | //Returns all models which have a "title" attribute that
218 | //contains the string "Test", e.g. "Testing", "Tests", "Test", etc.
219 | ```
220 |
221 | ### $likeI
222 | The same as above but performs a case insensitive search using indexOf and toLowerCase (still faster than Regex)
223 |
224 | ```js
225 | MyCollection.query({ title: {$likeI: "Test" } });
226 | //Returns all models which have a "title" attribute that
227 | //contains the string "Test", "test", "tEst","tesT", etc.
228 | ```
229 |
230 | ### $regex
231 | Checks if the model attribute matches the supplied regular expression. The regex query can be supplied without the `$regex` keyword
232 |
233 | ```js
234 | MyCollection.query({ content: {$regex: /coffeescript/gi } });
235 | // Checks for a regex match in the content attribute
236 | MyCollection.query({ content: /coffeescript/gi });
237 | // Same as above
238 | ```
239 |
240 | ### $cb
241 | A callback function can be supplied as a test. The callback will receive the attribute and should return either true or false.
242 | `this` will be set to the current model, this can help with tests against computed properties
243 |
244 | ```js
245 | MyCollection.query({ title: {$cb: function(attr){ return attr.charAt(0) === "c";}} });
246 | // Returns all models that have a title attribute that starts with "c"
247 |
248 | MyCollection.query({ computed_test: {$cb: function(){ return this.computed_property() > 10;}} });
249 | // Returns all models where the computed_property method returns a value greater than 10.
250 | ```
251 |
252 | For callbacks that use `this` rather than the model attribute, the key name supplied is arbitrary and has no
253 | effect on the results. If the only test you were performing was like the above test it would make more sense
254 | to simply use `MyCollection.filter`. However if you are performing other tests or are using the paging / sorting /
255 | caching options of backbone query, then this functionality is useful.
256 |
257 | ### $elemMatch
258 | This operator allows you to perform queries in nested arrays similar to [MongoDB](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24elemMatch)
259 | For example you may have a collection of models in with this kind of data stucture:
260 |
261 | ```js
262 | var Posts = new QueryCollection([
263 | {title: "Home", comments:[
264 | {text:"I like this post"},
265 | {text:"I love this post"},
266 | {text:"I hate this post"}
267 | ]},
268 | {title: "About", comments:[
269 | {text:"I like this page"},
270 | {text:"I love this page"},
271 | {text:"I really like this page"}
272 | ]}
273 | ]);
274 | ```
275 | To search for posts which have the text "really" in any of the comments you could search like this:
276 |
277 | ```js
278 | Posts.query({
279 | comments: {
280 | $elemMatch: {
281 | text: /really/i
282 | }
283 | }
284 | });
285 | ```
286 |
287 | All of the operators above can be performed on `$elemMatch` queries, e.g. `$all`, `$size` or `$lt`.
288 | `$elemMatch` queries also accept compound operators, for example this query searches for all posts that
289 | have at least one comment without the word "really" and with the word "totally".
290 | ```js
291 | Posts.query({
292 | comments: {
293 | $elemMatch: {
294 | $not: {
295 | text: /really/i
296 | },
297 | $and: {
298 | text: /totally/i
299 | }
300 | }
301 | }
302 | });
303 | ```
304 |
305 |
306 | ### $computed
307 | This operator allows you to perform queries on computed properties. For example you may want to perform a query
308 | for a persons full name, even though the first and last name are stored separately in your db / model.
309 | For example
310 |
311 | ```js
312 | testModel = Backbone.Model.extend({
313 | full_name: function() {
314 | return (this.get('first_name')) + " " + (this.get('last_name'));
315 | }
316 | });
317 |
318 | a = new testModel({
319 | first_name: "Dave",
320 | last_name: "Tonge"
321 | });
322 |
323 | b = new testModel({
324 | first_name: "John",
325 | last_name: "Smith"
326 | });
327 |
328 | MyCollection = new QueryCollection([a, b]);
329 |
330 | MyCollection.query({
331 | full_name: { $computed: "Dave Tonge" }
332 | });
333 | // Returns the model with the computed `full_name` equal to Dave Tonge
334 |
335 | MyCollection.query({
336 | full_name: { $computed: { $likeI: "john smi" } }
337 | });
338 | // Any of the previous operators can be used (including elemMatch is required)
339 | ```
340 |
341 |
342 | Combined Queries
343 | ================
344 |
345 | Multiple queries can be combined together. By default all supplied queries use the `$and` operator. However it is possible
346 | to specify either `$or`, `$nor`, `$not` to implement alternate logic.
347 |
348 | ### $and
349 |
350 | ```js
351 | MyCollection.query({ $and: { title: {$like: "News"}, likes: {$gt: 10}}});
352 | // Returns all models that contain "News" in the title and have more than 10 likes.
353 | MyCollection.query({ title: {$like: "News"}, likes: {$gt: 10} });
354 | // Same as above as $and is assumed if not supplied
355 | ```
356 |
357 | ### $or
358 |
359 | ```js
360 | MyCollection.query({ $or: { title: {$like: "News"}, likes: {$gt: 10}}});
361 | // Returns all models that contain "News" in the title OR have more than 10 likes.
362 | ```
363 |
364 | ### $nor
365 | The opposite of `$or`
366 |
367 | ```js
368 | MyCollection.query({ $nor: { title: {$like: "News"}, likes: {$gt: 10}}});
369 | // Returns all models that don't contain "News" in the title NOR have more than 10 likes.
370 | ```
371 |
372 | ### $not
373 | The opposite of `$and`
374 |
375 | ```js
376 | MyCollection.query({ $not: { title: {$like: "News"}, likes: {$gt: 10}}});
377 | // Returns all models that don't contain "News" in the title AND DON'T have more than 10 likes.
378 | ```
379 |
380 | If you need to perform multiple queries on the same key, then you can supply the query as an array:
381 | ```js
382 | MyCollection.query({
383 | $or:[
384 | {title:"News"},
385 | {title:"About"}
386 | ]
387 | });
388 | // Returns all models with the title "News" or "About".
389 | ```
390 |
391 |
392 | Compound Queries
393 | ================
394 |
395 | It is possible to use multiple combined queries, for example searching for models that have a specific title attribute,
396 | and either a category of "abc" or a tag of "xyz"
397 |
398 | ```js
399 | MyCollection.query({
400 | $and: { title: {$like: "News"}},
401 | $or: {likes: {$gt: 10}, color:{$contains:"red"}}
402 | });
403 | //Returns models that have "News" in their title and
404 | //either have more than 10 likes or contain the color red.
405 | ```
406 |
407 | Sorting
408 | =======
409 | Optional `sortBy` and `order` attributes can be supplied as part of an options object.
410 | `sortBy` can either be a model key or a callback function which will be called with each model in the array.
411 |
412 | ```js
413 | MyCollection.query({title: {$like: "News"}}, {sortBy: "likes"});
414 | // Returns all models that contain "News" in the title,
415 | // sorted according to their "likes" attribute (ascending)
416 |
417 | MyCollection.query({title: {$like: "News"}}, {sortBy: "likes", order:"desc"});
418 | // Same as above, but "descending"
419 | MyCollection.query(
420 | {title: {$like: "News"}},
421 | {sortBy: function(model){ return model.get("title").charAt(1);}}
422 | );
423 | // Results sorted according to 2nd character of the title attribute
424 | ```
425 |
426 |
427 | Paging
428 | ======
429 | To return only a subset of the results paging properties can be supplied as part of an options object.
430 | A `limit` property must be supplied and optionally a `offset` or a `page` property can be supplied.
431 |
432 | ```js
433 | MyCollection.query({likes:{$gt:10}}, {limit:10});
434 | // Returns the first 10 models that have more than 10 likes
435 |
436 | MyCollection.query({likes:{$gt:10}}, {limit:10, offset:5});
437 | // Returns 10 models that have more than 10 likes starting
438 | //at the 6th model in the results
439 |
440 | MyCollection.query({likes:{$gt:10}}, {limit:10, page:2});
441 | // Returns 10 models that have more than 10 likes starting
442 | //at the 11th model in the results (page 2)
443 | ```
444 |
445 | When using the paging functionality, you will normally need to know the number of pages so that you can render
446 | the correct interface for the user. Backbone Query can send the number of pages of results to a supplied callback.
447 | The callback should be passed as a `pager` property on the options object. This callback will also receive the sliced
448 | models as a second variable.
449 |
450 | Here is a coffeescript example of a simple paging setup using the pager callback option:
451 |
452 | ```coffeescript
453 | class MyView extends Backbone.View
454 | initialize: ->
455 | @template = -> #templating setup here
456 |
457 | events:
458 | "click .page": "change_page"
459 |
460 | query_collection: (page = 1) ->
461 | #Collection should be passed in when the view is instantiated
462 | @collection.query {category:"javascript"}, {limit:5, page:page, pager:@render_pages}
463 |
464 | change_page: (e) =>
465 | page_number = $(e.target).data('page_number')
466 | @query_collection page_number
467 |
468 | render_pages: (total_pages, results) =>
469 | content = @template results
470 | pages = [1..total_pages]
471 | nav = """
472 | "
479 |
480 | @$el.html content + nav
481 |
482 | render: => @query_collection()
483 |
484 | ```
485 |
486 |
487 | Caching Results
488 | ================
489 | To enable caching set the cache flag to true in the options object. This can greatly improve performance when paging
490 | through results as the unpaged results will be saved. This options is not enabled by default as if models are changed,
491 | added to, or removed from the collection, then the query cache will be out of date. If you know
492 | that your data is static and won't change then caching can be enabled without any problems.
493 | If your data is dynamic (as in most Backbone Apps) then a helper cache reset method is provided:
494 | `reset_query_cache`. This method should be bound to your collections change, add and remove events
495 | (depending on how your data can be changed).
496 |
497 | Cache will be saved in a `_query_cache` property on each collection where a cache query is performed.
498 |
499 | ```js
500 | MyCollection.query({likes:{$gt:10}}, {limit:10, page:1, cache:true});
501 | //The first query will operate as normal and return the first page of results
502 | MyCollection.query({likes:{$gt:10}}, {limit:10, page:2, cache:true});
503 | //The second query has an identical query object to the first query, so therefore the results will be retrieved
504 | //from the cache, before the paging paramaters are applied.
505 |
506 | // Binding the reset_query_cache method
507 | var MyCollection = Backbone.QueryCollection.extend({
508 | initialize: function(){
509 | this.bind("change", this.reset_query_cache, this);
510 | }
511 | });
512 |
513 |
514 | ```
515 |
516 |
517 | Contributors
518 | ===========
519 |
520 | Dave Tonge - [davidgtonge](http://github.com/davidgtonge)
521 | Rob W - [Rob W](https://github.com/Rob--W)
522 | Cezary Wojtkowski - [cezary](https://github.com/cezary)
523 |
--------------------------------------------------------------------------------
/backbone-query.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.3.3
2 | /*
3 | Backbone Query - A lightweight query API for Backbone Collections
4 | (c)2012 - Dave Tonge
5 | May be freely distributed according to MIT license.
6 | */
7 |
8 | var __slice = [].slice,
9 | __hasProp = {}.hasOwnProperty,
10 | __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; };
11 |
12 | (function(define) {
13 | return define('backbone-query', function(require, exports) {
14 | var Backbone, detect, filter, getCache, getSortedModels, getType, iterator, makeObj, pageModels, parseQuery, parseSubQuery, performQuery, processQuery, reject, runQuery, sortModels, testModelAttribute, testQueryValue, _;
15 | _ = require('underscore');
16 | Backbone = require('backbone');
17 | /* UTILS
18 | */
19 |
20 | filter = function(array, test) {
21 | var val, _i, _len, _results;
22 | _results = [];
23 | for (_i = 0, _len = array.length; _i < _len; _i++) {
24 | val = array[_i];
25 | if (test(val)) {
26 | _results.push(val);
27 | }
28 | }
29 | return _results;
30 | };
31 | reject = function(array, test) {
32 | var val, _i, _len, _results;
33 | _results = [];
34 | for (_i = 0, _len = array.length; _i < _len; _i++) {
35 | val = array[_i];
36 | if (!test(val)) {
37 | _results.push(val);
38 | }
39 | }
40 | return _results;
41 | };
42 | detect = function(array, test) {
43 | var val, _i, _len;
44 | for (_i = 0, _len = array.length; _i < _len; _i++) {
45 | val = array[_i];
46 | if (test(val)) {
47 | return true;
48 | }
49 | }
50 | return false;
51 | };
52 | makeObj = function() {
53 | var args, current, key, o, val;
54 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
55 | o = {};
56 | current = o;
57 | while (args.length) {
58 | key = args.shift();
59 | val = (args.length === 1 ? args.shift() : {});
60 | current = current[key] = val;
61 | }
62 | return o;
63 | };
64 | getType = function(item) {
65 | if (_.isRegExp(item)) {
66 | return "$regex";
67 | }
68 | if (_.isDate(item)) {
69 | return "$date";
70 | }
71 | if (_.isObject(item) && !_.isArray(item)) {
72 | return "object";
73 | }
74 | if (_.isArray(item)) {
75 | return "array";
76 | }
77 | if (_.isString(item)) {
78 | return "string";
79 | }
80 | if (_.isNumber(item)) {
81 | return "number";
82 | }
83 | if (_.isBoolean(item)) {
84 | return "boolean";
85 | }
86 | if (_.isFunction(item)) {
87 | return "function";
88 | }
89 | return false;
90 | };
91 | /*
92 | Function to parse raw queries
93 | @param {mixed} raw query
94 | @return {array} parsed query
95 |
96 | Allows queries of the following forms:
97 | query
98 | name: "test"
99 | id: $gte: 10
100 |
101 | query [
102 | {name:"test"}
103 | {id:$gte:10}
104 | ]
105 | */
106 |
107 | parseSubQuery = function(rawQuery) {
108 | var key, o, paramType, q, query, queryArray, queryParam, type, val, value, _i, _len, _results;
109 | if (_.isArray(rawQuery)) {
110 | queryArray = rawQuery;
111 | } else {
112 | queryArray = (function() {
113 | var _results;
114 | _results = [];
115 | for (key in rawQuery) {
116 | if (!__hasProp.call(rawQuery, key)) continue;
117 | val = rawQuery[key];
118 | _results.push(makeObj(key, val));
119 | }
120 | return _results;
121 | })();
122 | }
123 | _results = [];
124 | for (_i = 0, _len = queryArray.length; _i < _len; _i++) {
125 | query = queryArray[_i];
126 | for (key in query) {
127 | if (!__hasProp.call(query, key)) continue;
128 | queryParam = query[key];
129 | o = {
130 | key: key
131 | };
132 | paramType = getType(queryParam);
133 | switch (paramType) {
134 | case "$regex":
135 | case "$date":
136 | o.type = paramType;
137 | o.value = queryParam;
138 | break;
139 | case "object":
140 | if (key === "$and" || key === "$or" || key === "$nor" || key === "$not") {
141 | o.value = parseSubQuery(queryParam);
142 | o.type = key;
143 | o.key = null;
144 | } else {
145 | for (type in queryParam) {
146 | value = queryParam[type];
147 | if (testQueryValue(type, value)) {
148 | o.type = type;
149 | switch (type) {
150 | case "$elemMatch":
151 | case "$relationMatch":
152 | o.value = parseQuery(value);
153 | break;
154 | case "$computed":
155 | q = makeObj(key, value);
156 | o.value = parseSubQuery(q);
157 | break;
158 | default:
159 | o.value = value;
160 | }
161 | }
162 | }
163 | }
164 | break;
165 | default:
166 | o.type = "$equal";
167 | o.value = queryParam;
168 | }
169 | if ((o.type === "$equal") && (paramType === "object" || paramType === "array")) {
170 | o.type = "$oEqual";
171 | }
172 | }
173 | _results.push(o);
174 | }
175 | return _results;
176 | };
177 | testQueryValue = function(type, value) {
178 | switch (type) {
179 | case "$in":
180 | case "$nin":
181 | case "$all":
182 | case "$any":
183 | return _(value).isArray();
184 | case "$size":
185 | return _(value).isNumber();
186 | case "$regex":
187 | return _(value).isRegExp();
188 | case "$like":
189 | case "$likeI":
190 | return _(value).isString();
191 | case "$between":
192 | return _(value).isArray() && (value.length === 2);
193 | case "$cb":
194 | return _(value).isFunction();
195 | default:
196 | return true;
197 | }
198 | };
199 | testModelAttribute = function(type, value) {
200 | switch (type) {
201 | case "$like":
202 | case "$likeI":
203 | case "$regex":
204 | return _(value).isString();
205 | case "$contains":
206 | case "$all":
207 | case "$any":
208 | case "$elemMatch":
209 | return _(value).isArray();
210 | case "$size":
211 | return _(value).isArray() || _(value).isString();
212 | case "$in":
213 | case "$nin":
214 | return value != null;
215 | case "$relationMatch":
216 | return (value != null) && value.models;
217 | default:
218 | return true;
219 | }
220 | };
221 | performQuery = function(type, value, attr, model, key) {
222 | switch (type) {
223 | case "$equal":
224 | if (_(attr).isArray()) {
225 | return __indexOf.call(attr, value) >= 0;
226 | } else {
227 | return attr === value;
228 | }
229 | break;
230 | case "$oEqual":
231 | return _(attr).isEqual(value);
232 | case "$contains":
233 | return __indexOf.call(attr, value) >= 0;
234 | case "$ne":
235 | return attr !== value;
236 | case "$lt":
237 | return attr < value;
238 | case "$gt":
239 | return attr > value;
240 | case "$lte":
241 | return attr <= value;
242 | case "$gte":
243 | return attr >= value;
244 | case "$between":
245 | return (value[0] < attr && attr < value[1]);
246 | case "$in":
247 | return __indexOf.call(value, attr) >= 0;
248 | case "$nin":
249 | return __indexOf.call(value, attr) < 0;
250 | case "$all":
251 | return _(value).all(function(item) {
252 | return __indexOf.call(attr, item) >= 0;
253 | });
254 | case "$any":
255 | return _(attr).any(function(item) {
256 | return __indexOf.call(value, item) >= 0;
257 | });
258 | case "$size":
259 | return attr.length === value;
260 | case "$exists":
261 | case "$has":
262 | return (attr != null) === value;
263 | case "$like":
264 | return attr.indexOf(value) !== -1;
265 | case "$likeI":
266 | return attr.toLowerCase().indexOf(value.toLowerCase()) !== -1;
267 | case "$regex":
268 | return value.test(attr);
269 | case "$cb":
270 | return value.call(model, attr);
271 | case "$elemMatch":
272 | return (runQuery(attr, value, "elemMatch")).length > 0;
273 | case "$relationMatch":
274 | return (runQuery(attr.models, value, "relationMatch")).length > 0;
275 | case "$computed":
276 | return iterator([model], value, false, detect, "computed");
277 | case "$and":
278 | case "$or":
279 | case "$nor":
280 | case "$not":
281 | return (processQuery[type]([model], value)).length === 1;
282 | default:
283 | return false;
284 | }
285 | };
286 | iterator = function(models, query, andOr, filterFunction, itemType) {
287 | if (itemType == null) {
288 | itemType = false;
289 | }
290 | return filterFunction(models, function(model) {
291 | var attr, q, test, _i, _len;
292 | for (_i = 0, _len = query.length; _i < _len; _i++) {
293 | q = query[_i];
294 | attr = (function() {
295 | switch (itemType) {
296 | case "elemMatch":
297 | return model[q.key];
298 | case "computed":
299 | return model[q.key]();
300 | default:
301 | return model.get(q.key);
302 | }
303 | })();
304 | test = testModelAttribute(q.type, attr);
305 | if (test) {
306 | test = performQuery(q.type, q.value, attr, model, q.key);
307 | }
308 | if (andOr === test) {
309 | return andOr;
310 | }
311 | }
312 | return !andOr;
313 | });
314 | };
315 | processQuery = {
316 | $and: function(models, query, itemType) {
317 | return iterator(models, query, false, filter, itemType);
318 | },
319 | $or: function(models, query, itemType) {
320 | return iterator(models, query, true, filter, itemType);
321 | },
322 | $nor: function(models, query, itemType) {
323 | return iterator(models, query, true, reject, itemType);
324 | },
325 | $not: function(models, query, itemType) {
326 | return iterator(models, query, false, reject, itemType);
327 | }
328 | };
329 | parseQuery = function(query) {
330 | var compoundKeys, compoundQuery, key, queryKeys, type, val;
331 | queryKeys = _(query).keys();
332 | compoundKeys = ["$and", "$not", "$or", "$nor"];
333 | compoundQuery = _.intersection(compoundKeys, queryKeys);
334 | if (compoundQuery.length === 0) {
335 | return [
336 | {
337 | type: "$and",
338 | parsedQuery: parseSubQuery(query)
339 | }
340 | ];
341 | } else {
342 | if (compoundQuery.length !== queryKeys.length) {
343 | if (__indexOf.call(compoundQuery, "$and") < 0) {
344 | query.$and = {};
345 | compoundQuery.unshift("$and");
346 | }
347 | for (key in query) {
348 | if (!__hasProp.call(query, key)) continue;
349 | val = query[key];
350 | if (!(__indexOf.call(compoundKeys, key) < 0)) {
351 | continue;
352 | }
353 | query.$and[key] = val;
354 | delete query[key];
355 | }
356 | }
357 | return (function() {
358 | var _i, _len, _results;
359 | _results = [];
360 | for (_i = 0, _len = compoundQuery.length; _i < _len; _i++) {
361 | type = compoundQuery[_i];
362 | _results.push({
363 | type: type,
364 | parsedQuery: parseSubQuery(query[type])
365 | });
366 | }
367 | return _results;
368 | })();
369 | }
370 | };
371 | runQuery = function(items, query, itemType) {
372 | var reduceIterator;
373 | if (!itemType) {
374 | query = parseQuery(query);
375 | }
376 | reduceIterator = function(memo, queryItem) {
377 | return processQuery[queryItem.type](memo, queryItem.parsedQuery, itemType);
378 | };
379 | return _.reduce(query, reduceIterator, items);
380 | };
381 | getCache = function(collection, query, options) {
382 | var cache, models, queryString, _ref;
383 | queryString = JSON.stringify(query);
384 | cache = (_ref = collection._queryCache) != null ? _ref : collection._queryCache = {};
385 | models = cache[queryString];
386 | if (!models) {
387 | models = getSortedModels(collection, query, options);
388 | cache[queryString] = models;
389 | }
390 | return models;
391 | };
392 | getSortedModels = function(collection, query, options) {
393 | var models;
394 | models = runQuery(collection.models, query);
395 | if (options.sortBy) {
396 | models = sortModels(models, options);
397 | }
398 | return models;
399 | };
400 | sortModels = function(models, options) {
401 | if (_(options.sortBy).isString()) {
402 | models = _(models).sortBy(function(model) {
403 | return model.get(options.sortBy);
404 | });
405 | } else if (_(options.sortBy).isFunction()) {
406 | models = _(models).sortBy(options.sortBy);
407 | }
408 | if (options.order === "desc") {
409 | models = models.reverse();
410 | }
411 | return models;
412 | };
413 | pageModels = function(models, options) {
414 | var end, sliced_models, start, total_pages;
415 | if (options.offset) {
416 | start = options.offset;
417 | } else if (options.page) {
418 | start = (options.page - 1) * options.limit;
419 | } else {
420 | start = 0;
421 | }
422 | end = start + options.limit;
423 | sliced_models = models.slice(start, end);
424 | if (options.pager && _.isFunction(options.pager)) {
425 | total_pages = Math.ceil(models.length / options.limit);
426 | options.pager(total_pages, sliced_models);
427 | }
428 | return sliced_models;
429 | };
430 | Backbone.QueryCollection = Backbone.Collection.extend({
431 | query: function(query, options) {
432 | var models;
433 | if (options == null) {
434 | options = {};
435 | }
436 | if (options.cache) {
437 | models = getCache(this, query, options);
438 | } else {
439 | models = getSortedModels(this, query, options);
440 | }
441 | if (options.limit) {
442 | models = pageModels(models, options);
443 | }
444 | return models;
445 | },
446 | findOne: function(query) {
447 | return this.query(query)[0];
448 | },
449 | whereBy: function(params, options) {
450 | if (options == null) {
451 | options = {};
452 | }
453 | return new this.constructor(this.query(params, options));
454 | },
455 | resetQueryCache: function() {
456 | return this._queryCache = {};
457 | }
458 | });
459 | return exports.QueryCollection = Backbone.QueryCollection;
460 | });
461 | }).call(this, typeof define === 'function' && define.amd ? define : function(id, factory) {
462 | if (typeof exports !== 'undefined') {
463 | factory((function(id) {
464 | return require(id);
465 | }), exports);
466 | } else {
467 | factory((function(id) {
468 | return this[id === 'underscore' ? '_' : 'Backbone'];
469 | }), {});
470 | }
471 | });
472 |
--------------------------------------------------------------------------------
/backbone-query.min.js:
--------------------------------------------------------------------------------
1 | var __slice=[].slice,__hasProp={}.hasOwnProperty,__indexOf=[].indexOf||function(a){for(var b=0,c=this.length;b=0:c===b;case"$oEqual":return u(c).isEqual(b);case"$contains":return __indexOf.call(c,b)>=0;case"$ne":return c!==b;case"$lt":return cb;case"$lte":return c<=b;case"$gte":return c>=b;case"$between":return b[0]=0;case"$nin":return __indexOf.call(b,c)<0;case"$all":return u(b).all(function(a){return __indexOf.call(c,a)>=0});case"$any":return u(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(e,c);case"$elemMatch":return q(c,b,"elemMatch").length>0;case"$relationMatch":return q(c.models,b,"relationMatch").length>0;case"$computed":return i([e],b,!1,d,"computed");case"$and":case"$or":case"$nor":case"$not":return o[a]([e],b).length===1;default:return!1}},i=function(a,b,c,d,e){return e==null&&(e=!1),d(a,function(a){var d,f,g,h,i;for(h=0,i=b.length;h (https://github.com/davidgtonge)",
6 | "tags": [
7 | "backbone",
8 | "underscore",
9 | "mongo",
10 | "query"
11 | ],
12 | "main": "./backbone-query",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://davidgtonge@github.com/davidgtonge/backbone_query.git"
16 | },
17 | "dependencies": {
18 | "backbone": ">=0.5.x",
19 | "underscore": ">=1.1.x"
20 | },
21 | "devDependencies": {
22 | "coffee-script": ">=1.x.x",
23 | "mocha": "",
24 | "uglify-js": "1.2.2"
25 | },
26 | "jam": {
27 | "dependencies": {
28 | "underscore": "*",
29 | "backbone": "*",
30 | "jquery": "*"
31 | },
32 | "include": ["backbone-query.js"],
33 |
34 | "main": "backbone-query.js",
35 |
36 | "shim": {
37 | "deps": ["jquery", "backbone", "underscore"],
38 | "exports": "Backbone.QueryCollection"
39 | }
40 | },
41 | "scripts": {
42 | "test": "mocha"
43 | }
44 | }
--------------------------------------------------------------------------------
/src/backbone-query.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | Backbone Query - A lightweight query API for Backbone Collections
3 | (c)2012 - Dave Tonge
4 | May be freely distributed according to MIT license.
5 | ###
6 | ((define) -> define 'backbone-query', (require, exports) ->
7 | _ = require('underscore')
8 | Backbone = require('backbone')
9 |
10 | ### UTILS ###
11 |
12 | # Custom Filter / Reject methods faster than underscore methods as use for loops
13 | # http://jsperf.com/filter-vs-for-loop2
14 | filter = (array, test) -> (val for val in array when test val)
15 | reject = (array, test) -> (val for val in array when not test val)
16 | detect = (array, test) ->
17 | for val in array
18 | return true if test val
19 | false
20 |
21 | # Utility Function to turn a list of values into an object
22 | makeObj = (args...)->
23 | o = {}
24 | current = o
25 | while args.length
26 | key = args.shift()
27 | val = (if args.length is 1 then args.shift() else {})
28 | current = current[key] = val
29 | o
30 |
31 | # Get the type as a string
32 | getType = (item) ->
33 | return "$regex" if _.isRegExp(item)
34 | return "$date" if _.isDate(item)
35 | return "object" if _.isObject(item) and not _.isArray(item)
36 | return "array" if _.isArray(item)
37 | return "string" if _.isString(item)
38 | return "number" if _.isNumber(item)
39 | return "boolean" if _.isBoolean(item)
40 | return "function" if _.isFunction(item)
41 | false
42 |
43 | ###
44 | Function to parse raw queries
45 | @param {mixed} raw query
46 | @return {array} parsed query
47 |
48 | Allows queries of the following forms:
49 | query
50 | name: "test"
51 | id: $gte: 10
52 |
53 | query [
54 | {name:"test"}
55 | {id:$gte:10}
56 | ]
57 | ###
58 | parseSubQuery = (rawQuery) ->
59 |
60 | if _.isArray(rawQuery)
61 | queryArray = rawQuery
62 | else
63 | queryArray = (makeObj(key, val) for own key, val of rawQuery)
64 |
65 | (for query in queryArray
66 | for own key, queryParam of query
67 | o = {key}
68 | paramType = getType(queryParam)
69 | switch paramType
70 | # Test for Regexs and Dates as they can be supplied without an operator
71 | when "$regex", "$date"
72 | o.type = paramType
73 | o.value = queryParam
74 |
75 | # If the query paramater is an object then extract the key and value
76 | when "object"
77 | if key in ["$and", "$or", "$nor", "$not"]
78 | o.value = parseSubQuery queryParam
79 | o.type = key
80 | o.key = null
81 | else
82 | for type, value of queryParam
83 | # Before adding the query, its value is checked to make sure it is the right type
84 | if testQueryValue type, value
85 | o.type = type
86 | switch type
87 | when "$elemMatch", "$relationMatch"
88 | o.value = parseQuery value
89 | when "$computed"
90 | q = makeObj(key,value)
91 | o.value = parseSubQuery q
92 | else
93 | o.value = value
94 |
95 | # If the query_param is not an object or a regexp then revert to the default operator: $equal
96 | else
97 | o.type = "$equal"
98 | o.value = queryParam
99 |
100 | # For "$equal" queries with arrays or objects we need to perform a deep equal
101 | if (o.type is "$equal") and (paramType in ["object","array"])
102 | o.type = "$oEqual"
103 | o)
104 |
105 |
106 |
107 | # Tests query value, to ensure that it is of the correct type
108 | testQueryValue = (type, value) ->
109 | switch type
110 | when "$in","$nin","$all", "$any" then _(value).isArray()
111 | when "$size" then _(value).isNumber()
112 | when "$regex" then _(value).isRegExp()
113 | when "$like", "$likeI" then _(value).isString()
114 | when "$between" then _(value).isArray() and (value.length is 2)
115 | when "$cb" then _(value).isFunction()
116 | else true
117 |
118 | # Test each attribute that is being tested to ensure that is of the correct type
119 | testModelAttribute = (type, value) ->
120 | switch type
121 | when "$like", "$likeI", "$regex" then _(value).isString()
122 | when "$contains", "$all", "$any", "$elemMatch" then _(value).isArray()
123 | when "$size" then _(value).isArray() or _(value).isString()
124 | when "$in", "$nin" then value?
125 | when "$relationMatch" then value? and value.models
126 | else true
127 |
128 | # Perform the actual query logic for each query and each model/attribute
129 | performQuery = (type, value, attr, model, key) ->
130 | switch type
131 | when "$equal"
132 | # If the attribute is an array then search for the query value in the array the same as Mongo
133 | if _(attr).isArray() then value in attr else attr is value
134 | when "$oEqual" then _(attr).isEqual value
135 | when "$contains" then value in attr
136 | when "$ne" then attr isnt value
137 | when "$lt" then attr < value
138 | when "$gt" then attr > value
139 | when "$lte" then attr <= value
140 | when "$gte" then attr >= value
141 | when "$between" then value[0] < attr < value[1]
142 | when "$in" then attr in value
143 | when "$nin" then attr not in value
144 | when "$all" then _(value).all (item) -> item in attr
145 | when "$any" then _(attr).any (item) -> item in value
146 | when "$size" then attr.length is value
147 | when "$exists", "$has" then attr? is value
148 | when "$like" then attr.indexOf(value) isnt -1
149 | when "$likeI" then attr.toLowerCase().indexOf(value.toLowerCase()) isnt -1
150 | when "$regex" then value.test attr
151 | when "$cb" then value.call model, attr
152 | when "$elemMatch" then (runQuery(attr,value,"elemMatch")).length > 0
153 | when "$relationMatch" then (runQuery(attr.models,value,"relationMatch")).length > 0
154 | when "$computed" then iterator [model], value, false, detect, "computed"
155 | when "$and", "$or", "$nor", "$not"
156 | (processQuery[type]([model], value)).length is 1
157 | else false
158 |
159 |
160 | # The main iterator that actually applies the query
161 | iterator = (models, query, andOr, filterFunction, itemType = false) ->
162 | # The collections filter or reject method is used to iterate through each model in the collection
163 | filterFunction models, (model) ->
164 | # For each model in the collection, iterate through the supplied queries
165 | for q in query
166 | # Retrieve the attribute value from the model
167 | attr = switch itemType
168 | when "elemMatch" then model[q.key]
169 | when "computed" then model[q.key]()
170 | else model.get(q.key)
171 | # Check if the attribute value is the right type (some operators need a string, or an array)
172 | test = testModelAttribute(q.type, attr)
173 | # If the attribute test is true, perform the query
174 | if test then test = performQuery q.type, q.value, attr, model, q.key
175 | # If the query is an "or" query than as soon as a match is found we return "true"
176 | # Whereas if the query is an "and" query then we return "false" as soon as a match isn't found.
177 | return andOr if andOr is test
178 |
179 | # For an "or" query, if all the queries are false, then we return false
180 | # For an "and" query, if all the queries are true, then we return true
181 | not andOr
182 |
183 |
184 |
185 | # An object with or, and, nor and not methods
186 | processQuery =
187 | $and: (models, query, itemType) -> iterator models, query, false, filter, itemType
188 | $or: (models, query, itemType) -> iterator models, query, true, filter, itemType
189 | $nor: (models, query, itemType) -> iterator models, query, true, reject, itemType
190 | $not: (models, query, itemType) -> iterator models, query, false, reject, itemType
191 |
192 | parseQuery = (query) ->
193 | queryKeys = _(query).keys()
194 | compoundKeys = ["$and", "$not", "$or", "$nor"]
195 | compoundQuery = _.intersection compoundKeys, queryKeys
196 |
197 | # If no compound methods are found then use the "and" iterator
198 | if compoundQuery.length is 0
199 | return [{type:"$and", parsedQuery:parseSubQuery(query)}]
200 | else
201 | # Detect if there is an implicit $and compundQuery operator
202 | if compoundQuery.length isnt queryKeys.length
203 | # Add the and compund query operator (with a sanity check that it doesn't exist)
204 | if "$and" not in compoundQuery
205 | query.$and = {}
206 | compoundQuery.unshift "$and"
207 | for own key, val of query when key not in compoundKeys
208 | query.$and[key] = val
209 | delete query[key]
210 | return (for type in compoundQuery
211 | {type, parsedQuery:parseSubQuery(query[type])})
212 |
213 |
214 | runQuery = (items, query, itemType) ->
215 | query = parseQuery(query) unless itemType
216 | reduceIterator = (memo, queryItem) ->
217 | processQuery[queryItem.type] memo, queryItem.parsedQuery, itemType
218 | _.reduce query, reduceIterator, items
219 |
220 |
221 | # This method attempts to retrieve the result from the cache.
222 | # If no match is found in the cache, then the query is run and
223 | # the results are saved in the cache
224 | getCache = (collection, query, options) ->
225 | # Convert the query to a string to use as a key in the cache
226 | queryString = JSON.stringify query
227 | # Create cache if doesn't exist
228 | cache = collection._queryCache ?= {}
229 | # Retrieve cached results
230 | models = cache[queryString]
231 | # If no results are retrieved then use the get_models method and cache the result
232 | unless models
233 | models = getSortedModels collection, query, options
234 | cache[queryString] = models
235 | # Return the results
236 | models
237 |
238 |
239 | # Gets the results and optionally sorts them
240 | getSortedModels = (collection, query, options) ->
241 | models = runQuery(collection.models, query)
242 | if options.sortBy then models = sortModels models, options
243 | models
244 |
245 | # Sorts models either be a model attribute or with a callback
246 | sortModels = (models, options) ->
247 | # If the sortBy param is a string then we sort according to the model attribute with that string as a key
248 | if _(options.sortBy).isString()
249 | models = _(models).sortBy (model) -> model.get(options.sortBy)
250 | # If a function is supplied then it is passed directly to the sortBy iterator
251 | else if _(options.sortBy).isFunction()
252 | models = _(models).sortBy(options.sortBy)
253 |
254 | # If there is an order property of "desc" then the results can be reversed
255 | # (sortBy provides result in ascending order by default)
256 | if options.order is "desc" then models = models.reverse()
257 | # The sorted models are returned
258 | models
259 |
260 | # Slices the results set according to the supplied options
261 | pageModels = (models, options) ->
262 | # Expects object in the form: {limit: num, offset: num, page: num, pager:callback}
263 | if options.offset then start = options.offset
264 | else if options.page then start = (options.page - 1) * options.limit
265 | else start = 0
266 |
267 | end = start + options.limit
268 |
269 | # The results are sliced according to the calculated start and end params
270 | sliced_models = models[start...end]
271 |
272 | if options.pager and _.isFunction(options.pager)
273 | total_pages = Math.ceil (models.length / options.limit)
274 | options.pager total_pages, sliced_models
275 |
276 | sliced_models
277 |
278 |
279 | Backbone.QueryCollection = Backbone.Collection.extend
280 |
281 | # The main query method
282 | query: (query, options = {}) ->
283 |
284 | # Retrieve matching models using the supplied query
285 | if options.cache
286 | models = getCache @, query, options
287 | else
288 | models = getSortedModels @, query, options
289 |
290 | # If a limit param is specified than slice the results
291 | if options.limit then models = pageModels models, options
292 |
293 | # Return the results
294 | models
295 |
296 | findOne: (query) -> @query(query)[0]
297 |
298 | # Where method wraps query and returns a new collection
299 | whereBy: (params, options = {})->
300 | new @constructor @query params, options
301 |
302 | # Helper method to reset the query cache
303 | # Defined as a separate method to make it easy to bind to collection's change/add/remove events
304 | resetQueryCache: -> @_queryCache = {}
305 |
306 | # On the server the new Query Collection is added to exports
307 | exports.QueryCollection = Backbone.QueryCollection
308 | ).call this, if typeof define == 'function' and define.amd then define else (id, factory) ->
309 | unless typeof exports is 'undefined'
310 | factory ((id) -> require id), exports
311 | else
312 | # Load Underscore and backbone. No need to export QueryCollection in an module-less environment
313 | factory ((id) -> this[if id == 'underscore' then '_' else 'Backbone']), {}
314 | return
315 |
316 |
--------------------------------------------------------------------------------
/test/bq-test.coffee:
--------------------------------------------------------------------------------
1 | # Requires
2 | assert = require('assert')
3 | {QueryCollection} = require "../src/backbone-query"
4 | Backbone = require('backbone')
5 |
6 | create = ->
7 | new QueryCollection [
8 | {title:"Home", colors:["red","yellow","blue"], likes:12, featured:true, content: "Dummy content about coffeescript"}
9 | {title:"About", colors:["red"], likes:2, featured:true, content: "dummy content about javascript"}
10 | {title:"Contact", colors:["red","blue"], likes:20, content: "Dummy content about PHP"}
11 | ]
12 |
13 |
14 |
15 |
16 |
17 | describe "Backbone Query Tests", ->
18 |
19 | it "Equals query", ->
20 | a = create()
21 | result = a.query title:"Home"
22 | assert.equal result.length, 1
23 | assert.equal result[0].get("title"), "Home"
24 |
25 | result = a.whereBy colors: "blue"
26 | assert.equal result.length, 2
27 |
28 | result = a.whereBy colors: ["red", "blue"]
29 | assert.equal result.length, 1
30 |
31 | it "Simple equals query (no results)", ->
32 | a = create()
33 | result = a.whereBy title:"Homes"
34 | assert.equal result.length, 0
35 |
36 | it "Simple equals query with explicit $equal", ->
37 | a = create()
38 | result = a.whereBy title: {$equal: "About"}
39 | assert.equal result.length, 1
40 | assert.equal result.at(0).get("title"), "About"
41 |
42 | it "$contains operator", ->
43 | a = create()
44 | result = a.whereBy colors: {$contains: "blue"}
45 | assert.equal result.length, 2
46 |
47 | it "$ne operator", ->
48 | a = create()
49 | result = a.whereBy title: {$ne: "Home"}
50 | assert.equal result.length, 2
51 |
52 | it "$lt operator", ->
53 | a = create()
54 | result = a.whereBy likes: {$lt: 12}
55 | assert.equal result.length, 1
56 | assert.equal result.at(0).get("title"), "About"
57 |
58 | it "$lte operator", ->
59 | a = create()
60 | result = a.whereBy likes: {$lte: 12}
61 | assert.equal result.length, 2
62 |
63 | it "$gt operator", ->
64 | a = create()
65 | result = a.whereBy likes: {$gt: 12}
66 | assert.equal result.length, 1
67 | assert.equal result.at(0).get("title"), "Contact"
68 |
69 | it "$gte operator", ->
70 | a = create()
71 | result = a.whereBy likes: {$gte: 12}
72 | assert.equal result.length, 2
73 |
74 | it "$between operator", ->
75 | a = create()
76 | result = a.whereBy likes: {$between: [1,5]}
77 | assert.equal result.length, 1
78 | assert.equal result.at(0).get("title"), "About"
79 |
80 | it "$in operator", ->
81 | a = create()
82 | result = a.whereBy title: {$in: ["Home","About"]}
83 | assert.equal result.length, 2
84 |
85 | it "$in operator with wrong query value", ->
86 | a = create()
87 | result = a.whereBy title: {$in: "Home"}
88 | assert.equal result.length, 0
89 |
90 | it "$nin operator", ->
91 | a = create()
92 | result = a.whereBy title: {$nin: ["Home","About"]}
93 | assert.equal result.length, 1
94 | assert.equal result.at(0).get("title"), "Contact"
95 |
96 | it "$all operator", ->
97 | a = create()
98 | result = a.whereBy colors: {$all: ["red","blue"]}
99 | assert.equal result.length, 2
100 |
101 | it "$all operator (wrong values)", ->
102 | a = create()
103 | result = a.whereBy title: {$all: ["red","blue"]}
104 | assert.equal result.length, 0
105 |
106 | result = a.whereBy colors: {$all: "red"}
107 | assert.equal result.length, 0
108 |
109 | it "$any operator", ->
110 | a = create()
111 | result = a.whereBy colors: {$any: ["red","blue"]}
112 | assert.equal result.length, 3
113 |
114 | result = a.whereBy colors: {$any: ["yellow","blue"]}
115 | assert.equal result.length, 2
116 |
117 | it "$size operator", ->
118 | a = create()
119 | result = a.whereBy colors: {$size: 3}
120 | assert.equal result.length, 1
121 | assert.equal result.at(0).get("title"), "Home"
122 |
123 | it "$exists operator", ->
124 | a = create()
125 | result = a.whereBy featured: {$exists: true}
126 | assert.equal result.length, 2
127 |
128 | it "$has operator", ->
129 | a = create()
130 | result = a.whereBy featured: {$exists: false}
131 | assert.equal result.length, 1
132 | assert.equal result.at(0).get("title"), "Contact"
133 |
134 | it "$like operator", ->
135 | a = create()
136 | result = a.whereBy content: {$like: "javascript"}
137 | assert.equal result.length, 1
138 | assert.equal result.at(0).get("title"), "About"
139 |
140 | it "$like operator 2", ->
141 | a = create()
142 | result = a.whereBy content: {$like: "content"}
143 | assert.equal result.length, 3
144 |
145 | it "$likeI operator", ->
146 | a = create()
147 | result = a.whereBy content: {$likeI: "dummy"}
148 | assert.equal result.length, 3
149 | result = a.whereBy content: {$like: "dummy"}
150 | assert.equal result.length, 1
151 |
152 | it "$regex", ->
153 | a = create()
154 | result = a.whereBy content: {$regex: /javascript/gi}
155 | assert.equal result.length, 1
156 | assert.equal result.at(0).get("title"), "About"
157 |
158 | it "$regex2", ->
159 | a = create()
160 | result = a.whereBy content: {$regex: /dummy/}
161 | assert.equal result.length, 1
162 |
163 | it "$regex3", ->
164 | a = create()
165 | result = a.whereBy content: {$regex: /dummy/i}
166 | assert.equal result.length, 3
167 |
168 | it "$regex4", ->
169 | a = create()
170 | result = a.whereBy content: /javascript/i
171 | assert.equal result.length, 1
172 |
173 | it "$cb - callback", ->
174 | a = create()
175 | result = a.whereBy title: {$cb: (attr) -> attr.charAt(0).toLowerCase() is "c"}
176 | assert.equal result.length, 1
177 | assert.equal result.at(0).get("title"), "Contact"
178 |
179 | it "$cb - callback - checking 'this' is the model", ->
180 | a = create()
181 | result = a.whereBy title:
182 | $cb: (attr) -> @get("title") is "Home"
183 | assert.equal result.length, 1
184 | assert.equal result.at(0).get("title"), "Home"
185 |
186 | it "$and operator", ->
187 | a = create()
188 | result = a.whereBy likes: {$gt: 5}, colors: {$contains: "yellow"}
189 | assert.equal result.length, 1
190 | assert.equal result.at(0).get("title"), "Home"
191 |
192 | it "$and operator (explicit)", ->
193 | a = create()
194 | result = a.whereBy $and: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
195 | assert.equal result.length, 1
196 | assert.equal result.at(0).get("title"), "Home"
197 |
198 | it "$or operator", ->
199 | a = create()
200 | result = a.whereBy $or: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
201 | assert.equal result.length, 2
202 |
203 | it "$or2 operator", ->
204 | a = create()
205 | result = a.whereBy $or: {likes: {$gt: 5}, featured: true}
206 | assert.equal result.length, 3
207 |
208 | it "$nor operator", ->
209 | a = create()
210 | result = a.whereBy $nor: {likes: {$gt: 5}, colors: {$contains: "yellow"}}
211 | assert.equal result.length, 1
212 | assert.equal result.at(0).get("title"), "About"
213 |
214 | it "Compound Queries", ->
215 | a = create()
216 | result = a.whereBy $and: {likes: {$gt: 5}}, $or: {content: {$like: "PHP"}, colors: {$contains: "yellow"}}
217 | assert.equal result.length, 2
218 |
219 | result = a.whereBy
220 | $and:
221 | likes: $lt: 15
222 | $or:
223 | content:
224 | $like: "Dummy"
225 | featured:
226 | $exists:true
227 | $not:
228 | colors: $contains: "yellow"
229 | assert.equal result.length, 1
230 | assert.equal result.at(0).get("title"), "About"
231 |
232 |
233 |
234 | it "Limit", ->
235 | a = create()
236 | result = a.whereBy {likes: {$gt: 1}}, {limit:2}
237 | assert.equal result.length, 2
238 |
239 | it "Offset", ->
240 | a = create()
241 | result = a.whereBy {likes: {$gt: 1}}, {limit:2, offset:2}
242 | assert.equal result.length, 1
243 |
244 | it "Page", ->
245 | a = create()
246 | result = a.whereBy {likes: {$gt: 1}}, {limit:3, page:2}
247 | assert.equal result.length, 0
248 |
249 | it "Sorder by model key", ->
250 | a = create()
251 | result = a.query {likes: {$gt: 1}}, {sortBy:"likes"}
252 | assert.equal result.length, 3
253 | assert.equal result[0].get("title"), "About"
254 | assert.equal result[1].get("title"), "Home"
255 | assert.equal result[2].get("title"), "Contact"
256 |
257 | it "Sorder by model key with descending order", ->
258 | a = create()
259 | result = a.query {likes: {$gt: 1}}, {sortBy:"likes", order:"desc"}
260 | assert.equal result.length, 3
261 | assert.equal result[2].get("title"), "About"
262 | assert.equal result[1].get("title"), "Home"
263 | assert.equal result[0].get("title"), "Contact"
264 |
265 | it "Sorder by function", ->
266 | a = create()
267 | result = a.query {likes: {$gt: 1}}, {sortBy: (model) -> model.get("title").charAt(2) }
268 | assert.equal result.length, 3
269 | assert.equal result[2].get("title"), "About"
270 | assert.equal result[0].get("title"), "Home"
271 | assert.equal result[1].get("title"), "Contact"
272 |
273 | it "cache", ->
274 | a = create()
275 | result = a.whereBy {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
276 | assert.equal result.length, 3
277 | result = a.whereBy {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
278 | assert.equal result.length, 3
279 | a.remove result.at(0)
280 | result = a.whereBy {likes: {$gt: 1}}, {sortBy: (model) -> model.get("title").charAt(2) }
281 | assert.equal result.length, 2
282 | result = a.whereBy {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
283 | assert.equal result.length, 3
284 |
285 |
286 | it "cache with multiple collections", ->
287 | a = create()
288 | b = create()
289 | b.remove b.at(0)
290 | assert.equal b.length, 2
291 | assert.equal a.length, 3
292 |
293 |
294 | a_result = a.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
295 | assert.equal a_result.length, 3
296 | b_result = b.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
297 | assert.equal b_result.length, 2
298 |
299 | a.remove a_result[0]
300 | b.remove b_result[0]
301 |
302 | a_result = a.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
303 | assert.equal a_result.length, 3
304 | assert.equal a.length, 2
305 |
306 |
307 | b_result = b.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
308 | assert.equal b_result.length, 2
309 | assert.equal b.length, 1
310 |
311 | a.resetQueryCache()
312 | a_result = a.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
313 | assert.equal a_result.length, 2
314 | assert.equal a.length, 2
315 |
316 | b_result = b.query {likes: {$gt: 1}}, {cache:true, sortBy: (model) -> model.get("title").charAt(2) }
317 | assert.equal b_result.length, 2
318 | assert.equal b.length, 1
319 |
320 |
321 | it "null attribute with various operators", ->
322 | a = create()
323 | result = a.whereBy wrong_key: {$like: "test"}
324 | assert.equal result.length, 0
325 | result = a.whereBy wrong_key: {$regex: /test/}
326 | assert.equal result.length, 0
327 | result = a.whereBy wrong_key: {$contains: "test"}
328 | assert.equal result.length, 0
329 | result = a.whereBy wrong_key: {$all: [12,23]}
330 | assert.equal result.length, 0
331 | result = a.whereBy wrong_key: {$any: [12,23]}
332 | assert.equal result.length, 0
333 | result = a.whereBy wrong_key: {$size: 10}
334 | assert.equal result.length, 0
335 | result = a.whereBy wrong_key: {$in: [12,23]}
336 | assert.equal result.length, 0
337 | result = a.whereBy wrong_key: {$nin: [12,23]}
338 | assert.equal result.length, 0
339 |
340 | it "Where method", ->
341 | a = create()
342 | result = a.whereBy likes: $gt: 5
343 | assert.equal result.length, 2
344 | assert.equal result.models.length, result.length
345 |
346 |
347 | it "$computed", ->
348 | class testModel extends Backbone.Model
349 | full_name: -> "#{@get 'first_name'} #{@get 'last_name'}"
350 |
351 | a = new testModel
352 | first_name: "Dave"
353 | last_name: "Tonge"
354 | b = new testModel
355 | first_name: "John"
356 | last_name: "Smith"
357 | c = new QueryCollection [a,b]
358 |
359 | result = c.query
360 | full_name: $computed: "Dave Tonge"
361 |
362 | assert.equal result.length, 1
363 | assert.equal result[0].get("first_name"), "Dave"
364 |
365 | result = c.query
366 | full_name: $computed: $likeI: "n sm"
367 | assert.equal result.length, 1
368 | assert.equal result[0].get("first_name"), "John"
369 |
370 |
371 | it "$elemMatch", ->
372 | a = new QueryCollection [
373 | {title: "Home", comments:[
374 | {text:"I like this post"}
375 | {text:"I love this post"}
376 | {text:"I hate this post"}
377 | ]}
378 | {title: "About", comments:[
379 | {text:"I like this page"}
380 | {text:"I love this page"}
381 | {text:"I really like this page"}
382 | ]}
383 | ]
384 |
385 | b = new QueryCollection [
386 | {foo: [
387 | {shape: "square", color: "purple", thick: false}
388 | {shape: "circle", color: "red", thick: true}
389 | ]}
390 | {foo: [
391 | {shape: "square", color: "red", thick: true}
392 | {shape: "circle", color: "purple", thick: false}
393 | ]}
394 | ]
395 |
396 | text_search = {$likeI: "love"}
397 |
398 | result = a.query $or:
399 | comments:
400 | $elemMatch:
401 | text: text_search
402 | title: text_search
403 | assert.equal result.length, 2
404 |
405 | result = a.query $or:
406 | comments:
407 | $elemMatch:
408 | text: /post/
409 | assert.equal result.length, 1
410 |
411 | result = a.query $or:
412 | comments:
413 | $elemMatch:
414 | text: /post/
415 | title: /about/i
416 | assert.equal result.length, 2
417 |
418 | result = a.query $or:
419 | comments:
420 | $elemMatch:
421 | text: /really/
422 | assert.equal result.length, 1
423 |
424 | result = b.query
425 | foo:
426 | $elemMatch:
427 | shape:"square"
428 | color:"purple"
429 |
430 | assert.equal result.length, 1
431 | assert.equal result[0].get("foo")[0].shape, "square"
432 | assert.equal result[0].get("foo")[0].color, "purple"
433 | assert.equal result[0].get("foo")[0].thick, false
434 |
435 |
436 | it "$any and $all", ->
437 | a = name: "test", tags1: ["red","yellow"], tags2: ["orange", "green", "red", "blue"]
438 | b = name: "test1", tags1: ["purple","blue"], tags2: ["orange", "red", "blue"]
439 | c = name: "test2", tags1: ["black","yellow"], tags2: ["green", "orange", "blue"]
440 | d = name: "test3", tags1: ["red","yellow","blue"], tags2: ["green"]
441 | e = new QueryCollection [a,b,c,d]
442 |
443 | result = e.query
444 | tags1: $any: ["red","purple"] # should match a, b, d
445 | tags2: $all: ["orange","green"] # should match a, c
446 |
447 | assert.equal result.length, 1
448 | assert.equal result[0].get("name"), "test"
449 |
450 | it "$elemMatch - compound queries", ->
451 | a = new QueryCollection [
452 | {title: "Home", comments:[
453 | {text:"I like this post"}
454 | {text:"I love this post"}
455 | {text:"I hate this post"}
456 | ]}
457 | {title: "About", comments:[
458 | {text:"I like this page"}
459 | {text:"I love this page"}
460 | {text:"I really like this page"}
461 | ]}
462 | ]
463 |
464 | result = a.query
465 | comments:
466 | $elemMatch:
467 | $not:
468 | text:/page/
469 |
470 | assert.equal result.length, 1
471 |
472 |
473 | # Test from RobW - https://github.com/Rob--W
474 | it "Explicit $and combined with matching $or must return the correct number of items", ->
475 | Col = new QueryCollection [
476 | {equ:'ok', same: 'ok'},
477 | {equ:'ok', same: 'ok'}
478 | ]
479 | result = Col.query
480 | $and:
481 | equ: 'ok' # Matches both items
482 | $or:
483 | same: 'ok' # Matches both items
484 | assert.equal result.length, 2
485 |
486 | # Test from RobW - https://github.com/Rob--W
487 | it "Implicit $and consisting of non-matching subquery and $or must return empty list", ->
488 | Col = new QueryCollection [
489 | {equ:'ok', same: 'ok'},
490 | {equ:'ok', same: 'ok'}
491 | ]
492 | result = Col.query
493 | equ: 'bogus' # Matches nothing
494 | $or:
495 | same: 'ok' # Matches all items, but due to implicit $and, this subquery should not affect the result
496 | assert.equal result.length, 0
497 |
498 | it "Testing nested compound operators", ->
499 | a = create()
500 | result = a.whereBy
501 | $and:
502 | colors: $contains: "blue" # Matches 1,3
503 | $or:
504 | featured:true # Matches 1,2
505 | likes:12 # Matches 1
506 | # And only matches 1
507 |
508 | $or:[
509 | {content:$like:"dummy"} # Matches 2
510 | {content:$like:"Dummy"} # Matches 1,3
511 | ]
512 | # Or matches 3
513 | assert.equal result.length, 1
514 |
515 | result = a.whereBy
516 | $and:
517 | colors: $contains: "blue" # Matches 1,3
518 | $or:
519 | featured:true # Matches 1,2
520 | likes:20 # Matches 3
521 | # And only matches 2
522 |
523 | $or:[
524 | {content:$like:"dummy"} # Matches 2
525 | {content:$like:"Dummy"} # Matches 1,3
526 | ]
527 | # Or matches 3
528 | assert.equal result.length, 2
529 |
530 | it "works with queries supplied as arrays", ->
531 | a = create()
532 | result = a.query
533 | $or: [
534 | {title:"Home"}
535 | {title:"About"}
536 | ]
537 | assert.equal result.length, 2
538 | assert.equal result[0].get("title"), "Home"
539 | assert.equal result[1].get("title"), "About"
540 |
541 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter spec
2 | --ui bdd
3 | --ignore-leaks
4 | --growl
5 | --bail
6 | --compilers coffee:coffee-script
--------------------------------------------------------------------------------