├── .gitignore ├── .travis.yml ├── test ├── mocha.opts └── query.js ├── package.json ├── README.md └── jsonquery.js /.gitignore: -------------------------------------------------------------------------------- 1 | .node-version 2 | *.swp 3 | .DS_Store 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --ui bdd 3 | --timeout 600000 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonquery", 3 | "version": "0.2.0", 4 | "description": "MongoDB query language implemented as a node.js Stream", 5 | "main": "jsonquery.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/eugeneware/jsonquery" 12 | }, 13 | "keywords": [ 14 | "json", 15 | "mongodb", 16 | "mongo", 17 | "stream", 18 | "filter", 19 | "query", 20 | "database", 21 | "levelup", 22 | "leveldb" 23 | ], 24 | "author": "Eugene Ware ", 25 | "license": "BSD", 26 | "devDependencies": { 27 | "chai": "^1.10.0", 28 | "mocha": "^5.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonquery 2 | 3 | MongoDB query language implemented as a Streaming filter. 4 | 5 | This library implements the entire MongoDB query language as a node.js 6 | filtering stream; 7 | 8 | [![build status](https://secure.travis-ci.org/eugeneware/jsonquery.png)](http://travis-ci.org/eugeneware/jsonquery) 9 | 10 | ## Installation 11 | 12 | To install, use npm: 13 | 14 | ``` 15 | $ npm install jsonquery 16 | ``` 17 | 18 | ## Examples 19 | 20 | Here's an example of usage: 21 | 22 | ``` js 23 | var jsonquery = require('jsonquery'); 24 | 25 | var count = 0; 26 | generator(100) // a readable stream that outputs JSON documents 27 | .pipe(jsonquery({ val: { $and: [ { $gt: 970 }, { $gt: 950 } ] } })) // filter 28 | .on('data', function (doc) { 29 | expect(doc.val).to.be.above(970); 30 | expect(doc.val).to.be.above(950); 31 | count++; 32 | }) 33 | .on('end', function () { 34 | expect(count).to.equal(2); 35 | }); 36 | ``` 37 | -------------------------------------------------------------------------------- /jsonquery.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.7.1 2 | (function() { 3 | var Stream, checkType, eq, lookup, match, operator, queryStream, valOp, valOpMatch; 4 | 5 | Stream = require('stream'); 6 | 7 | lookup = function(needle, haystack) { 8 | var key, keys, val, _i, _len; 9 | keys = needle.split('.'); 10 | val = haystack; 11 | for (_i = 0, _len = keys.length; _i < _len; _i++) { 12 | key = keys[_i]; 13 | if (val === void 0) { 14 | return; 15 | } 16 | val = val[key]; 17 | } 18 | return val; 19 | }; 20 | 21 | eq = function(a, b) { 22 | var i, v; 23 | if (b instanceof RegExp) { 24 | if (Array.isArray(a)) { 25 | for (i = 0; i < a.length; i++) { 26 | v = a[i]; 27 | if (b.test(v)) { 28 | return true; 29 | } 30 | } 31 | return false; 32 | } else { 33 | return b.test(a); 34 | } 35 | } else if (Array.isArray(a)) { 36 | return a.indexOf(b) !== -1; 37 | } else { 38 | return b === a; 39 | } 40 | }; 41 | 42 | checkType = function(val, typeId) { 43 | switch (typeId) { 44 | case 1: 45 | return typeof val === 'number'; 46 | case 2: 47 | return typeof val === 'string'; 48 | case 3: 49 | return typeof val === 'object' && !(val instanceof Array); 50 | case 4: 51 | return val instanceof Array; 52 | case 8: 53 | return typeof val === 'boolean'; 54 | case 10: 55 | return typeof val === 'object' && !val; 56 | default: 57 | return false; 58 | } 59 | }; 60 | 61 | match = function(predicate, haystack) { 62 | var matchCount, matches, n, v; 63 | matches = 0; 64 | matchCount = Object.keys(predicate).length; 65 | for (n in predicate) { 66 | v = predicate[n]; 67 | if (n[0] === '$') { 68 | matches += operator(n, v, haystack); 69 | } else if (v && (v.constructor === Object || 70 | (typeof v === 'object' && !(v instanceof RegExp)))) { 71 | if (valOpMatch(lookup(n, haystack), v, haystack)) { 72 | matches++; 73 | } 74 | } else { 75 | if (eq(lookup(n, haystack), v)) { 76 | matches++; 77 | } 78 | } 79 | } 80 | return matches === matchCount; 81 | }; 82 | 83 | valOpMatch = function(val, predicate, haystack) { 84 | var matchCount, matches, n, v; 85 | matchCount = Object.keys(predicate).length; 86 | matches = 0; 87 | for (n in predicate) { 88 | v = predicate[n]; 89 | if (n[0] === '$') { 90 | if (valOp(n, val, v, haystack)) { 91 | matches++; 92 | } 93 | } 94 | } 95 | return matches === matchCount; 96 | }; 97 | 98 | valOp = function(op, val, args, haystack) { 99 | var matchCount, matches, part, v, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _m; 100 | switch (op) { 101 | case '$in': 102 | matchCount = 0; 103 | matches = 0; 104 | for (_i = 0, _len = args.length; _i < _len; _i++) { 105 | part = args[_i]; 106 | if (eq(val, part)) { 107 | matches++; 108 | } 109 | } 110 | return matches > 0; 111 | case '$nin': 112 | return !valOp('$in', val, args, haystack); 113 | case '$all': 114 | matchCount = args.length; 115 | matches = 0; 116 | if (val) { 117 | for (_j = 0, _len1 = args.length; _j < _len1; _j++) { 118 | part = args[_j]; 119 | for (_k = 0, _len2 = val.length; _k < _len2; _k++) { 120 | v = val[_k]; 121 | if (eq(v, part)) { 122 | matches++; 123 | break; 124 | } 125 | } 126 | } 127 | } 128 | return matches === matchCount; 129 | case '$elemMatch': 130 | return match(args, val); 131 | case '$or': 132 | matches = 0; 133 | for (_l = 0, _len3 = args.length; _l < _len3; _l++) { 134 | part = args[_l]; 135 | if (valOpMatch(val, part, haystack)) { 136 | matches++; 137 | } 138 | } 139 | return matches > 0; 140 | case '$and': 141 | matchCount = args.length; 142 | matches = 0; 143 | for (_m = 0, _len4 = args.length; _m < _len4; _m++) { 144 | part = args[_m]; 145 | if (valOpMatch(val, part, haystack)) { 146 | matches++; 147 | } 148 | } 149 | return matches === matchCount; 150 | case '$not': 151 | return !valOpMatch(val, args, haystack); 152 | case '$gt': 153 | return val > args; 154 | case '$gte': 155 | return val >= args; 156 | case '$ne': 157 | return val !== args; 158 | case '$lt': 159 | return val < args; 160 | case '$lte': 161 | return val <= args; 162 | case '$mod': 163 | return (val % args[0]) === args[1]; 164 | case '$size': 165 | return val instanceof Array && val.length === args; 166 | case '$exists': 167 | if (args) { 168 | return val !== void 0; 169 | } else { 170 | return val === void 0; 171 | } 172 | break; 173 | case '$type': 174 | return checkType(val, args); 175 | default: 176 | return false; 177 | } 178 | }; 179 | 180 | operator = function(op, predicate, haystack) { 181 | var matchCount, matches, part, _i, _j, _len, _len1; 182 | switch (op) { 183 | case '$or': 184 | matches = 0; 185 | for (_i = 0, _len = predicate.length; _i < _len; _i++) { 186 | part = predicate[_i]; 187 | if (match(part, haystack)) { 188 | matches++; 189 | } 190 | } 191 | return matches > 0; 192 | case '$and': 193 | matchCount = predicate.length; 194 | matches = 0; 195 | for (_j = 0, _len1 = predicate.length; _j < _len1; _j++) { 196 | part = predicate[_j]; 197 | if (match(part, haystack)) { 198 | matches++; 199 | } 200 | } 201 | return matches === matchCount; 202 | case '$not': 203 | return !match(predicate, haystack); 204 | default: 205 | return false; 206 | } 207 | }; 208 | 209 | queryStream = function(query) { 210 | var s; 211 | s = new Stream; 212 | s.writable = true; 213 | s.readable = true; 214 | s.write = function(buf) { 215 | if (match(query, buf)) { 216 | return s.emit('data', buf); 217 | } 218 | }; 219 | s.end = function(buf) { 220 | if (arguments.length) { 221 | s.write(buf); 222 | } 223 | s.writeable = false; 224 | return s.emit('end'); 225 | }; 226 | s.destroy = function() { 227 | return s.writeable = false; 228 | }; 229 | return s; 230 | }; 231 | 232 | module.exports = queryStream; 233 | 234 | module.exports.match = function(haystack, predicate) { 235 | return match(predicate, haystack); 236 | }; 237 | 238 | }).call(this); 239 | -------------------------------------------------------------------------------- /test/query.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , Stream = require('stream') 3 | , jsonquery = require('../jsonquery'); 4 | 5 | function generator(n) { 6 | var s = new Stream; 7 | s.readable = true; 8 | var i = 0; 9 | function next() { 10 | s.emit('data', { 11 | name: "Name " + i, 12 | number: "Number " + i, 13 | val: i*10, 14 | favorites: [i*10, (i + 1)*10], 15 | awesome: true, 16 | nullify: null, 17 | tree: { 18 | a: i, 19 | b: i + 1, 20 | } 21 | }); 22 | i++; 23 | if (i == n) { 24 | s.emit('end'); 25 | } else { 26 | process.nextTick(next); 27 | } 28 | } 29 | process.nextTick(next); 30 | return s; 31 | } 32 | 33 | describe('jsonquery tests', function () { 34 | var size = 100; 35 | 36 | it('should be able to do basic queries', function (done) { 37 | var count = 0; 38 | generator(size) 39 | .pipe(jsonquery({ number: 'Number 7' })) 40 | .on('data', function (doc) { 41 | expect(doc.name).to.equal('Name 7'); 42 | expect(doc.number).to.equal('Number 7'); 43 | expect(doc.val).to.equal(70); 44 | count++; 45 | }) 46 | .on('end', function () { 47 | expect(count).to.equal(1); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should be able to do basic AND queries', function (done) { 53 | var count = 0; 54 | generator(size) 55 | .pipe(jsonquery({ number: 'Number 7', val: 70 })) 56 | .on('data', function (doc) { 57 | expect(doc.name).to.equal('Name 7'); 58 | expect(doc.number).to.equal('Number 7'); 59 | expect(doc.val).to.equal(70); 60 | count++; 61 | }) 62 | .on('end', function () { 63 | expect(count).to.equal(1); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should be able to do basic OR queries', function (done) { 69 | var count = 0; 70 | generator(size) 71 | .pipe(jsonquery({ $or: [ { number: 'Number 7' }, { val: 50 } ] })) 72 | .on('data', function (doc) { 73 | expect(doc.val == 50 || doc.number == 'Number 7').to.be.true; 74 | count++; 75 | }) 76 | .on('end', function () { 77 | expect(count).to.equal(2); 78 | done(); 79 | }); 80 | }); 81 | 82 | it('should be able to do basic $AND queries', function (done) { 83 | var count = 0; 84 | generator(size) 85 | .pipe(jsonquery({ $and: [ { number: 'Number 7' }, { val: 70 } ] })) 86 | .on('data', function (doc) { 87 | expect(doc.number).to.equal('Number 7'); 88 | expect(doc.val).to.equal(70); 89 | count++; 90 | }) 91 | .on('end', function () { 92 | expect(count).to.equal(1); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('should be able to do nested $or and $and queries', function (done) { 98 | var count = 0; 99 | generator(size) 100 | .pipe(jsonquery({ $or: [ { $and: [ { number: 'Number 7' }, { val: 70 } ] }, { val: 50 } ] })) 101 | .on('data', function (doc) { 102 | expect(doc.val == 50 || doc.number == 'Number 7').to.be.true; 103 | count++; 104 | }) 105 | .on('end', function () { 106 | expect(count).to.equal(2); 107 | done(); 108 | }); 109 | }); 110 | 111 | it('should be able to do basic $in queries', function (done) { 112 | var count = 0; 113 | generator(size) 114 | .pipe(jsonquery({ val: { $in: [ 70, 50 ] } })) 115 | .on('data', function (doc) { 116 | expect([50, 70]).to.include(doc.val); 117 | count++; 118 | }) 119 | .on('end', function () { 120 | expect(count).to.equal(2); 121 | done(); 122 | }); 123 | }); 124 | 125 | it('should be able to do $or and $in queries', function (done) { 126 | var count = 0; 127 | generator(size) 128 | .pipe(jsonquery({ val: { $or: [ { $in: [ 70, 50 ] }, { $in: [ 60, 20 ] } ] } })) 129 | .on('data', function (doc) { 130 | expect([20, 50, 60, 70]).to.include(doc.val); 131 | count++; 132 | }) 133 | .on('end', function () { 134 | expect(count).to.equal(4); 135 | done(); 136 | }); 137 | }); 138 | 139 | it('should be able to do $gt queries', function (done) { 140 | var count = 0; 141 | generator(size) 142 | .pipe(jsonquery({ val: { $gt: 900 } })) 143 | .on('data', function (doc) { 144 | expect(doc.val).to.be.above(900); 145 | count++; 146 | }) 147 | .on('end', function () { 148 | expect(count).to.equal(9); 149 | done(); 150 | }); 151 | }); 152 | 153 | it('should be able to do $lt queries', function (done) { 154 | var count = 0; 155 | generator(size) 156 | .pipe(jsonquery({ val: { $lt: 900 } })) 157 | .on('data', function (doc) { 158 | expect(doc.val).to.be.below(900); 159 | count++; 160 | }) 161 | .on('end', function () { 162 | expect(count).to.equal(90); 163 | done(); 164 | }); 165 | }); 166 | 167 | it('should be able to nest $or, $lt, and $gt', function (done) { 168 | var count = 0; 169 | generator(size) 170 | .pipe(jsonquery({ val: { $or: [ { $lt: 20 }, { $gt: 950 } ] } })) 171 | .on('data', function (doc) { 172 | count++; 173 | }) 174 | .on('end', function () { 175 | expect(count).to.equal(6); 176 | done(); 177 | }); 178 | }); 179 | 180 | it('should be able to nest $and, $lt, and $gt', function (done) { 181 | var count = 0; 182 | generator(size) 183 | .pipe(jsonquery({ val: { $and: [ { $gt: 970 }, { $gt: 950 } ] } })) 184 | .on('data', function (doc) { 185 | expect(doc.val).to.be.above(970); 186 | expect(doc.val).to.be.above(950); 187 | count++; 188 | }) 189 | .on('end', function () { 190 | expect(count).to.equal(2); 191 | done(); 192 | }); 193 | }); 194 | 195 | it('should be able to do $ne queries', function (done) { 196 | var count = 0; 197 | generator(size) 198 | .pipe(jsonquery({ val: { $ne: 900 } })) 199 | .on('data', function (doc) { 200 | expect(doc.val).to.not.equal(900); 201 | count++; 202 | }) 203 | .on('end', function () { 204 | expect(count).to.equal(99); 205 | done(); 206 | }); 207 | }); 208 | 209 | it('should be able to do $lte queries', function (done) { 210 | var count = 0; 211 | generator(size) 212 | .pipe(jsonquery({ val: { $lte: 900 } })) 213 | .on('data', function (doc) { 214 | expect(doc.val).to.be.lte(900); 215 | count++; 216 | }) 217 | .on('end', function () { 218 | expect(count).to.equal(91); 219 | done(); 220 | }); 221 | }); 222 | 223 | it('should be able to do $gte queries', function (done) { 224 | var count = 0; 225 | generator(size) 226 | .pipe(jsonquery({ val: { $gte: 900 } })) 227 | .on('data', function (doc) { 228 | expect(doc.val).to.be.gte(900); 229 | count++; 230 | }) 231 | .on('end', function () { 232 | expect(count).to.equal(10); 233 | done(); 234 | }); 235 | }); 236 | 237 | it('should be able to do $all queries', function (done) { 238 | var count = 0; 239 | generator(size) 240 | .pipe(jsonquery({ favorites: { $all: [50, 60] } })) 241 | .on('data', function (doc) { 242 | expect(doc.favorites).to.deep.equal([50, 60]); 243 | count++; 244 | }) 245 | .on('end', function () { 246 | expect(count).to.equal(1); 247 | done(); 248 | }); 249 | }); 250 | 251 | it('should be able to do $elemMatch queries', function (done) { 252 | var count = 0; 253 | generator(size) 254 | .pipe(jsonquery({ tree: { $elemMatch: { a: 1, b: 2 } } })) 255 | .on('data', function (doc) { 256 | expect(doc.tree.a).to.equal(1); 257 | expect(doc.tree.b).to.equal(2); 258 | count++; 259 | }) 260 | .on('end', function () { 261 | expect(count).to.equal(1); 262 | done(); 263 | }); 264 | }); 265 | 266 | it('should be able to do child queries', function (done) { 267 | var count = 0; 268 | generator(size) 269 | .pipe(jsonquery({ 'tree.a': 1 })) 270 | .on('data', function (doc) { 271 | expect(doc.tree.a).to.equal(1); 272 | count++; 273 | }) 274 | .on('end', function () { 275 | expect(count).to.equal(1); 276 | done(); 277 | }); 278 | }); 279 | 280 | it('should be able to do nested child queries', function (done) { 281 | var count = 0; 282 | generator(size) 283 | .pipe(jsonquery({ 'tree.a': { $in: [1, 5] } })) 284 | .on('data', function (doc) { 285 | expect([1, 5]).to.include(doc.tree.a); 286 | count++; 287 | }) 288 | .on('end', function () { 289 | expect(count).to.equal(2); 290 | done(); 291 | }); 292 | }); 293 | 294 | it('should be able to do regex queries', function (done) { 295 | var count = 0; 296 | generator(size) 297 | .pipe(jsonquery({ number: /er 7$/ })) 298 | .on('data', function (doc) { 299 | expect(doc.number).to.equal('Number 7'); 300 | count++; 301 | }) 302 | .on('end', function () { 303 | expect(count).to.equal(1); 304 | done(); 305 | }); 306 | }); 307 | 308 | it('should be able to do $in queries with regex', function (done) { 309 | var count = 0; 310 | generator(size) 311 | .pipe(jsonquery({ number: { $in: [ /er 7$/, /er 5$/ ] } })) 312 | .on('data', function (doc) { 313 | expect(['Number 5', 'Number 7']).to.include(doc.number); 314 | count++; 315 | }) 316 | .on('end', function () { 317 | expect(count).to.equal(2); 318 | done(); 319 | }); 320 | }); 321 | 322 | it('should be able to do $all queries with regex', function (done) { 323 | var count = 0; 324 | generator(size) 325 | .pipe(jsonquery({ favorites: { $all: [/^50$/, /^60$/] } })) 326 | .on('data', function (doc) { 327 | expect(doc.favorites).to.deep.equal([50, 60]); 328 | count++; 329 | }) 330 | .on('end', function () { 331 | expect(count).to.equal(1); 332 | done(); 333 | }); 334 | }); 335 | 336 | it('should be able to do $in queries with regex over array', function (done) { 337 | function containsQueriedValues(haystack) { 338 | for (var i = 0; i < haystack.length; i++) { 339 | if ([50, 60].indexOf(haystack[i]) >= 0) { 340 | return true; 341 | } 342 | } 343 | return false; 344 | } 345 | var count = 0; 346 | generator(size) 347 | .pipe(jsonquery({ favorites: { $in: [ /^50$/, /^60$/ ] } })) 348 | .on('data', function (doc) { 349 | expect(doc.favorites).to.satisfy(containsQueriedValues); 350 | count++; 351 | }) 352 | .on('end', function () { 353 | expect(count).to.equal(3); 354 | done(); 355 | }); 356 | }); 357 | 358 | it('should be able to do $elemMatch queries with regex', function (done) { 359 | var count = 0; 360 | generator(size) 361 | .pipe(jsonquery({ tree: { $elemMatch: { a: /^1$/, b: 2 } } })) 362 | .on('data', function (doc) { 363 | expect(doc.tree.a).to.equal(1); 364 | expect(doc.tree.b).to.equal(2); 365 | count++; 366 | }) 367 | .on('end', function () { 368 | expect(count).to.equal(1); 369 | done(); 370 | }); 371 | }); 372 | 373 | it('should be able to do basic $nin queries', function (done) { 374 | var count = 0; 375 | generator(size) 376 | .pipe(jsonquery({ val: { $nin: [ 70, 50 ] } })) 377 | .on('data', function (doc) { 378 | expect(doc.val).to.not.equal(70); 379 | expect(doc.val).to.not.equal(50); 380 | count++; 381 | }) 382 | .on('end', function () { 383 | expect(count).to.equal(98); 384 | done(); 385 | }); 386 | }); 387 | 388 | it('should be able to do basic $mod queries', function (done) { 389 | var count = 0; 390 | generator(size) 391 | .pipe(jsonquery({ val: { $mod: [ 7, 1 ] } })) 392 | .on('data', function (doc) { 393 | expect(doc.val % 7 === 1).to.be.true; 394 | count++; 395 | }) 396 | .on('end', function () { 397 | expect(count).to.equal(14); 398 | done(); 399 | }); 400 | }); 401 | 402 | it('should be able to do $size queries', function (done) { 403 | var count = 0; 404 | generator(size) 405 | .pipe(jsonquery({ favorites: { $size: 2 } })) 406 | .on('data', function (doc) { 407 | expect(doc.favorites.length).to.equal(2); 408 | count++; 409 | }) 410 | .on('end', function () { 411 | expect(count).to.equal(100); 412 | done(); 413 | }); 414 | }); 415 | 416 | it('should be able to do $exists queries', function (done) { 417 | var count = 0; 418 | generator(size) 419 | .pipe(jsonquery({ $and: [ { tree: { $exists: true } }, { missing: { $exists: false } } ] })) 420 | .on('data', function (doc) { 421 | expect(doc.tree).to.exist; 422 | expect(doc.missing).to.not.exist; 423 | count++; 424 | }) 425 | .on('end', function () { 426 | expect(count).to.equal(100); 427 | done(); 428 | }); 429 | }); 430 | 431 | it('should have limited support for $type queries', function (done) { 432 | var count = 0; 433 | generator(size) 434 | .pipe(jsonquery({ 435 | name: { $type: 2 }, 436 | val: { $type: 1 }, 437 | tree: { $type: 3 }, 438 | favorites: { $type: 4 }, 439 | awesome: { $type: 8 }, 440 | nullify: { $type: 10 } 441 | })) 442 | .on('data', function (doc) { 443 | expect(doc.name).to.be.a('string'); 444 | count++; 445 | }) 446 | .on('end', function () { 447 | expect(count).to.equal(100); 448 | done(); 449 | }); 450 | }); 451 | 452 | it('should be able to do basic $not queries part 1', function (done) { 453 | var count = 0; 454 | generator(size) 455 | .pipe(jsonquery({ $not: { number: 'Number 7', val: 70 } })) 456 | .on('data', function (doc) { 457 | expect(doc.number).to.not.equal('Number 7'); 458 | expect(doc.number).to.not.equal(70); 459 | count++; 460 | }) 461 | .on('end', function () { 462 | expect(count).to.equal(99); 463 | done(); 464 | }); 465 | }); 466 | 467 | it('should be able to do basic $not queries part 2', function (done) { 468 | var count = 0; 469 | generator(size) 470 | .pipe(jsonquery({ number: { $not: { $nin: ['Number 7'] } } })) 471 | .on('data', function (doc) { 472 | expect(doc.number).to.equal('Number 7'); 473 | count++; 474 | }) 475 | .on('end', function () { 476 | expect(count).to.equal(1); 477 | done(); 478 | }); 479 | }); 480 | 481 | it('should not match a document missing a field', function (done) { 482 | var count = 0; 483 | stream = jsonquery({ foo: { $all: ['bar'] } }) 484 | 485 | stream 486 | .on('data', function (doc) { 487 | expect(doc.foo).to.deep.equal(['bar']); 488 | count++; 489 | }) 490 | .on('end', function () { 491 | expect(count).to.equal(1); 492 | done(); 493 | }); 494 | 495 | stream.write({ bar: ['baz'] }) 496 | stream.write({ foo: ['bar'] }) 497 | stream.end() 498 | }); 499 | }); 500 | --------------------------------------------------------------------------------