(function () { /* Imports */ var Meteor = Package.meteor.Meteor; var _ = Package.underscore._; var EJSON = Package.ejson.EJSON; var IdMap = Package['id-map'].IdMap; var OrderedDict = Package['ordered-dict'].OrderedDict; var Tracker = Package.tracker.Tracker; var Deps = Package.tracker.Deps; var Random = Package.random.Random; var GeoJSON = Package['geojson-utils'].GeoJSON; /* Package-scope variables */ var LocalCollection, Minimongo, MinimongoTest, MinimongoError, isArray, isPlainObject, isIndexable, isOperatorObject, isNumericKey, regexpElementMatcher, equalityElementMatcher, ELEMENT_OPERATORS, makeLookupFunction, expandArraysInBranches, projectionDetails, pathsToTree, combineImportantPathsIntoProjection; (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/minimongo.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // XXX type checking on selectors (graceful error if malformed) // 1 // 2 // LocalCollection: a set of documents that supports queries and modifiers. // 3 // 4 // Cursor: a specification for a particular subset of documents, w/ // 5 // a defined order, limit, and offset. creating a Cursor with LocalCollection.find(), // 6 // 7 // ObserveHandle: the return value of a live query. // 8 // 9 LocalCollection = function (name) { // 10 var self = this; // 11 self.name = name; // 12 // _id -> document (also containing id) // 13 self._docs = new LocalCollection._IdMap; // 14 // 15 self._observeQueue = new Meteor._SynchronousQueue(); // 16 // 17 self.next_qid = 1; // live query id generator // 18 // 19 // qid -> live query object. keys: // 20 // ordered: bool. ordered queries have addedBefore/movedBefore callbacks. // 21 // results: array (ordered) or object (unordered) of current results // 22 // (aliased with self._docs!) // 23 // resultsSnapshot: snapshot of results. null if not paused. // 24 // cursor: Cursor object for the query. // 25 // selector, sorter, (callbacks): functions // 26 self.queries = {}; // 27 // 28 // null if not saving originals; an IdMap from id to original document value if // 29 // saving originals. See comments before saveOriginals(). // 30 self._savedOriginals = null; // 31 // 32 // True when observers are paused and we should not send callbacks. // 33 self.paused = false; // 34 }; // 35 // 36 Minimongo = {}; // 37 // 38 // Object exported only for unit testing. // 39 // Use it to export private functions to test in Tinytest. // 40 MinimongoTest = {}; // 41 // 42 LocalCollection._applyChanges = function (doc, changeFields) { // 43 _.each(changeFields, function (value, key) { // 44 if (value === undefined) // 45 delete doc[key]; // 46 else // 47 doc[key] = value; // 48 }); // 49 }; // 50 // 51 MinimongoError = function (message) { // 52 var e = new Error(message); // 53 e.name = "MinimongoError"; // 54 return e; // 55 }; // 56 // 57 // 58 // options may include sort, skip, limit, reactive // 59 // sort may be any of these forms: // 60 // {a: 1, b: -1} // 61 // [["a", "asc"], ["b", "desc"]] // 62 // ["a", ["b", "desc"]] // 63 // (in the first form you're beholden to key enumeration order in // 64 // your javascript VM) // 65 // // 66 // reactive: if given, and false, don't register with Tracker (default // 67 // is true) // 68 // // 69 // XXX possibly should support retrieving a subset of fields? and // 70 // have it be a hint (ignored on the client, when not copying the // 71 // doc?) // 72 // // 73 // XXX sort does not yet support subkeys ('a.b') .. fix that! // 74 // XXX add one more sort form: "key" // 75 // XXX tests // 76 LocalCollection.prototype.find = function (selector, options) { // 77 // default syntax for everything is to omit the selector argument. // 78 // but if selector is explicitly passed in as false or undefined, we // 79 // want a selector that matches nothing. // 80 if (arguments.length === 0) // 81 selector = {}; // 82 // 83 return new LocalCollection.Cursor(this, selector, options); // 84 }; // 85 // 86 // don't call this ctor directly. use LocalCollection.find(). // 87 // 88 LocalCollection.Cursor = function (collection, selector, options) { // 89 var self = this; // 90 if (!options) options = {}; // 91 // 92 self.collection = collection; // 93 self.sorter = null; // 94 // 95 if (LocalCollection._selectorIsId(selector)) { // 96 // stash for fast path // 97 self._selectorId = selector; // 98 self.matcher = new Minimongo.Matcher(selector); // 99 } else { // 100 self._selectorId = undefined; // 101 self.matcher = new Minimongo.Matcher(selector); // 102 if (self.matcher.hasGeoQuery() || options.sort) { // 103 self.sorter = new Minimongo.Sorter(options.sort || [], // 104 { matcher: self.matcher }); // 105 } // 106 } // 107 self.skip = options.skip; // 108 self.limit = options.limit; // 109 self.fields = options.fields; // 110 // 111 self._projectionFn = LocalCollection._compileProjection(self.fields || {}); // 112 // 113 self._transform = LocalCollection.wrapTransform(options.transform); // 114 // 115 // by default, queries register w/ Tracker when it is available. // 116 if (typeof Tracker !== "undefined") // 117 self.reactive = (options.reactive === undefined) ? true : options.reactive; // 118 }; // 119 // 120 // Since we don't actually have a "nextObject" interface, there's really no // 121 // reason to have a "rewind" interface. All it did was make multiple calls // 122 // to fetch/map/forEach return nothing the second time. // 123 // XXX COMPAT WITH 0.8.1 // 124 LocalCollection.Cursor.prototype.rewind = function () { // 125 }; // 126 // 127 LocalCollection.prototype.findOne = function (selector, options) { // 128 if (arguments.length === 0) // 129 selector = {}; // 130 // 131 // NOTE: by setting limit 1 here, we end up using very inefficient // 132 // code that recomputes the whole query on each update. The upside is // 133 // that when you reactively depend on a findOne you only get // 134 // invalidated when the found object changes, not any object in the // 135 // collection. Most findOne will be by id, which has a fast path, so // 136 // this might not be a big deal. In most cases, invalidation causes // 137 // the called to re-query anyway, so this should be a net performance // 138 // improvement. // 139 options = options || {}; // 140 options.limit = 1; // 141 // 142 return this.find(selector, options).fetch()[0]; // 143 }; // 144 // 145 /** // 146 * @callback IterationCallback // 147 * @param {Object} doc // 148 * @param {Number} index // 149 */ // 150 /** // 151 * @summary Call `callback` once for each matching document, sequentially and synchronously. // 152 * @locus Anywhere // 153 * @method forEach // 154 * @instance // 155 * @memberOf Mongo.Cursor // 156 * @param {IterationCallback} callback Function to call. It will be called with three arguments: the document, a 0-based index, and cursor itself. * @param {Any} [thisArg] An object which will be the value of `this` inside `callback`. // 158 */ // 159 LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) { // 160 var self = this; // 161 // 162 var objects = self._getRawObjects({ordered: true}); // 163 // 164 if (self.reactive) { // 165 self._depend({ // 166 addedBefore: true, // 167 removed: true, // 168 changed: true, // 169 movedBefore: true}); // 170 } // 171 // 172 _.each(objects, function (elt, i) { // 173 // This doubles as a clone operation. // 174 elt = self._projectionFn(elt); // 175 // 176 if (self._transform) // 177 elt = self._transform(elt); // 178 callback.call(thisArg, elt, i, self); // 179 }); // 180 }; // 181 // 182 LocalCollection.Cursor.prototype.getTransform = function () { // 183 return this._transform; // 184 }; // 185 // 186 /** // 187 * @summary Map callback over all matching documents. Returns an Array. // 188 * @locus Anywhere // 189 * @method map // 190 * @instance // 191 * @memberOf Mongo.Cursor // 192 * @param {IterationCallback} callback Function to call. It will be called with three arguments: the document, a 0-based index, and cursor itself. * @param {Any} [thisArg] An object which will be the value of `this` inside `callback`. // 194 */ // 195 LocalCollection.Cursor.prototype.map = function (callback, thisArg) { // 196 var self = this; // 197 var res = []; // 198 self.forEach(function (doc, index) { // 199 res.push(callback.call(thisArg, doc, index, self)); // 200 }); // 201 return res; // 202 }; // 203 // 204 /** // 205 * @summary Return all matching documents as an Array. // 206 * @memberOf Mongo.Cursor // 207 * @method fetch // 208 * @instance // 209 * @locus Anywhere // 210 * @returns {Object[]} // 211 */ // 212 LocalCollection.Cursor.prototype.fetch = function () { // 213 var self = this; // 214 var res = []; // 215 self.forEach(function (doc) { // 216 res.push(doc); // 217 }); // 218 return res; // 219 }; // 220 // 221 /** // 222 * @summary Returns the number of documents that match a query. // 223 * @memberOf Mongo.Cursor // 224 * @method count // 225 * @instance // 226 * @locus Anywhere // 227 * @returns {Number} // 228 */ // 229 LocalCollection.Cursor.prototype.count = function () { // 230 var self = this; // 231 // 232 if (self.reactive) // 233 self._depend({added: true, removed: true}, // 234 true /* allow the observe to be unordered */); // 235 // 236 return self._getRawObjects({ordered: true}).length; // 237 }; // 238 // 239 LocalCollection.Cursor.prototype._publishCursor = function (sub) { // 240 var self = this; // 241 if (! self.collection.name) // 242 throw new Error("Can't publish a cursor from a collection without a name."); // 243 var collection = self.collection.name; // 244 // 245 // XXX minimongo should not depend on mongo-livedata! // 246 return Mongo.Collection._publishCursor(self, sub, collection); // 247 }; // 248 // 249 LocalCollection.Cursor.prototype._getCollectionName = function () { // 250 var self = this; // 251 return self.collection.name; // 252 }; // 253 // 254 LocalCollection._observeChangesCallbacksAreOrdered = function (callbacks) { // 255 if (callbacks.added && callbacks.addedBefore) // 256 throw new Error("Please specify only one of added() and addedBefore()"); // 257 return !!(callbacks.addedBefore || callbacks.movedBefore); // 258 }; // 259 // 260 LocalCollection._observeCallbacksAreOrdered = function (callbacks) { // 261 if (callbacks.addedAt && callbacks.added) // 262 throw new Error("Please specify only one of added() and addedAt()"); // 263 if (callbacks.changedAt && callbacks.changed) // 264 throw new Error("Please specify only one of changed() and changedAt()"); // 265 if (callbacks.removed && callbacks.removedAt) // 266 throw new Error("Please specify only one of removed() and removedAt()"); // 267 // 268 return !!(callbacks.addedAt || callbacks.movedTo || callbacks.changedAt // 269 || callbacks.removedAt); // 270 }; // 271 // 272 // the handle that comes back from observe. // 273 LocalCollection.ObserveHandle = function () {}; // 274 // 275 // options to contain: // 276 // * callbacks for observe(): // 277 // - addedAt (document, atIndex) // 278 // - added (document) // 279 // - changedAt (newDocument, oldDocument, atIndex) // 280 // - changed (newDocument, oldDocument) // 281 // - removedAt (document, atIndex) // 282 // - removed (document) // 283 // - movedTo (document, oldIndex, newIndex) // 284 // // 285 // attributes available on returned query handle: // 286 // * stop(): end updates // 287 // * collection: the collection this query is querying // 288 // // 289 // iff x is a returned query handle, (x instanceof // 290 // LocalCollection.ObserveHandle) is true // 291 // // 292 // initial results delivered through added callback // 293 // XXX maybe callbacks should take a list of objects, to expose transactions? // 294 // XXX maybe support field limiting (to limit what you're notified on) // 295 // 296 _.extend(LocalCollection.Cursor.prototype, { // 297 /** // 298 * @summary Watch a query. Receive callbacks as the result set changes. // 299 * @locus Anywhere // 300 * @memberOf Mongo.Cursor // 301 * @instance // 302 * @param {Object} callbacks Functions to call to deliver the result set as it changes // 303 */ // 304 observe: function (options) { // 305 var self = this; // 306 return LocalCollection._observeFromObserveChanges(self, options); // 307 }, // 308 // 309 /** // 310 * @summary Watch a query. Receive callbacks as the result set changes. Only the differences between the old and new documents are passed to the callbacks. * @locus Anywhere // 312 * @memberOf Mongo.Cursor // 313 * @instance // 314 * @param {Object} callbacks Functions to call to deliver the result set as it changes // 315 */ // 316 observeChanges: function (options) { // 317 var self = this; // 318 // 319 var ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); // 320 // 321 // there are several places that assume you aren't combining skip/limit with // 322 // unordered observe. eg, update's EJSON.clone, and the "there are several" // 323 // comment in _modifyAndNotify // 324 // XXX allow skip/limit with unordered observe // 325 if (!options._allow_unordered && !ordered && (self.skip || self.limit)) // 326 throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); // 327 // 328 if (self.fields && (self.fields._id === 0 || self.fields._id === false)) // 329 throw Error("You may not observe a cursor with {fields: {_id: 0}}"); // 330 // 331 var query = { // 332 matcher: self.matcher, // not fast pathed // 333 sorter: ordered && self.sorter, // 334 distances: ( // 335 self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), // 336 resultsSnapshot: null, // 337 ordered: ordered, // 338 cursor: self, // 339 projectionFn: self._projectionFn // 340 }; // 341 var qid; // 342 // 343 // Non-reactive queries call added[Before] and then never call anything // 344 // else. // 345 if (self.reactive) { // 346 qid = self.collection.next_qid++; // 347 self.collection.queries[qid] = query; // 348 } // 349 query.results = self._getRawObjects({ // 350 ordered: ordered, distances: query.distances}); // 351 if (self.collection.paused) // 352 query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); // 353 // 354 // wrap callbacks we were passed. callbacks only fire when not paused and // 355 // are never undefined // 356 // Filters out blacklisted fields according to cursor's projection. // 357 // XXX wrong place for this? // 358 // 359 // furthermore, callbacks enqueue until the operation we're working on is // 360 // done. // 361 var wrapCallback = function (f) { // 362 if (!f) // 363 return function () {}; // 364 return function (/*args*/) { // 365 var context = this; // 366 var args = arguments; // 367 // 368 if (self.collection.paused) // 369 return; // 370 // 371 self.collection._observeQueue.queueTask(function () { // 372 f.apply(context, args); // 373 }); // 374 }; // 375 }; // 376 query.added = wrapCallback(options.added); // 377 query.changed = wrapCallback(options.changed); // 378 query.removed = wrapCallback(options.removed); // 379 if (ordered) { // 380 query.addedBefore = wrapCallback(options.addedBefore); // 381 query.movedBefore = wrapCallback(options.movedBefore); // 382 } // 383 // 384 if (!options._suppress_initial && !self.collection.paused) { // 385 // XXX unify ordered and unordered interface // 386 var each = ordered // 387 ? _.bind(_.each, null, query.results) // 388 : _.bind(query.results.forEach, query.results); // 389 each(function (doc) { // 390 var fields = EJSON.clone(doc); // 391 // 392 delete fields._id; // 393 if (ordered) // 394 query.addedBefore(doc._id, self._projectionFn(fields), null); // 395 query.added(doc._id, self._projectionFn(fields)); // 396 }); // 397 } // 398 // 399 var handle = new LocalCollection.ObserveHandle; // 400 _.extend(handle, { // 401 collection: self.collection, // 402 stop: function () { // 403 if (self.reactive) // 404 delete self.collection.queries[qid]; // 405 } // 406 }); // 407 // 408 if (self.reactive && Tracker.active) { // 409 // XXX in many cases, the same observe will be recreated when // 410 // the current autorun is rerun. we could save work by // 411 // letting it linger across rerun and potentially get // 412 // repurposed if the same observe is performed, using logic // 413 // similar to that of Meteor.subscribe. // 414 Tracker.onInvalidate(function () { // 415 handle.stop(); // 416 }); // 417 } // 418 // run the observe callbacks resulting from the initial contents // 419 // before we leave the observe. // 420 self.collection._observeQueue.drain(); // 421 // 422 return handle; // 423 } // 424 }); // 425 // 426 // Returns a collection of matching objects, but doesn't deep copy them. // 427 // // 428 // If ordered is set, returns a sorted array, respecting sorter, skip, and limit // 429 // properties of the query. if sorter is falsey, no sort -- you get the natural // 430 // order. // 431 // // 432 // If ordered is not set, returns an object mapping from ID to doc (sorter, skip // 433 // and limit should not be set). // 434 // // 435 // If ordered is set and this cursor is a $near geoquery, then this function // 436 // will use an _IdMap to track each distance from the $near argument point in // 437 // order to use it as a sort key. If an _IdMap is passed in the 'distances' // 438 // argument, this function will clear it and use it for this purpose (otherwise // 439 // it will just create its own _IdMap). The observeChanges implementation uses // 440 // this to remember the distances after this function returns. // 441 LocalCollection.Cursor.prototype._getRawObjects = function (options) { // 442 var self = this; // 443 options = options || {}; // 444 // 445 // XXX use OrderedDict instead of array, and make IdMap and OrderedDict // 446 // compatible // 447 var results = options.ordered ? [] : new LocalCollection._IdMap; // 448 // 449 // fast path for single ID value // 450 if (self._selectorId !== undefined) { // 451 // If you have non-zero skip and ask for a single id, you get // 452 // nothing. This is so it matches the behavior of the '{_id: foo}' // 453 // path. // 454 if (self.skip) // 455 return results; // 456 // 457 var selectedDoc = self.collection._docs.get(self._selectorId); // 458 if (selectedDoc) { // 459 if (options.ordered) // 460 results.push(selectedDoc); // 461 else // 462 results.set(self._selectorId, selectedDoc); // 463 } // 464 return results; // 465 } // 466 // 467 // slow path for arbitrary selector, sort, skip, limit // 468 // 469 // in the observeChanges case, distances is actually part of the "query" (ie, // 470 // live results set) object. in other cases, distances is only used inside // 471 // this function. // 472 var distances; // 473 if (self.matcher.hasGeoQuery() && options.ordered) { // 474 if (options.distances) { // 475 distances = options.distances; // 476 distances.clear(); // 477 } else { // 478 distances = new LocalCollection._IdMap(); // 479 } // 480 } // 481 // 482 self.collection._docs.forEach(function (doc, id) { // 483 var matchResult = self.matcher.documentMatches(doc); // 484 if (matchResult.result) { // 485 if (options.ordered) { // 486 results.push(doc); // 487 if (distances && matchResult.distance !== undefined) // 488 distances.set(id, matchResult.distance); // 489 } else { // 490 results.set(id, doc); // 491 } // 492 } // 493 // Fast path for limited unsorted queries. // 494 // XXX 'length' check here seems wrong for ordered // 495 if (self.limit && !self.skip && !self.sorter && // 496 results.length === self.limit) // 497 return false; // break // 498 return true; // continue // 499 }); // 500 // 501 if (!options.ordered) // 502 return results; // 503 // 504 if (self.sorter) { // 505 var comparator = self.sorter.getComparator({distances: distances}); // 506 results.sort(comparator); // 507 } // 508 // 509 var idx_start = self.skip || 0; // 510 var idx_end = self.limit ? (self.limit + idx_start) : results.length; // 511 return results.slice(idx_start, idx_end); // 512 }; // 513 // 514 // XXX Maybe we need a version of observe that just calls a callback if // 515 // anything changed. // 516 LocalCollection.Cursor.prototype._depend = function (changers, _allow_unordered) { // 517 var self = this; // 518 // 519 if (Tracker.active) { // 520 var v = new Tracker.Dependency; // 521 v.depend(); // 522 var notifyChange = _.bind(v.changed, v); // 523 // 524 var options = { // 525 _suppress_initial: true, // 526 _allow_unordered: _allow_unordered // 527 }; // 528 _.each(['added', 'changed', 'removed', 'addedBefore', 'movedBefore'], // 529 function (fnName) { // 530 if (changers[fnName]) // 531 options[fnName] = notifyChange; // 532 }); // 533 // 534 // observeChanges will stop() when this computation is invalidated // 535 self.observeChanges(options); // 536 } // 537 }; // 538 // 539 // XXX enforce rule that field names can't start with '$' or contain '.' // 540 // (real mongodb does in fact enforce this) // 541 // XXX possibly enforce that 'undefined' does not appear (we assume // 542 // this in our handling of null and $exists) // 543 LocalCollection.prototype.insert = function (doc, callback) { // 544 var self = this; // 545 doc = EJSON.clone(doc); // 546 // 547 if (!_.has(doc, '_id')) { // 548 // if you really want to use ObjectIDs, set this global. // 549 // Mongo.Collection specifies its own ids and does not use this code. // 550 doc._id = LocalCollection._useOID ? new LocalCollection._ObjectID() // 551 : Random.id(); // 552 } // 553 var id = doc._id; // 554 // 555 if (self._docs.has(id)) // 556 throw MinimongoError("Duplicate _id '" + id + "'"); // 557 // 558 self._saveOriginal(id, undefined); // 559 self._docs.set(id, doc); // 560 // 561 var queriesToRecompute = []; // 562 // trigger live queries that match // 563 for (var qid in self.queries) { // 564 var query = self.queries[qid]; // 565 var matchResult = query.matcher.documentMatches(doc); // 566 if (matchResult.result) { // 567 if (query.distances && matchResult.distance !== undefined) // 568 query.distances.set(id, matchResult.distance); // 569 if (query.cursor.skip || query.cursor.limit) // 570 queriesToRecompute.push(qid); // 571 else // 572 LocalCollection._insertInResults(query, doc); // 573 } // 574 } // 575 // 576 _.each(queriesToRecompute, function (qid) { // 577 if (self.queries[qid]) // 578 self._recomputeResults(self.queries[qid]); // 579 }); // 580 self._observeQueue.drain(); // 581 // 582 // Defer because the caller likely doesn't expect the callback to be run // 583 // immediately. // 584 if (callback) // 585 Meteor.defer(function () { // 586 callback(null, id); // 587 }); // 588 return id; // 589 }; // 590 // 591 // Iterates over a subset of documents that could match selector; calls // 592 // f(doc, id) on each of them. Specifically, if selector specifies // 593 // specific _id's, it only looks at those. doc is *not* cloned: it is the // 594 // same object that is in _docs. // 595 LocalCollection.prototype._eachPossiblyMatchingDoc = function (selector, f) { // 596 var self = this; // 597 var specificIds = LocalCollection._idsMatchedBySelector(selector); // 598 if (specificIds) { // 599 for (var i = 0; i < specificIds.length; ++i) { // 600 var id = specificIds[i]; // 601 var doc = self._docs.get(id); // 602 if (doc) { // 603 var breakIfFalse = f(doc, id); // 604 if (breakIfFalse === false) // 605 break; // 606 } // 607 } // 608 } else { // 609 self._docs.forEach(f); // 610 } // 611 }; // 612 // 613 LocalCollection.prototype.remove = function (selector, callback) { // 614 var self = this; // 615 // 616 // Easy special case: if we're not calling observeChanges callbacks and we're // 617 // not saving originals and we got asked to remove everything, then just empty // 618 // everything directly. // 619 if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) { // 620 var result = self._docs.size(); // 621 self._docs.clear(); // 622 _.each(self.queries, function (query) { // 623 if (query.ordered) { // 624 query.results = []; // 625 } else { // 626 query.results.clear(); // 627 } // 628 }); // 629 if (callback) { // 630 Meteor.defer(function () { // 631 callback(null, result); // 632 }); // 633 } // 634 return result; // 635 } // 636 // 637 var matcher = new Minimongo.Matcher(selector); // 638 var remove = []; // 639 self._eachPossiblyMatchingDoc(selector, function (doc, id) { // 640 if (matcher.documentMatches(doc).result) // 641 remove.push(id); // 642 }); // 643 // 644 var queriesToRecompute = []; // 645 var queryRemove = []; // 646 for (var i = 0; i < remove.length; i++) { // 647 var removeId = remove[i]; // 648 var removeDoc = self._docs.get(removeId); // 649 _.each(self.queries, function (query, qid) { // 650 if (query.matcher.documentMatches(removeDoc).result) { // 651 if (query.cursor.skip || query.cursor.limit) // 652 queriesToRecompute.push(qid); // 653 else // 654 queryRemove.push({qid: qid, doc: removeDoc}); // 655 } // 656 }); // 657 self._saveOriginal(removeId, removeDoc); // 658 self._docs.remove(removeId); // 659 } // 660 // 661 // run live query callbacks _after_ we've removed the documents. // 662 _.each(queryRemove, function (remove) { // 663 var query = self.queries[remove.qid]; // 664 if (query) { // 665 query.distances && query.distances.remove(remove.doc._id); // 666 LocalCollection._removeFromResults(query, remove.doc); // 667 } // 668 }); // 669 _.each(queriesToRecompute, function (qid) { // 670 var query = self.queries[qid]; // 671 if (query) // 672 self._recomputeResults(query); // 673 }); // 674 self._observeQueue.drain(); // 675 result = remove.length; // 676 if (callback) // 677 Meteor.defer(function () { // 678 callback(null, result); // 679 }); // 680 return result; // 681 }; // 682 // 683 // XXX atomicity: if multi is true, and one modification fails, do // 684 // we rollback the whole operation, or what? // 685 LocalCollection.prototype.update = function (selector, mod, options, callback) { // 686 var self = this; // 687 if (! callback && options instanceof Function) { // 688 callback = options; // 689 options = null; // 690 } // 691 if (!options) options = {}; // 692 // 693 var matcher = new Minimongo.Matcher(selector); // 694 // 695 // Save the original results of any query that we might need to // 696 // _recomputeResults on, because _modifyAndNotify will mutate the objects in // 697 // it. (We don't need to save the original results of paused queries because // 698 // they already have a resultsSnapshot and we won't be diffing in // 699 // _recomputeResults.) // 700 var qidToOriginalResults = {}; // 701 _.each(self.queries, function (query, qid) { // 702 // XXX for now, skip/limit implies ordered observe, so query.results is // 703 // always an array // 704 if ((query.cursor.skip || query.cursor.limit) && ! self.paused) // 705 qidToOriginalResults[qid] = EJSON.clone(query.results); // 706 }); // 707 var recomputeQids = {}; // 708 // 709 var updateCount = 0; // 710 // 711 self._eachPossiblyMatchingDoc(selector, function (doc, id) { // 712 var queryResult = matcher.documentMatches(doc); // 713 if (queryResult.result) { // 714 // XXX Should we save the original even if mod ends up being a no-op? // 715 self._saveOriginal(id, doc); // 716 self._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices); // 717 ++updateCount; // 718 if (!options.multi) // 719 return false; // break // 720 } // 721 return true; // 722 }); // 723 // 724 _.each(recomputeQids, function (dummy, qid) { // 725 var query = self.queries[qid]; // 726 if (query) // 727 self._recomputeResults(query, qidToOriginalResults[qid]); // 728 }); // 729 self._observeQueue.drain(); // 730 // 731 // If we are doing an upsert, and we didn't modify any documents yet, then // 732 // it's time to do an insert. Figure out what document we are inserting, and // 733 // generate an id for it. // 734 var insertedId; // 735 if (updateCount === 0 && options.upsert) { // 736 var newDoc = LocalCollection._removeDollarOperators(selector); // 737 LocalCollection._modify(newDoc, mod, {isInsert: true}); // 738 if (! newDoc._id && options.insertedId) // 739 newDoc._id = options.insertedId; // 740 insertedId = self.insert(newDoc); // 741 updateCount = 1; // 742 } // 743 // 744 // Return the number of affected documents, or in the upsert case, an object // 745 // containing the number of affected docs and the id of the doc that was // 746 // inserted, if any. // 747 var result; // 748 if (options._returnObject) { // 749 result = { // 750 numberAffected: updateCount // 751 }; // 752 if (insertedId !== undefined) // 753 result.insertedId = insertedId; // 754 } else { // 755 result = updateCount; // 756 } // 757 // 758 if (callback) // 759 Meteor.defer(function () { // 760 callback(null, result); // 761 }); // 762 return result; // 763 }; // 764 // 765 // A convenience wrapper on update. LocalCollection.upsert(sel, mod) is // 766 // equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject: // 767 // true }). // 768 LocalCollection.prototype.upsert = function (selector, mod, options, callback) { // 769 var self = this; // 770 if (! callback && typeof options === "function") { // 771 callback = options; // 772 options = {}; // 773 } // 774 return self.update(selector, mod, _.extend({}, options, { // 775 upsert: true, // 776 _returnObject: true // 777 }), callback); // 778 }; // 779 // 780 LocalCollection.prototype._modifyAndNotify = function ( // 781 doc, mod, recomputeQids, arrayIndices) { // 782 var self = this; // 783 // 784 var matched_before = {}; // 785 for (var qid in self.queries) { // 786 var query = self.queries[qid]; // 787 if (query.ordered) { // 788 matched_before[qid] = query.matcher.documentMatches(doc).result; // 789 } else { // 790 // Because we don't support skip or limit (yet) in unordered queries, we // 791 // can just do a direct lookup. // 792 matched_before[qid] = query.results.has(doc._id); // 793 } // 794 } // 795 // 796 var old_doc = EJSON.clone(doc); // 797 // 798 LocalCollection._modify(doc, mod, {arrayIndices: arrayIndices}); // 799 // 800 for (qid in self.queries) { // 801 query = self.queries[qid]; // 802 var before = matched_before[qid]; // 803 var afterMatch = query.matcher.documentMatches(doc); // 804 var after = afterMatch.result; // 805 if (after && query.distances && afterMatch.distance !== undefined) // 806 query.distances.set(doc._id, afterMatch.distance); // 807 // 808 if (query.cursor.skip || query.cursor.limit) { // 809 // We need to recompute any query where the doc may have been in the // 810 // cursor's window either before or after the update. (Note that if skip // 811 // or limit is set, "before" and "after" being true do not necessarily // 812 // mean that the document is in the cursor's output after skip/limit is // 813 // applied... but if they are false, then the document definitely is NOT // 814 // in the output. So it's safe to skip recompute if neither before or // 815 // after are true.) // 816 if (before || after) // 817 recomputeQids[qid] = true; // 818 } else if (before && !after) { // 819 LocalCollection._removeFromResults(query, doc); // 820 } else if (!before && after) { // 821 LocalCollection._insertInResults(query, doc); // 822 } else if (before && after) { // 823 LocalCollection._updateInResults(query, doc, old_doc); // 824 } // 825 } // 826 }; // 827 // 828 // XXX the sorted-query logic below is laughably inefficient. we'll // 829 // need to come up with a better datastructure for this. // 830 // // 831 // XXX the logic for observing with a skip or a limit is even more // 832 // laughably inefficient. we recompute the whole results every time! // 833 // 834 LocalCollection._insertInResults = function (query, doc) { // 835 var fields = EJSON.clone(doc); // 836 delete fields._id; // 837 if (query.ordered) { // 838 if (!query.sorter) { // 839 query.addedBefore(doc._id, query.projectionFn(fields), null); // 840 query.results.push(doc); // 841 } else { // 842 var i = LocalCollection._insertInSortedList( // 843 query.sorter.getComparator({distances: query.distances}), // 844 query.results, doc); // 845 var next = query.results[i+1]; // 846 if (next) // 847 next = next._id; // 848 else // 849 next = null; // 850 query.addedBefore(doc._id, query.projectionFn(fields), next); // 851 } // 852 query.added(doc._id, query.projectionFn(fields)); // 853 } else { // 854 query.added(doc._id, query.projectionFn(fields)); // 855 query.results.set(doc._id, doc); // 856 } // 857 }; // 858 // 859 LocalCollection._removeFromResults = function (query, doc) { // 860 if (query.ordered) { // 861 var i = LocalCollection._findInOrderedResults(query, doc); // 862 query.removed(doc._id); // 863 query.results.splice(i, 1); // 864 } else { // 865 var id = doc._id; // in case callback mutates doc // 866 query.removed(doc._id); // 867 query.results.remove(id); // 868 } // 869 }; // 870 // 871 LocalCollection._updateInResults = function (query, doc, old_doc) { // 872 if (!EJSON.equals(doc._id, old_doc._id)) // 873 throw new Error("Can't change a doc's _id while updating"); // 874 var projectionFn = query.projectionFn; // 875 var changedFields = LocalCollection._makeChangedFields( // 876 projectionFn(doc), projectionFn(old_doc)); // 877 // 878 if (!query.ordered) { // 879 if (!_.isEmpty(changedFields)) { // 880 query.changed(doc._id, changedFields); // 881 query.results.set(doc._id, doc); // 882 } // 883 return; // 884 } // 885 // 886 var orig_idx = LocalCollection._findInOrderedResults(query, doc); // 887 // 888 if (!_.isEmpty(changedFields)) // 889 query.changed(doc._id, changedFields); // 890 if (!query.sorter) // 891 return; // 892 // 893 // just take it out and put it back in again, and see if the index // 894 // changes // 895 query.results.splice(orig_idx, 1); // 896 var new_idx = LocalCollection._insertInSortedList( // 897 query.sorter.getComparator({distances: query.distances}), // 898 query.results, doc); // 899 if (orig_idx !== new_idx) { // 900 var next = query.results[new_idx+1]; // 901 if (next) // 902 next = next._id; // 903 else // 904 next = null; // 905 query.movedBefore && query.movedBefore(doc._id, next); // 906 } // 907 }; // 908 // 909 // Recomputes the results of a query and runs observe callbacks for the // 910 // difference between the previous results and the current results (unless // 911 // paused). Used for skip/limit queries. // 912 // // 913 // When this is used by insert or remove, it can just use query.results for the // 914 // old results (and there's no need to pass in oldResults), because these // 915 // operations don't mutate the documents in the collection. Update needs to pass // 916 // in an oldResults which was deep-copied before the modifier was applied. // 917 // // 918 // oldResults is guaranteed to be ignored if the query is not paused. // 919 LocalCollection.prototype._recomputeResults = function (query, oldResults) { // 920 var self = this; // 921 if (! self.paused && ! oldResults) // 922 oldResults = query.results; // 923 if (query.distances) // 924 query.distances.clear(); // 925 query.results = query.cursor._getRawObjects({ // 926 ordered: query.ordered, distances: query.distances}); // 927 // 928 if (! self.paused) { // 929 LocalCollection._diffQueryChanges( // 930 query.ordered, oldResults, query.results, query, // 931 { projectionFn: query.projectionFn }); // 932 } // 933 }; // 934 // 935 // 936 LocalCollection._findInOrderedResults = function (query, doc) { // 937 if (!query.ordered) // 938 throw new Error("Can't call _findInOrderedResults on unordered query"); // 939 for (var i = 0; i < query.results.length; i++) // 940 if (query.results[i] === doc) // 941 return i; // 942 throw Error("object missing from query"); // 943 }; // 944 // 945 // This binary search puts a value between any equal values, and the first // 946 // lesser value. // 947 LocalCollection._binarySearch = function (cmp, array, value) { // 948 var first = 0, rangeLength = array.length; // 949 // 950 while (rangeLength > 0) { // 951 var halfRange = Math.floor(rangeLength/2); // 952 if (cmp(value, array[first + halfRange]) >= 0) { // 953 first += halfRange + 1; // 954 rangeLength -= halfRange + 1; // 955 } else { // 956 rangeLength = halfRange; // 957 } // 958 } // 959 return first; // 960 }; // 961 // 962 LocalCollection._insertInSortedList = function (cmp, array, value) { // 963 if (array.length === 0) { // 964 array.push(value); // 965 return 0; // 966 } // 967 // 968 var idx = LocalCollection._binarySearch(cmp, array, value); // 969 array.splice(idx, 0, value); // 970 return idx; // 971 }; // 972 // 973 // To track what documents are affected by a piece of code, call saveOriginals() // 974 // before it and retrieveOriginals() after it. retrieveOriginals returns an // 975 // object whose keys are the ids of the documents that were affected since the // 976 // call to saveOriginals(), and the values are equal to the document's contents // 977 // at the time of saveOriginals. (In the case of an inserted document, undefined // 978 // is the value.) You must alternate between calls to saveOriginals() and // 979 // retrieveOriginals(). // 980 LocalCollection.prototype.saveOriginals = function () { // 981 var self = this; // 982 if (self._savedOriginals) // 983 throw new Error("Called saveOriginals twice without retrieveOriginals"); // 984 self._savedOriginals = new LocalCollection._IdMap; // 985 }; // 986 LocalCollection.prototype.retrieveOriginals = function () { // 987 var self = this; // 988 if (!self._savedOriginals) // 989 throw new Error("Called retrieveOriginals without saveOriginals"); // 990 // 991 var originals = self._savedOriginals; // 992 self._savedOriginals = null; // 993 return originals; // 994 }; // 995 // 996 LocalCollection.prototype._saveOriginal = function (id, doc) { // 997 var self = this; // 998 // Are we even trying to save originals? // 999 if (!self._savedOriginals) // 1000 return; // 1001 // Have we previously mutated the original (and so 'doc' is not actually // 1002 // original)? (Note the 'has' check rather than truth: we store undefined // 1003 // here for inserted docs!) // 1004 if (self._savedOriginals.has(id)) // 1005 return; // 1006 self._savedOriginals.set(id, EJSON.clone(doc)); // 1007 }; // 1008 // 1009 // Pause the observers. No callbacks from observers will fire until // 1010 // 'resumeObservers' is called. // 1011 LocalCollection.prototype.pauseObservers = function () { // 1012 // No-op if already paused. // 1013 if (this.paused) // 1014 return; // 1015 // 1016 // Set the 'paused' flag such that new observer messages don't fire. // 1017 this.paused = true; // 1018 // 1019 // Take a snapshot of the query results for each query. // 1020 for (var qid in this.queries) { // 1021 var query = this.queries[qid]; // 1022 // 1023 query.resultsSnapshot = EJSON.clone(query.results); // 1024 } // 1025 }; // 1026 // 1027 // Resume the observers. Observers immediately receive change // 1028 // notifications to bring them to the current state of the // 1029 // database. Note that this is not just replaying all the changes that // 1030 // happened during the pause, it is a smarter 'coalesced' diff. // 1031 LocalCollection.prototype.resumeObservers = function () { // 1032 var self = this; // 1033 // No-op if not paused. // 1034 if (!this.paused) // 1035 return; // 1036 // 1037 // Unset the 'paused' flag. Make sure to do this first, otherwise // 1038 // observer methods won't actually fire when we trigger them. // 1039 this.paused = false; // 1040 // 1041 for (var qid in this.queries) { // 1042 var query = self.queries[qid]; // 1043 // Diff the current results against the snapshot and send to observers. // 1044 // pass the query object for its observer callbacks. // 1045 LocalCollection._diffQueryChanges( // 1046 query.ordered, query.resultsSnapshot, query.results, query, // 1047 { projectionFn: query.projectionFn }); // 1048 query.resultsSnapshot = null; // 1049 } // 1050 self._observeQueue.drain(); // 1051 }; // 1052 // 1053 // 1054 // NB: used by livedata // 1055 LocalCollection._idStringify = function (id) { // 1056 if (id instanceof LocalCollection._ObjectID) { // 1057 return id.valueOf(); // 1058 } else if (typeof id === 'string') { // 1059 if (id === "") { // 1060 return id; // 1061 } else if (id.substr(0, 1) === "-" || // escape previously dashed strings // 1062 id.substr(0, 1) === "~" || // escape escaped numbers, true, false // 1063 LocalCollection._looksLikeObjectID(id) || // escape object-id-form strings // 1064 id.substr(0, 1) === '{') { // escape object-form strings, for maybe implementing later // 1065 return "-" + id; // 1066 } else { // 1067 return id; // other strings go through unchanged. // 1068 } // 1069 } else if (id === undefined) { // 1070 return '-'; // 1071 } else if (typeof id === 'object' && id !== null) { // 1072 throw new Error("Meteor does not currently support objects other than ObjectID as ids"); // 1073 } else { // Numbers, true, false, null // 1074 return "~" + JSON.stringify(id); // 1075 } // 1076 }; // 1077 // 1078 // 1079 // NB: used by livedata // 1080 LocalCollection._idParse = function (id) { // 1081 if (id === "") { // 1082 return id; // 1083 } else if (id === '-') { // 1084 return undefined; // 1085 } else if (id.substr(0, 1) === '-') { // 1086 return id.substr(1); // 1087 } else if (id.substr(0, 1) === '~') { // 1088 return JSON.parse(id.substr(1)); // 1089 } else if (LocalCollection._looksLikeObjectID(id)) { // 1090 return new LocalCollection._ObjectID(id); // 1091 } else { // 1092 return id; // 1093 } // 1094 }; // 1095 // 1096 LocalCollection._makeChangedFields = function (newDoc, oldDoc) { // 1097 var fields = {}; // 1098 LocalCollection._diffObjects(oldDoc, newDoc, { // 1099 leftOnly: function (key, value) { // 1100 fields[key] = undefined; // 1101 }, // 1102 rightOnly: function (key, value) { // 1103 fields[key] = value; // 1104 }, // 1105 both: function (key, leftValue, rightValue) { // 1106 if (!EJSON.equals(leftValue, rightValue)) // 1107 fields[key] = rightValue; // 1108 } // 1109 }); // 1110 return fields; // 1111 }; // 1112 // 1113 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/wrap_transform.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Wrap a transform function to return objects that have the _id field // 1 // of the untransformed document. This ensures that subsystems such as // 2 // the observe-sequence package that call `observe` can keep track of // 3 // the documents identities. // 4 // // 5 // - Require that it returns objects // 6 // - If the return value has an _id field, verify that it matches the // 7 // original _id field // 8 // - If the return value doesn't have an _id field, add it back. // 9 LocalCollection.wrapTransform = function (transform) { // 10 if (! transform) // 11 return null; // 12 // 13 // No need to doubly-wrap transforms. // 14 if (transform.__wrappedTransform__) // 15 return transform; // 16 // 17 var wrapped = function (doc) { // 18 if (!_.has(doc, '_id')) { // 19 // XXX do we ever have a transform on the oplog's collection? because that // 20 // collection has no _id. // 21 throw new Error("can only transform documents with _id"); // 22 } // 23 // 24 var id = doc._id; // 25 // XXX consider making tracker a weak dependency and checking Package.tracker here // 26 var transformed = Tracker.nonreactive(function () { // 27 return transform(doc); // 28 }); // 29 // 30 if (!isPlainObject(transformed)) { // 31 throw new Error("transform must return object"); // 32 } // 33 // 34 if (_.has(transformed, '_id')) { // 35 if (!EJSON.equals(transformed._id, id)) { // 36 throw new Error("transformed document can't have different _id"); // 37 } // 38 } else { // 39 transformed._id = id; // 40 } // 41 return transformed; // 42 }; // 43 wrapped.__wrappedTransform__ = true; // 44 return wrapped; // 45 }; // 46 // 47 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/helpers.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Like _.isArray, but doesn't regard polyfilled Uint8Arrays on old browsers as // 1 // arrays. // 2 // XXX maybe this should be EJSON.isArray // 3 isArray = function (x) { // 4 return _.isArray(x) && !EJSON.isBinary(x); // 5 }; // 6 // 7 // XXX maybe this should be EJSON.isObject, though EJSON doesn't know about // 8 // RegExp // 9 // XXX note that _type(undefined) === 3!!!! // 10 isPlainObject = LocalCollection._isPlainObject = function (x) { // 11 return x && LocalCollection._f._type(x) === 3; // 12 }; // 13 // 14 isIndexable = function (x) { // 15 return isArray(x) || isPlainObject(x); // 16 }; // 17 // 18 // Returns true if this is an object with at least one key and all keys begin // 19 // with $. Unless inconsistentOK is set, throws if some keys begin with $ and // 20 // others don't. // 21 isOperatorObject = function (valueSelector, inconsistentOK) { // 22 if (!isPlainObject(valueSelector)) // 23 return false; // 24 // 25 var theseAreOperators = undefined; // 26 _.each(valueSelector, function (value, selKey) { // 27 var thisIsOperator = selKey.substr(0, 1) === '$'; // 28 if (theseAreOperators === undefined) { // 29 theseAreOperators = thisIsOperator; // 30 } else if (theseAreOperators !== thisIsOperator) { // 31 if (!inconsistentOK) // 32 throw new Error("Inconsistent operator: " + // 33 JSON.stringify(valueSelector)); // 34 theseAreOperators = false; // 35 } // 36 }); // 37 return !!theseAreOperators; // {} has no operators // 38 }; // 39 // 40 // 41 // string can be converted to integer // 42 isNumericKey = function (s) { // 43 return /^[0-9]+$/.test(s); // 44 }; // 45 // 46 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/selector.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // The minimongo selector compiler! // 1 // 2 // Terminology: // 3 // - a "selector" is the EJSON object representing a selector // 4 // - a "matcher" is its compiled form (whether a full Minimongo.Matcher // 5 // object or one of the component lambdas that matches parts of it) // 6 // - a "result object" is an object with a "result" field and maybe // 7 // distance and arrayIndices. // 8 // - a "branched value" is an object with a "value" field and maybe // 9 // "dontIterate" and "arrayIndices". // 10 // - a "document" is a top-level object that can be stored in a collection. // 11 // - a "lookup function" is a function that takes in a document and returns // 12 // an array of "branched values". // 13 // - a "branched matcher" maps from an array of branched values to a result // 14 // object. // 15 // - an "element matcher" maps from a single value to a bool. // 16 // 17 // Main entry point. // 18 // var matcher = new Minimongo.Matcher({a: {$gt: 5}}); // 19 // if (matcher.documentMatches({a: 7})) ... // 20 Minimongo.Matcher = function (selector) { // 21 var self = this; // 22 // A set (object mapping string -> *) of all of the document paths looked // 23 // at by the selector. Also includes the empty string if it may look at any // 24 // path (eg, $where). // 25 self._paths = {}; // 26 // Set to true if compilation finds a $near. // 27 self._hasGeoQuery = false; // 28 // Set to true if compilation finds a $where. // 29 self._hasWhere = false; // 30 // Set to false if compilation finds anything other than a simple equality or // 31 // one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used with // 32 // scalars as operands. // 33 self._isSimple = true; // 34 // Set to a dummy document which always matches this Matcher. Or set to null // 35 // if such document is too hard to find. // 36 self._matchingDocument = undefined; // 37 // A clone of the original selector. It may just be a function if the user // 38 // passed in a function; otherwise is definitely an object (eg, IDs are // 39 // translated into {_id: ID} first. Used by canBecomeTrueByModifier and // 40 // Sorter._useWithMatcher. // 41 self._selector = null; // 42 self._docMatcher = self._compileSelector(selector); // 43 }; // 44 // 45 _.extend(Minimongo.Matcher.prototype, { // 46 documentMatches: function (doc) { // 47 if (!doc || typeof doc !== "object") { // 48 throw Error("documentMatches needs a document"); // 49 } // 50 return this._docMatcher(doc); // 51 }, // 52 hasGeoQuery: function () { // 53 return this._hasGeoQuery; // 54 }, // 55 hasWhere: function () { // 56 return this._hasWhere; // 57 }, // 58 isSimple: function () { // 59 return this._isSimple; // 60 }, // 61 // 62 // Given a selector, return a function that takes one argument, a // 63 // document. It returns a result object. // 64 _compileSelector: function (selector) { // 65 var self = this; // 66 // you can pass a literal function instead of a selector // 67 if (selector instanceof Function) { // 68 self._isSimple = false; // 69 self._selector = selector; // 70 self._recordPathUsed(''); // 71 return function (doc) { // 72 return {result: !!selector.call(doc)}; // 73 }; // 74 } // 75 // 76 // shorthand -- scalars match _id // 77 if (LocalCollection._selectorIsId(selector)) { // 78 self._selector = {_id: selector}; // 79 self._recordPathUsed('_id'); // 80 return function (doc) { // 81 return {result: EJSON.equals(doc._id, selector)}; // 82 }; // 83 } // 84 // 85 // protect against dangerous selectors. falsey and {_id: falsey} are both // 86 // likely programmer error, and not what you want, particularly for // 87 // destructive operations. // 88 if (!selector || (('_id' in selector) && !selector._id)) { // 89 self._isSimple = false; // 90 return nothingMatcher; // 91 } // 92 // 93 // Top level can't be an array or true or binary. // 94 if (typeof(selector) === 'boolean' || isArray(selector) || // 95 EJSON.isBinary(selector)) // 96 throw new Error("Invalid selector: " + selector); // 97 // 98 self._selector = EJSON.clone(selector); // 99 return compileDocumentSelector(selector, self, {isRoot: true}); // 100 }, // 101 _recordPathUsed: function (path) { // 102 this._paths[path] = true; // 103 }, // 104 // Returns a list of key paths the given selector is looking for. It includes // 105 // the empty string if there is a $where. // 106 _getPaths: function () { // 107 return _.keys(this._paths); // 108 } // 109 }); // 110 // 111 // 112 // Takes in a selector that could match a full document (eg, the original // 113 // selector). Returns a function mapping document->result object. // 114 // // 115 // matcher is the Matcher object we are compiling. // 116 // // 117 // If this is the root document selector (ie, not wrapped in $and or the like), // 118 // then isRoot is true. (This is used by $near.) // 119 var compileDocumentSelector = function (docSelector, matcher, options) { // 120 options = options || {}; // 121 var docMatchers = []; // 122 _.each(docSelector, function (subSelector, key) { // 123 if (key.substr(0, 1) === '$') { // 124 // Outer operators are either logical operators (they recurse back into // 125 // this function), or $where. // 126 if (!_.has(LOGICAL_OPERATORS, key)) // 127 throw new Error("Unrecognized logical operator: " + key); // 128 matcher._isSimple = false; // 129 docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, // 130 options.inElemMatch)); // 131 } else { // 132 // Record this path, but only if we aren't in an elemMatcher, since in an // 133 // elemMatch this is a path inside an object in an array, not in the doc // 134 // root. // 135 if (!options.inElemMatch) // 136 matcher._recordPathUsed(key); // 137 var lookUpByIndex = makeLookupFunction(key); // 138 var valueMatcher = // 139 compileValueSelector(subSelector, matcher, options.isRoot); // 140 docMatchers.push(function (doc) { // 141 var branchValues = lookUpByIndex(doc); // 142 return valueMatcher(branchValues); // 143 }); // 144 } // 145 }); // 146 // 147 return andDocumentMatchers(docMatchers); // 148 }; // 149 // 150 // Takes in a selector that could match a key-indexed value in a document; eg, // 151 // {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to // 152 // indicate equality). Returns a branched matcher: a function mapping // 153 // [branched value]->result object. // 154 var compileValueSelector = function (valueSelector, matcher, isRoot) { // 155 if (valueSelector instanceof RegExp) { // 156 matcher._isSimple = false; // 157 return convertElementMatcherToBranchedMatcher( // 158 regexpElementMatcher(valueSelector)); // 159 } else if (isOperatorObject(valueSelector)) { // 160 return operatorBranchedMatcher(valueSelector, matcher, isRoot); // 161 } else { // 162 return convertElementMatcherToBranchedMatcher( // 163 equalityElementMatcher(valueSelector)); // 164 } // 165 }; // 166 // 167 // Given an element matcher (which evaluates a single value), returns a branched // 168 // value (which evaluates the element matcher on all the branches and returns a // 169 // more structured return value possibly including arrayIndices). // 170 var convertElementMatcherToBranchedMatcher = function ( // 171 elementMatcher, options) { // 172 options = options || {}; // 173 return function (branches) { // 174 var expanded = branches; // 175 if (!options.dontExpandLeafArrays) { // 176 expanded = expandArraysInBranches( // 177 branches, options.dontIncludeLeafArrays); // 178 } // 179 var ret = {}; // 180 ret.result = _.any(expanded, function (element) { // 181 var matched = elementMatcher(element.value); // 182 // 183 // Special case for $elemMatch: it means "true, and use this as an array // 184 // index if I didn't already have one". // 185 if (typeof matched === 'number') { // 186 // XXX This code dates from when we only stored a single array index // 187 // (for the outermost array). Should we be also including deeper array // 188 // indices from the $elemMatch match? // 189 if (!element.arrayIndices) // 190 element.arrayIndices = [matched]; // 191 matched = true; // 192 } // 193 // 194 // If some element matched, and it's tagged with array indices, include // 195 // those indices in our result object. // 196 if (matched && element.arrayIndices) // 197 ret.arrayIndices = element.arrayIndices; // 198 // 199 return matched; // 200 }); // 201 return ret; // 202 }; // 203 }; // 204 // 205 // Takes a RegExp object and returns an element matcher. // 206 regexpElementMatcher = function (regexp) { // 207 return function (value) { // 208 if (value instanceof RegExp) { // 209 // Comparing two regexps means seeing if the regexps are identical // 210 // (really!). Underscore knows how. // 211 return _.isEqual(value, regexp); // 212 } // 213 // Regexps only work against strings. // 214 if (typeof value !== 'string') // 215 return false; // 216 // 217 // Reset regexp's state to avoid inconsistent matching for objects with the // 218 // same value on consecutive calls of regexp.test. This happens only if the // 219 // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for // 220 // which we should *not* change the lastIndex but MongoDB doesn't support // 221 // either of these flags. // 222 regexp.lastIndex = 0; // 223 // 224 return regexp.test(value); // 225 }; // 226 }; // 227 // 228 // Takes something that is not an operator object and returns an element matcher // 229 // for equality with that thing. // 230 equalityElementMatcher = function (elementSelector) { // 231 if (isOperatorObject(elementSelector)) // 232 throw Error("Can't create equalityValueSelector for operator object"); // 233 // 234 // Special-case: null and undefined are equal (if you got undefined in there // 235 // somewhere, or if you got it due to some branch being non-existent in the // 236 // weird special case), even though they aren't with EJSON.equals. // 237 if (elementSelector == null) { // undefined or null // 238 return function (value) { // 239 return value == null; // undefined or null // 240 }; // 241 } // 242 // 243 return function (value) { // 244 return LocalCollection._f._equal(elementSelector, value); // 245 }; // 246 }; // 247 // 248 // Takes an operator object (an object with $ keys) and returns a branched // 249 // matcher for it. // 250 var operatorBranchedMatcher = function (valueSelector, matcher, isRoot) { // 251 // Each valueSelector works separately on the various branches. So one // 252 // operator can match one branch and another can match another branch. This // 253 // is OK. // 254 // 255 var operatorMatchers = []; // 256 _.each(valueSelector, function (operand, operator) { // 257 // XXX we should actually implement $eq, which is new in 2.6 // 258 var simpleRange = _.contains(['$lt', '$lte', '$gt', '$gte'], operator) && // 259 _.isNumber(operand); // 260 var simpleInequality = operator === '$ne' && !_.isObject(operand); // 261 var simpleInclusion = _.contains(['$in', '$nin'], operator) && // 262 _.isArray(operand) && !_.any(operand, _.isObject); // 263 // 264 if (! (operator === '$eq' || simpleRange || // 265 simpleInclusion || simpleInequality)) { // 266 matcher._isSimple = false; // 267 } // 268 // 269 if (_.has(VALUE_OPERATORS, operator)) { // 270 operatorMatchers.push( // 271 VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); // 272 } else if (_.has(ELEMENT_OPERATORS, operator)) { // 273 var options = ELEMENT_OPERATORS[operator]; // 274 operatorMatchers.push( // 275 convertElementMatcherToBranchedMatcher( // 276 options.compileElementSelector( // 277 operand, valueSelector, matcher), // 278 options)); // 279 } else { // 280 throw new Error("Unrecognized operator: " + operator); // 281 } // 282 }); // 283 // 284 return andBranchedMatchers(operatorMatchers); // 285 }; // 286 // 287 var compileArrayOfDocumentSelectors = function ( // 288 selectors, matcher, inElemMatch) { // 289 if (!isArray(selectors) || _.isEmpty(selectors)) // 290 throw Error("$and/$or/$nor must be nonempty array"); // 291 return _.map(selectors, function (subSelector) { // 292 if (!isPlainObject(subSelector)) // 293 throw Error("$or/$and/$nor entries need to be full objects"); // 294 return compileDocumentSelector( // 295 subSelector, matcher, {inElemMatch: inElemMatch}); // 296 }); // 297 }; // 298 // 299 // Operators that appear at the top level of a document selector. // 300 var LOGICAL_OPERATORS = { // 301 $and: function (subSelector, matcher, inElemMatch) { // 302 var matchers = compileArrayOfDocumentSelectors( // 303 subSelector, matcher, inElemMatch); // 304 return andDocumentMatchers(matchers); // 305 }, // 306 // 307 $or: function (subSelector, matcher, inElemMatch) { // 308 var matchers = compileArrayOfDocumentSelectors( // 309 subSelector, matcher, inElemMatch); // 310 // 311 // Special case: if there is only one matcher, use it directly, *preserving* // 312 // any arrayIndices it returns. // 313 if (matchers.length === 1) // 314 return matchers[0]; // 315 // 316 return function (doc) { // 317 var result = _.any(matchers, function (f) { // 318 return f(doc).result; // 319 }); // 320 // $or does NOT set arrayIndices when it has multiple // 321 // sub-expressions. (Tested against MongoDB.) // 322 return {result: result}; // 323 }; // 324 }, // 325 // 326 $nor: function (subSelector, matcher, inElemMatch) { // 327 var matchers = compileArrayOfDocumentSelectors( // 328 subSelector, matcher, inElemMatch); // 329 return function (doc) { // 330 var result = _.all(matchers, function (f) { // 331 return !f(doc).result; // 332 }); // 333 // Never set arrayIndices, because we only match if nothing in particular // 334 // "matched" (and because this is consistent with MongoDB). // 335 return {result: result}; // 336 }; // 337 }, // 338 // 339 $where: function (selectorValue, matcher) { // 340 // Record that *any* path may be used. // 341 matcher._recordPathUsed(''); // 342 matcher._hasWhere = true; // 343 if (!(selectorValue instanceof Function)) { // 344 // XXX MongoDB seems to have more complex logic to decide where or or not // 345 // to add "return"; not sure exactly what it is. // 346 selectorValue = Function("obj", "return " + selectorValue); // 347 } // 348 return function (doc) { // 349 // We make the document available as both `this` and `obj`. // 350 // XXX not sure what we should do if this throws // 351 return {result: selectorValue.call(doc, doc)}; // 352 }; // 353 }, // 354 // 355 // This is just used as a comment in the query (in MongoDB, it also ends up in // 356 // query logs); it has no effect on the actual selection. // 357 $comment: function () { // 358 return function () { // 359 return {result: true}; // 360 }; // 361 } // 362 }; // 363 // 364 // Returns a branched matcher that matches iff the given matcher does not. // 365 // Note that this implicitly "deMorganizes" the wrapped function. ie, it // 366 // means that ALL branch values need to fail to match innerBranchedMatcher. // 367 var invertBranchedMatcher = function (branchedMatcher) { // 368 return function (branchValues) { // 369 var invertMe = branchedMatcher(branchValues); // 370 // We explicitly choose to strip arrayIndices here: it doesn't make sense to // 371 // say "update the array element that does not match something", at least // 372 // in mongo-land. // 373 return {result: !invertMe.result}; // 374 }; // 375 }; // 376 // 377 // Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a // 378 // document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as // 379 // "match each branched value independently and combine with // 380 // convertElementMatcherToBranchedMatcher". // 381 var VALUE_OPERATORS = { // 382 $not: function (operand, valueSelector, matcher) { // 383 return invertBranchedMatcher(compileValueSelector(operand, matcher)); // 384 }, // 385 $ne: function (operand) { // 386 return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( // 387 equalityElementMatcher(operand))); // 388 }, // 389 $nin: function (operand) { // 390 return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( // 391 ELEMENT_OPERATORS.$in.compileElementSelector(operand))); // 392 }, // 393 $exists: function (operand) { // 394 var exists = convertElementMatcherToBranchedMatcher(function (value) { // 395 return value !== undefined; // 396 }); // 397 return operand ? exists : invertBranchedMatcher(exists); // 398 }, // 399 // $options just provides options for $regex; its logic is inside $regex // 400 $options: function (operand, valueSelector) { // 401 if (!_.has(valueSelector, '$regex')) // 402 throw Error("$options needs a $regex"); // 403 return everythingMatcher; // 404 }, // 405 // $maxDistance is basically an argument to $near // 406 $maxDistance: function (operand, valueSelector) { // 407 if (!valueSelector.$near) // 408 throw Error("$maxDistance needs a $near"); // 409 return everythingMatcher; // 410 }, // 411 $all: function (operand, valueSelector, matcher) { // 412 if (!isArray(operand)) // 413 throw Error("$all requires array"); // 414 // Not sure why, but this seems to be what MongoDB does. // 415 if (_.isEmpty(operand)) // 416 return nothingMatcher; // 417 // 418 var branchedMatchers = []; // 419 _.each(operand, function (criterion) { // 420 // XXX handle $all/$elemMatch combination // 421 if (isOperatorObject(criterion)) // 422 throw Error("no $ expressions in $all"); // 423 // This is always a regexp or equality selector. // 424 branchedMatchers.push(compileValueSelector(criterion, matcher)); // 425 }); // 426 // andBranchedMatchers does NOT require all selectors to return true on the // 427 // SAME branch. // 428 return andBranchedMatchers(branchedMatchers); // 429 }, // 430 $near: function (operand, valueSelector, matcher, isRoot) { // 431 if (!isRoot) // 432 throw Error("$near can't be inside another $ operator"); // 433 matcher._hasGeoQuery = true; // 434 // 435 // There are two kinds of geodata in MongoDB: coordinate pairs and // 436 // GeoJSON. They use different distance metrics, too. GeoJSON queries are // 437 // marked with a $geometry property. // 438 // 439 var maxDistance, point, distance; // 440 if (isPlainObject(operand) && _.has(operand, '$geometry')) { // 441 // GeoJSON "2dsphere" mode. // 442 maxDistance = operand.$maxDistance; // 443 point = operand.$geometry; // 444 distance = function (value) { // 445 // XXX: for now, we don't calculate the actual distance between, say, // 446 // polygon and circle. If people care about this use-case it will get // 447 // a priority. // 448 if (!value || !value.type) // 449 return null; // 450 if (value.type === "Point") { // 451 return GeoJSON.pointDistance(point, value); // 452 } else { // 453 return GeoJSON.geometryWithinRadius(value, point, maxDistance) // 454 ? 0 : maxDistance + 1; // 455 } // 456 }; // 457 } else { // 458 maxDistance = valueSelector.$maxDistance; // 459 if (!isArray(operand) && !isPlainObject(operand)) // 460 throw Error("$near argument must be coordinate pair or GeoJSON"); // 461 point = pointToArray(operand); // 462 distance = function (value) { // 463 if (!isArray(value) && !isPlainObject(value)) // 464 return null; // 465 return distanceCoordinatePairs(point, value); // 466 }; // 467 } // 468 // 469 return function (branchedValues) { // 470 // There might be multiple points in the document that match the given // 471 // field. Only one of them needs to be within $maxDistance, but we need to // 472 // evaluate all of them and use the nearest one for the implicit sort // 473 // specifier. (That's why we can't just use ELEMENT_OPERATORS here.) // 474 // // 475 // Note: This differs from MongoDB's implementation, where a document will // 476 // actually show up *multiple times* in the result set, with one entry for // 477 // each within-$maxDistance branching point. // 478 branchedValues = expandArraysInBranches(branchedValues); // 479 var result = {result: false}; // 480 _.each(branchedValues, function (branch) { // 481 var curDistance = distance(branch.value); // 482 // Skip branches that aren't real points or are too far away. // 483 if (curDistance === null || curDistance > maxDistance) // 484 return; // 485 // Skip anything that's a tie. // 486 if (result.distance !== undefined && result.distance <= curDistance) // 487 return; // 488 result.result = true; // 489 result.distance = curDistance; // 490 if (!branch.arrayIndices) // 491 delete result.arrayIndices; // 492 else // 493 result.arrayIndices = branch.arrayIndices; // 494 }); // 495 return result; // 496 }; // 497 } // 498 }; // 499 // 500 // Helpers for $near. // 501 var distanceCoordinatePairs = function (a, b) { // 502 a = pointToArray(a); // 503 b = pointToArray(b); // 504 var x = a[0] - b[0]; // 505 var y = a[1] - b[1]; // 506 if (_.isNaN(x) || _.isNaN(y)) // 507 return null; // 508 return Math.sqrt(x * x + y * y); // 509 }; // 510 // Makes sure we get 2 elements array and assume the first one to be x and // 511 // the second one to y no matter what user passes. // 512 // In case user passes { lon: x, lat: y } returns [x, y] // 513 var pointToArray = function (point) { // 514 return _.map(point, _.identity); // 515 }; // 516 // 517 // Helper for $lt/$gt/$lte/$gte. // 518 var makeInequality = function (cmpValueComparator) { // 519 return { // 520 compileElementSelector: function (operand) { // 521 // Arrays never compare false with non-arrays for any inequality. // 522 // XXX This was behavior we observed in pre-release MongoDB 2.5, but // 523 // it seems to have been reverted. // 524 // See https://jira.mongodb.org/browse/SERVER-11444 // 525 if (isArray(operand)) { // 526 return function () { // 527 return false; // 528 }; // 529 } // 530 // 531 // Special case: consider undefined and null the same (so true with // 532 // $gte/$lte). // 533 if (operand === undefined) // 534 operand = null; // 535 // 536 var operandType = LocalCollection._f._type(operand); // 537 // 538 return function (value) { // 539 if (value === undefined) // 540 value = null; // 541 // Comparisons are never true among things of different type (except // 542 // null vs undefined). // 543 if (LocalCollection._f._type(value) !== operandType) // 544 return false; // 545 return cmpValueComparator(LocalCollection._f._cmp(value, operand)); // 546 }; // 547 } // 548 }; // 549 }; // 550 // 551 // Each element selector contains: // 552 // - compileElementSelector, a function with args: // 553 // - operand - the "right hand side" of the operator // 554 // - valueSelector - the "context" for the operator (so that $regex can find // 555 // $options) // 556 // - matcher - the Matcher this is going into (so that $elemMatch can compile // 557 // more things) // 558 // returning a function mapping a single value to bool. // 559 // - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from // 560 // being called // 561 // - dontIncludeLeafArrays, a bool which causes an argument to be passed to // 562 // expandArraysInBranches if it is called // 563 ELEMENT_OPERATORS = { // 564 $lt: makeInequality(function (cmpValue) { // 565 return cmpValue < 0; // 566 }), // 567 $gt: makeInequality(function (cmpValue) { // 568 return cmpValue > 0; // 569 }), // 570 $lte: makeInequality(function (cmpValue) { // 571 return cmpValue <= 0; // 572 }), // 573 $gte: makeInequality(function (cmpValue) { // 574 return cmpValue >= 0; // 575 }), // 576 $mod: { // 577 compileElementSelector: function (operand) { // 578 if (!(isArray(operand) && operand.length === 2 // 579 && typeof(operand[0]) === 'number' // 580 && typeof(operand[1]) === 'number')) { // 581 throw Error("argument to $mod must be an array of two numbers"); // 582 } // 583 // XXX could require to be ints or round or something // 584 var divisor = operand[0]; // 585 var remainder = operand[1]; // 586 return function (value) { // 587 return typeof value === 'number' && value % divisor === remainder; // 588 }; // 589 } // 590 }, // 591 $in: { // 592 compileElementSelector: function (operand) { // 593 if (!isArray(operand)) // 594 throw Error("$in needs an array"); // 595 // 596 var elementMatchers = []; // 597 _.each(operand, function (option) { // 598 if (option instanceof RegExp) // 599 elementMatchers.push(regexpElementMatcher(option)); // 600 else if (isOperatorObject(option)) // 601 throw Error("cannot nest $ under $in"); // 602 else // 603 elementMatchers.push(equalityElementMatcher(option)); // 604 }); // 605 // 606 return function (value) { // 607 // Allow {a: {$in: [null]}} to match when 'a' does not exist. // 608 if (value === undefined) // 609 value = null; // 610 return _.any(elementMatchers, function (e) { // 611 return e(value); // 612 }); // 613 }; // 614 } // 615 }, // 616 $size: { // 617 // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we // 618 // don't want to consider the element [5,5] in the leaf array [[5,5]] as a // 619 // possible value. // 620 dontExpandLeafArrays: true, // 621 compileElementSelector: function (operand) { // 622 if (typeof operand === 'string') { // 623 // Don't ask me why, but by experimentation, this seems to be what Mongo // 624 // does. // 625 operand = 0; // 626 } else if (typeof operand !== 'number') { // 627 throw Error("$size needs a number"); // 628 } // 629 return function (value) { // 630 return isArray(value) && value.length === operand; // 631 }; // 632 } // 633 }, // 634 $type: { // 635 // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should // 636 // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: // 637 // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but // 638 // should *not* include it itself. // 639 dontIncludeLeafArrays: true, // 640 compileElementSelector: function (operand) { // 641 if (typeof operand !== 'number') // 642 throw Error("$type needs a number"); // 643 return function (value) { // 644 return value !== undefined // 645 && LocalCollection._f._type(value) === operand; // 646 }; // 647 } // 648 }, // 649 $regex: { // 650 compileElementSelector: function (operand, valueSelector) { // 651 if (!(typeof operand === 'string' || operand instanceof RegExp)) // 652 throw Error("$regex has to be a string or RegExp"); // 653 // 654 var regexp; // 655 if (valueSelector.$options !== undefined) { // 656 // Options passed in $options (even the empty string) always overrides // 657 // options in the RegExp object itself. (See also // 658 // Mongo.Collection._rewriteSelector.) // 659 // 660 // Be clear that we only support the JS-supported options, not extended // 661 // ones (eg, Mongo supports x and s). Ideally we would implement x and s // 662 // by transforming the regexp, but not today... // 663 if (/[^gim]/.test(valueSelector.$options)) // 664 throw new Error("Only the i, m, and g regexp options are supported"); // 665 // 666 var regexSource = operand instanceof RegExp ? operand.source : operand; // 667 regexp = new RegExp(regexSource, valueSelector.$options); // 668 } else if (operand instanceof RegExp) { // 669 regexp = operand; // 670 } else { // 671 regexp = new RegExp(operand); // 672 } // 673 return regexpElementMatcher(regexp); // 674 } // 675 }, // 676 $elemMatch: { // 677 dontExpandLeafArrays: true, // 678 compileElementSelector: function (operand, valueSelector, matcher) { // 679 if (!isPlainObject(operand)) // 680 throw Error("$elemMatch need an object"); // 681 // 682 var subMatcher, isDocMatcher; // 683 if (isOperatorObject(operand, true)) { // 684 subMatcher = compileValueSelector(operand, matcher); // 685 isDocMatcher = false; // 686 } else { // 687 // This is NOT the same as compileValueSelector(operand), and not just // 688 // because of the slightly different calling convention. // 689 // {$elemMatch: {x: 3}} means "an element has a field x:3", not // 690 // "consists only of a field x:3". Also, regexps and sub-$ are allowed. // 691 subMatcher = compileDocumentSelector(operand, matcher, // 692 {inElemMatch: true}); // 693 isDocMatcher = true; // 694 } // 695 // 696 return function (value) { // 697 if (!isArray(value)) // 698 return false; // 699 for (var i = 0; i < value.length; ++i) { // 700 var arrayElement = value[i]; // 701 var arg; // 702 if (isDocMatcher) { // 703 // We can only match {$elemMatch: {b: 3}} against objects. // 704 // (We can also match against arrays, if there's numeric indices, // 705 // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) // 706 if (!isPlainObject(arrayElement) && !isArray(arrayElement)) // 707 return false; // 708 arg = arrayElement; // 709 } else { // 710 // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches // 711 // {a: [8]} but not {a: [[8]]} // 712 arg = [{value: arrayElement, dontIterate: true}]; // 713 } // 714 // XXX support $near in $elemMatch by propagating $distance? // 715 if (subMatcher(arg).result) // 716 return i; // specially understood to mean "use as arrayIndices" // 717 } // 718 return false; // 719 }; // 720 } // 721 } // 722 }; // 723 // 724 // makeLookupFunction(key) returns a lookup function. // 725 // // 726 // A lookup function takes in a document and returns an array of matching // 727 // branches. If no arrays are found while looking up the key, this array will // 728 // have exactly one branches (possibly 'undefined', if some segment of the key // 729 // was not found). // 730 // // 731 // If arrays are found in the middle, this can have more than one element, since // 732 // we "branch". When we "branch", if there are more key segments to look up, // 733 // then we only pursue branches that are plain objects (not arrays or scalars). // 734 // This means we can actually end up with no branches! // 735 // // 736 // We do *NOT* branch on arrays that are found at the end (ie, at the last // 737 // dotted member of the key). We just return that array; if you want to // 738 // effectively "branch" over the array's values, post-process the lookup // 739 // function with expandArraysInBranches. // 740 // // 741 // Each branch is an object with keys: // 742 // - value: the value at the branch // 743 // - dontIterate: an optional bool; if true, it means that 'value' is an array // 744 // that expandArraysInBranches should NOT expand. This specifically happens // 745 // when there is a numeric index in the key, and ensures the // 746 // perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT // 747 // match {a: [[5]]}. // 748 // - arrayIndices: if any array indexing was done during lookup (either due to // 749 // explicit numeric indices or implicit branching), this will be an array of // 750 // the array indices used, from outermost to innermost; it is falsey or // 751 // absent if no array index is used. If an explicit numeric index is used, // 752 // the index will be followed in arrayIndices by the string 'x'. // 753 // // 754 // Note: arrayIndices is used for two purposes. First, it is used to // 755 // implement the '$' modifier feature, which only ever looks at its first // 756 // element. // 757 // // 758 // Second, it is used for sort key generation, which needs to be able to tell // 759 // the difference between different paths. Moreover, it needs to // 760 // differentiate between explicit and implicit branching, which is why // 761 // there's the somewhat hacky 'x' entry: this means that explicit and // 762 // implicit array lookups will have different full arrayIndices paths. (That // 763 // code only requires that different paths have different arrayIndices; it // 764 // doesn't actually "parse" arrayIndices. As an alternative, arrayIndices // 765 // could contain objects with flags like "implicit", but I think that only // 766 // makes the code surrounding them more complex.) // 767 // // 768 // (By the way, this field ends up getting passed around a lot without // 769 // cloning, so never mutate any arrayIndices field/var in this package!) // 770 // // 771 // // 772 // At the top level, you may only pass in a plain object or array. // 773 // // 774 // See the test 'minimongo - lookup' for some examples of what lookup functions // 775 // return. // 776 makeLookupFunction = function (key, options) { // 777 options = options || {}; // 778 var parts = key.split('.'); // 779 var firstPart = parts.length ? parts[0] : ''; // 780 var firstPartIsNumeric = isNumericKey(firstPart); // 781 var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); // 782 var lookupRest; // 783 if (parts.length > 1) { // 784 lookupRest = makeLookupFunction(parts.slice(1).join('.')); // 785 } // 786 // 787 var omitUnnecessaryFields = function (retVal) { // 788 if (!retVal.dontIterate) // 789 delete retVal.dontIterate; // 790 if (retVal.arrayIndices && !retVal.arrayIndices.length) // 791 delete retVal.arrayIndices; // 792 return retVal; // 793 }; // 794 // 795 // Doc will always be a plain object or an array. // 796 // apply an explicit numeric index, an array. // 797 return function (doc, arrayIndices) { // 798 if (!arrayIndices) // 799 arrayIndices = []; // 800 // 801 if (isArray(doc)) { // 802 // If we're being asked to do an invalid lookup into an array (non-integer // 803 // or out-of-bounds), return no results (which is different from returning // 804 // a single undefined result, in that `null` equality checks won't match). // 805 if (!(firstPartIsNumeric && firstPart < doc.length)) // 806 return []; // 807 // 808 // Remember that we used this array index. Include an 'x' to indicate that // 809 // the previous index came from being considered as an explicit array // 810 // index (not branching). // 811 arrayIndices = arrayIndices.concat(+firstPart, 'x'); // 812 } // 813 // 814 // Do our first lookup. // 815 var firstLevel = doc[firstPart]; // 816 // 817 // If there is no deeper to dig, return what we found. // 818 // // 819 // If what we found is an array, most value selectors will choose to treat // 820 // the elements of the array as matchable values in their own right, but // 821 // that's done outside of the lookup function. (Exceptions to this are $size // 822 // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a: // 823 // [[1, 2]]}.) // 824 // // 825 // That said, if we just did an *explicit* array lookup (on doc) to find // 826 // firstLevel, and firstLevel is an array too, we do NOT want value // 827 // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}. // 828 // So in that case, we mark the return value as "don't iterate". // 829 if (!lookupRest) { // 830 return [omitUnnecessaryFields({ // 831 value: firstLevel, // 832 dontIterate: isArray(doc) && isArray(firstLevel), // 833 arrayIndices: arrayIndices})]; // 834 } // 835 // 836 // We need to dig deeper. But if we can't, because what we've found is not // 837 // an array or plain object, we're done. If we just did a numeric index into // 838 // an array, we return nothing here (this is a change in Mongo 2.5 from // 839 // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise, // 840 // return a single `undefined` (which can, for example, match via equality // 841 // with `null`). // 842 if (!isIndexable(firstLevel)) { // 843 if (isArray(doc)) // 844 return []; // 845 return [omitUnnecessaryFields({value: undefined, // 846 arrayIndices: arrayIndices})]; // 847 } // 848 // 849 var result = []; // 850 var appendToResult = function (more) { // 851 Array.prototype.push.apply(result, more); // 852 }; // 853 // 854 // Dig deeper: look up the rest of the parts on whatever we've found. // 855 // (lookupRest is smart enough to not try to do invalid lookups into // 856 // firstLevel if it's an array.) // 857 appendToResult(lookupRest(firstLevel, arrayIndices)); // 858 // 859 // If we found an array, then in *addition* to potentially treating the next // 860 // part as a literal integer lookup, we should also "branch": try to look up // 861 // the rest of the parts on each array element in parallel. // 862 // // 863 // In this case, we *only* dig deeper into array elements that are plain // 864 // objects. (Recall that we only got this far if we have further to dig.) // 865 // This makes sense: we certainly don't dig deeper into non-indexable // 866 // objects. And it would be weird to dig into an array: it's simpler to have // 867 // a rule that explicit integer indexes only apply to an outer array, not to // 868 // an array you find after a branching search. // 869 // // 870 // In the special case of a numeric part in a *sort selector* (not a query // 871 // selector), we skip the branching: we ONLY allow the numeric part to mean // 872 // "look up this index" in that case, not "also look up this index in all // 873 // the elements of the array". // 874 if (isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { // 875 _.each(firstLevel, function (branch, arrayIndex) { // 876 if (isPlainObject(branch)) { // 877 appendToResult(lookupRest( // 878 branch, // 879 arrayIndices.concat(arrayIndex))); // 880 } // 881 }); // 882 } // 883 // 884 return result; // 885 }; // 886 }; // 887 MinimongoTest.makeLookupFunction = makeLookupFunction; // 888 // 889 expandArraysInBranches = function (branches, skipTheArrays) { // 890 var branchesOut = []; // 891 _.each(branches, function (branch) { // 892 var thisIsArray = isArray(branch.value); // 893 // We include the branch itself, *UNLESS* we it's an array that we're going // 894 // to iterate and we're told to skip arrays. (That's right, we include some // 895 // arrays even skipTheArrays is true: these are arrays that were found via // 896 // explicit numerical indices.) // 897 if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { // 898 branchesOut.push({ // 899 value: branch.value, // 900 arrayIndices: branch.arrayIndices // 901 }); // 902 } // 903 if (thisIsArray && !branch.dontIterate) { // 904 _.each(branch.value, function (leaf, i) { // 905 branchesOut.push({ // 906 value: leaf, // 907 arrayIndices: (branch.arrayIndices || []).concat(i) // 908 }); // 909 }); // 910 } // 911 }); // 912 return branchesOut; // 913 }; // 914 // 915 var nothingMatcher = function (docOrBranchedValues) { // 916 return {result: false}; // 917 }; // 918 // 919 var everythingMatcher = function (docOrBranchedValues) { // 920 return {result: true}; // 921 }; // 922 // 923 // 924 // NB: We are cheating and using this function to implement "AND" for both // 925 // "document matchers" and "branched matchers". They both return result objects // 926 // but the argument is different: for the former it's a whole doc, whereas for // 927 // the latter it's an array of "branched values". // 928 var andSomeMatchers = function (subMatchers) { // 929 if (subMatchers.length === 0) // 930 return everythingMatcher; // 931 if (subMatchers.length === 1) // 932 return subMatchers[0]; // 933 // 934 return function (docOrBranches) { // 935 var ret = {}; // 936 ret.result = _.all(subMatchers, function (f) { // 937 var subResult = f(docOrBranches); // 938 // Copy a 'distance' number out of the first sub-matcher that has // 939 // one. Yes, this means that if there are multiple $near fields in a // 940 // query, something arbitrary happens; this appears to be consistent with // 941 // Mongo. // 942 if (subResult.result && subResult.distance !== undefined // 943 && ret.distance === undefined) { // 944 ret.distance = subResult.distance; // 945 } // 946 // Similarly, propagate arrayIndices from sub-matchers... but to match // 947 // MongoDB behavior, this time the *last* sub-matcher with arrayIndices // 948 // wins. // 949 if (subResult.result && subResult.arrayIndices) { // 950 ret.arrayIndices = subResult.arrayIndices; // 951 } // 952 return subResult.result; // 953 }); // 954 // 955 // If we didn't actually match, forget any extra metadata we came up with. // 956 if (!ret.result) { // 957 delete ret.distance; // 958 delete ret.arrayIndices; // 959 } // 960 return ret; // 961 }; // 962 }; // 963 // 964 var andDocumentMatchers = andSomeMatchers; // 965 var andBranchedMatchers = andSomeMatchers; // 966 // 967 // 968 // helpers used by compiled selector code // 969 LocalCollection._f = { // 970 // XXX for _all and _in, consider building 'inquery' at compile time.. // 971 // 972 _type: function (v) { // 973 if (typeof v === "number") // 974 return 1; // 975 if (typeof v === "string") // 976 return 2; // 977 if (typeof v === "boolean") // 978 return 8; // 979 if (isArray(v)) // 980 return 4; // 981 if (v === null) // 982 return 10; // 983 if (v instanceof RegExp) // 984 // note that typeof(/x/) === "object" // 985 return 11; // 986 if (typeof v === "function") // 987 return 13; // 988 if (v instanceof Date) // 989 return 9; // 990 if (EJSON.isBinary(v)) // 991 return 5; // 992 if (v instanceof LocalCollection._ObjectID) // 993 return 7; // 994 return 3; // object // 995 // 996 // XXX support some/all of these: // 997 // 14, symbol // 998 // 15, javascript code with scope // 999 // 16, 18: 32-bit/64-bit integer // 1000 // 17, timestamp // 1001 // 255, minkey // 1002 // 127, maxkey // 1003 }, // 1004 // 1005 // deep equality test: use for literal document and array matches // 1006 _equal: function (a, b) { // 1007 return EJSON.equals(a, b, {keyOrderSensitive: true}); // 1008 }, // 1009 // 1010 // maps a type code to a value that can be used to sort values of // 1011 // different types // 1012 _typeorder: function (t) { // 1013 // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types // 1014 // XXX what is the correct sort position for Javascript code? // 1015 // ('100' in the matrix below) // 1016 // XXX minkey/maxkey // 1017 return [-1, // (not a type) // 1018 1, // number // 1019 2, // string // 1020 3, // object // 1021 4, // array // 1022 5, // binary // 1023 -1, // deprecated // 1024 6, // ObjectID // 1025 7, // bool // 1026 8, // Date // 1027 0, // null // 1028 9, // RegExp // 1029 -1, // deprecated // 1030 100, // JS code // 1031 2, // deprecated (symbol) // 1032 100, // JS code // 1033 1, // 32-bit int // 1034 8, // Mongo timestamp // 1035 1 // 64-bit int // 1036 ][t]; // 1037 }, // 1038 // 1039 // compare two values of unknown type according to BSON ordering // 1040 // semantics. (as an extension, consider 'undefined' to be less than // 1041 // any other value.) return negative if a is less, positive if b is // 1042 // less, or 0 if equal // 1043 _cmp: function (a, b) { // 1044 if (a === undefined) // 1045 return b === undefined ? 0 : -1; // 1046 if (b === undefined) // 1047 return 1; // 1048 var ta = LocalCollection._f._type(a); // 1049 var tb = LocalCollection._f._type(b); // 1050 var oa = LocalCollection._f._typeorder(ta); // 1051 var ob = LocalCollection._f._typeorder(tb); // 1052 if (oa !== ob) // 1053 return oa < ob ? -1 : 1; // 1054 if (ta !== tb) // 1055 // XXX need to implement this if we implement Symbol or integers, or // 1056 // Timestamp // 1057 throw Error("Missing type coercion logic in _cmp"); // 1058 if (ta === 7) { // ObjectID // 1059 // Convert to string. // 1060 ta = tb = 2; // 1061 a = a.toHexString(); // 1062 b = b.toHexString(); // 1063 } // 1064 if (ta === 9) { // Date // 1065 // Convert to millis. // 1066 ta = tb = 1; // 1067 a = a.getTime(); // 1068 b = b.getTime(); // 1069 } // 1070 // 1071 if (ta === 1) // double // 1072 return a - b; // 1073 if (tb === 2) // string // 1074 return a < b ? -1 : (a === b ? 0 : 1); // 1075 if (ta === 3) { // Object // 1076 // this could be much more efficient in the expected case ... // 1077 var to_array = function (obj) { // 1078 var ret = []; // 1079 for (var key in obj) { // 1080 ret.push(key); // 1081 ret.push(obj[key]); // 1082 } // 1083 return ret; // 1084 }; // 1085 return LocalCollection._f._cmp(to_array(a), to_array(b)); // 1086 } // 1087 if (ta === 4) { // Array // 1088 for (var i = 0; ; i++) { // 1089 if (i === a.length) // 1090 return (i === b.length) ? 0 : -1; // 1091 if (i === b.length) // 1092 return 1; // 1093 var s = LocalCollection._f._cmp(a[i], b[i]); // 1094 if (s !== 0) // 1095 return s; // 1096 } // 1097 } // 1098 if (ta === 5) { // binary // 1099 // Surprisingly, a small binary blob is always less than a large one in // 1100 // Mongo. // 1101 if (a.length !== b.length) // 1102 return a.length - b.length; // 1103 for (i = 0; i < a.length; i++) { // 1104 if (a[i] < b[i]) // 1105 return -1; // 1106 if (a[i] > b[i]) // 1107 return 1; // 1108 } // 1109 return 0; // 1110 } // 1111 if (ta === 8) { // boolean // 1112 if (a) return b ? 0 : 1; // 1113 return b ? -1 : 0; // 1114 } // 1115 if (ta === 10) // null // 1116 return 0; // 1117 if (ta === 11) // regexp // 1118 throw Error("Sorting not supported on regular expression"); // XXX // 1119 // 13: javascript code // 1120 // 14: symbol // 1121 // 15: javascript code with scope // 1122 // 16: 32-bit integer // 1123 // 17: timestamp // 1124 // 18: 64-bit integer // 1125 // 255: minkey // 1126 // 127: maxkey // 1127 if (ta === 13) // javascript code // 1128 throw Error("Sorting not supported on Javascript code"); // XXX // 1129 throw Error("Unknown type to sort"); // 1130 } // 1131 }; // 1132 // 1133 // Oddball function used by upsert. // 1134 LocalCollection._removeDollarOperators = function (selector) { // 1135 var selectorDoc = {}; // 1136 for (var k in selector) // 1137 if (k.substr(0, 1) !== '$') // 1138 selectorDoc[k] = selector[k]; // 1139 return selectorDoc; // 1140 }; // 1141 // 1142 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/sort.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Give a sort spec, which can be in any of these forms: // 1 // {"key1": 1, "key2": -1} // 2 // [["key1", "asc"], ["key2", "desc"]] // 3 // ["key1", ["key2", "desc"]] // 4 // // 5 // (.. with the first form being dependent on the key enumeration // 6 // behavior of your javascript VM, which usually does what you mean in // 7 // this case if the key names don't look like integers ..) // 8 // // 9 // return a function that takes two objects, and returns -1 if the // 10 // first object comes first in order, 1 if the second object comes // 11 // first, or 0 if neither object comes before the other. // 12 // 13 Minimongo.Sorter = function (spec, options) { // 14 var self = this; // 15 options = options || {}; // 16 // 17 self._sortSpecParts = []; // 18 // 19 var addSpecPart = function (path, ascending) { // 20 if (!path) // 21 throw Error("sort keys must be non-empty"); // 22 if (path.charAt(0) === '$') // 23 throw Error("unsupported sort key: " + path); // 24 self._sortSpecParts.push({ // 25 path: path, // 26 lookup: makeLookupFunction(path, {forSort: true}), // 27 ascending: ascending // 28 }); // 29 }; // 30 // 31 if (spec instanceof Array) { // 32 for (var i = 0; i < spec.length; i++) { // 33 if (typeof spec[i] === "string") { // 34 addSpecPart(spec[i], true); // 35 } else { // 36 addSpecPart(spec[i][0], spec[i][1] !== "desc"); // 37 } // 38 } // 39 } else if (typeof spec === "object") { // 40 _.each(spec, function (value, key) { // 41 addSpecPart(key, value >= 0); // 42 }); // 43 } else { // 44 throw Error("Bad sort specification: " + JSON.stringify(spec)); // 45 } // 46 // 47 // To implement affectedByModifier, we piggy-back on top of Matcher's // 48 // affectedByModifier code; we create a selector that is affected by the same // 49 // modifiers as this sort order. This is only implemented on the server. // 50 if (self.affectedByModifier) { // 51 var selector = {}; // 52 _.each(self._sortSpecParts, function (spec) { // 53 selector[spec.path] = 1; // 54 }); // 55 self._selectorForAffectedByModifier = new Minimongo.Matcher(selector); // 56 } // 57 // 58 self._keyComparator = composeComparators( // 59 _.map(self._sortSpecParts, function (spec, i) { // 60 return self._keyFieldComparator(i); // 61 })); // 62 // 63 // If you specify a matcher for this Sorter, _keyFilter may be set to a // 64 // function which selects whether or not a given "sort key" (tuple of values // 65 // for the different sort spec fields) is compatible with the selector. // 66 self._keyFilter = null; // 67 options.matcher && self._useWithMatcher(options.matcher); // 68 }; // 69 // 70 // In addition to these methods, sorter_project.js defines combineIntoProjection // 71 // on the server only. // 72 _.extend(Minimongo.Sorter.prototype, { // 73 getComparator: function (options) { // 74 var self = this; // 75 // 76 // If we have no distances, just use the comparator from the source // 77 // specification (which defaults to "everything is equal". // 78 if (!options || !options.distances) { // 79 return self._getBaseComparator(); // 80 } // 81 // 82 var distances = options.distances; // 83 // 84 // Return a comparator which first tries the sort specification, and if that // 85 // says "it's equal", breaks ties using $near distances. // 86 return composeComparators([self._getBaseComparator(), function (a, b) { // 87 if (!distances.has(a._id)) // 88 throw Error("Missing distance for " + a._id); // 89 if (!distances.has(b._id)) // 90 throw Error("Missing distance for " + b._id); // 91 return distances.get(a._id) - distances.get(b._id); // 92 }]); // 93 }, // 94 // 95 _getPaths: function () { // 96 var self = this; // 97 return _.pluck(self._sortSpecParts, 'path'); // 98 }, // 99 // 100 // Finds the minimum key from the doc, according to the sort specs. (We say // 101 // "minimum" here but this is with respect to the sort spec, so "descending" // 102 // sort fields mean we're finding the max for that field.) // 103 // // 104 // Note that this is NOT "find the minimum value of the first field, the // 105 // minimum value of the second field, etc"... it's "choose the // 106 // lexicographically minimum value of the key vector, allowing only keys which // 107 // you can find along the same paths". ie, for a doc {a: [{x: 0, y: 5}, {x: // 108 // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and // 109 // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key. // 110 _getMinKeyFromDoc: function (doc) { // 111 var self = this; // 112 var minKey = null; // 113 // 114 self._generateKeysFromDoc(doc, function (key) { // 115 if (!self._keyCompatibleWithSelector(key)) // 116 return; // 117 // 118 if (minKey === null) { // 119 minKey = key; // 120 return; // 121 } // 122 if (self._compareKeys(key, minKey) < 0) { // 123 minKey = key; // 124 } // 125 }); // 126 // 127 // This could happen if our key filter somehow filters out all the keys even // 128 // though somehow the selector matches. // 129 if (minKey === null) // 130 throw Error("sort selector found no keys in doc?"); // 131 return minKey; // 132 }, // 133 // 134 _keyCompatibleWithSelector: function (key) { // 135 var self = this; // 136 return !self._keyFilter || self._keyFilter(key); // 137 }, // 138 // 139 // Iterates over each possible "key" from doc (ie, over each branch), calling // 140 // 'cb' with the key. // 141 _generateKeysFromDoc: function (doc, cb) { // 142 var self = this; // 143 // 144 if (self._sortSpecParts.length === 0) // 145 throw new Error("can't generate keys without a spec"); // 146 // 147 // maps index -> ({'' -> value} or {path -> value}) // 148 var valuesByIndexAndPath = []; // 149 // 150 var pathFromIndices = function (indices) { // 151 return indices.join(',') + ','; // 152 }; // 153 // 154 var knownPaths = null; // 155 // 156 _.each(self._sortSpecParts, function (spec, whichField) { // 157 // Expand any leaf arrays that we find, and ignore those arrays // 158 // themselves. (We never sort based on an array itself.) // 159 var branches = expandArraysInBranches(spec.lookup(doc), true); // 160 // 161 // If there are no values for a key (eg, key goes to an empty array), // 162 // pretend we found one null value. // 163 if (!branches.length) // 164 branches = [{value: null}]; // 165 // 166 var usedPaths = false; // 167 valuesByIndexAndPath[whichField] = {}; // 168 _.each(branches, function (branch) { // 169 if (!branch.arrayIndices) { // 170 // If there are no array indices for a branch, then it must be the // 171 // only branch, because the only thing that produces multiple branches // 172 // is the use of arrays. // 173 if (branches.length > 1) // 174 throw Error("multiple branches but no array used?"); // 175 valuesByIndexAndPath[whichField][''] = branch.value; // 176 return; // 177 } // 178 // 179 usedPaths = true; // 180 var path = pathFromIndices(branch.arrayIndices); // 181 if (_.has(valuesByIndexAndPath[whichField], path)) // 182 throw Error("duplicate path: " + path); // 183 valuesByIndexAndPath[whichField][path] = branch.value; // 184 // 185 // If two sort fields both go into arrays, they have to go into the // 186 // exact same arrays and we have to find the same paths. This is // 187 // roughly the same condition that makes MongoDB throw this strange // 188 // error message. eg, the main thing is that if sort spec is {a: 1, // 189 // b:1} then a and b cannot both be arrays. // 190 // // 191 // (In MongoDB it seems to be OK to have {a: 1, 'a.x.y': 1} where 'a' // 192 // and 'a.x.y' are both arrays, but we don't allow this for now. // 193 // #NestedArraySort // 194 // XXX achieve full compatibility here // 195 if (knownPaths && !_.has(knownPaths, path)) { // 196 throw Error("cannot index parallel arrays"); // 197 } // 198 }); // 199 // 200 if (knownPaths) { // 201 // Similarly to above, paths must match everywhere, unless this is a // 202 // non-array field. // 203 if (!_.has(valuesByIndexAndPath[whichField], '') && // 204 _.size(knownPaths) !== _.size(valuesByIndexAndPath[whichField])) { // 205 throw Error("cannot index parallel arrays!"); // 206 } // 207 } else if (usedPaths) { // 208 knownPaths = {}; // 209 _.each(valuesByIndexAndPath[whichField], function (x, path) { // 210 knownPaths[path] = true; // 211 }); // 212 } // 213 }); // 214 // 215 if (!knownPaths) { // 216 // Easy case: no use of arrays. // 217 var soleKey = _.map(valuesByIndexAndPath, function (values) { // 218 if (!_.has(values, '')) // 219 throw Error("no value in sole key case?"); // 220 return values['']; // 221 }); // 222 cb(soleKey); // 223 return; // 224 } // 225 // 226 _.each(knownPaths, function (x, path) { // 227 var key = _.map(valuesByIndexAndPath, function (values) { // 228 if (_.has(values, '')) // 229 return values['']; // 230 if (!_.has(values, path)) // 231 throw Error("missing path?"); // 232 return values[path]; // 233 }); // 234 cb(key); // 235 }); // 236 }, // 237 // 238 // Takes in two keys: arrays whose lengths match the number of spec // 239 // parts. Returns negative, 0, or positive based on using the sort spec to // 240 // compare fields. // 241 _compareKeys: function (key1, key2) { // 242 var self = this; // 243 if (key1.length !== self._sortSpecParts.length || // 244 key2.length !== self._sortSpecParts.length) { // 245 throw Error("Key has wrong length"); // 246 } // 247 // 248 return self._keyComparator(key1, key2); // 249 }, // 250 // 251 // Given an index 'i', returns a comparator that compares two key arrays based // 252 // on field 'i'. // 253 _keyFieldComparator: function (i) { // 254 var self = this; // 255 var invert = !self._sortSpecParts[i].ascending; // 256 return function (key1, key2) { // 257 var compare = LocalCollection._f._cmp(key1[i], key2[i]); // 258 if (invert) // 259 compare = -compare; // 260 return compare; // 261 }; // 262 }, // 263 // 264 // Returns a comparator that represents the sort specification (but not // 265 // including a possible geoquery distance tie-breaker). // 266 _getBaseComparator: function () { // 267 var self = this; // 268 // 269 // If we're only sorting on geoquery distance and no specs, just say // 270 // everything is equal. // 271 if (!self._sortSpecParts.length) { // 272 return function (doc1, doc2) { // 273 return 0; // 274 }; // 275 } // 276 // 277 return function (doc1, doc2) { // 278 var key1 = self._getMinKeyFromDoc(doc1); // 279 var key2 = self._getMinKeyFromDoc(doc2); // 280 return self._compareKeys(key1, key2); // 281 }; // 282 }, // 283 // 284 // In MongoDB, if you have documents // 285 // {_id: 'x', a: [1, 10]} and // 286 // {_id: 'y', a: [5, 15]}, // 287 // then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5). // 288 // But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not // 289 // match the selector, and 5 comes before 10). // 290 // // 291 // The way this works is pretty subtle! For example, if the documents // 292 // are instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and // 293 // {_id: 'y', a: [{x: 5}, {x: 15}]}), // 294 // then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and // 295 // C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}}) // 296 // both follow this rule (y before x). (ie, you do have to apply this // 297 // through $elemMatch.) // 298 // // 299 // So if you pass a matcher to this sorter's constructor, we will attempt to // 300 // skip sort keys that don't match the selector. The logic here is pretty // 301 // subtle and undocumented; we've gotten as close as we can figure out based // 302 // on our understanding of Mongo's behavior. // 303 _useWithMatcher: function (matcher) { // 304 var self = this; // 305 // 306 if (self._keyFilter) // 307 throw Error("called _useWithMatcher twice?"); // 308 // 309 // If we are only sorting by distance, then we're not going to bother to // 310 // build a key filter. // 311 // XXX figure out how geoqueries interact with this stuff // 312 if (_.isEmpty(self._sortSpecParts)) // 313 return; // 314 // 315 var selector = matcher._selector; // 316 // 317 // If the user just passed a literal function to find(), then we can't get a // 318 // key filter from it. // 319 if (selector instanceof Function) // 320 return; // 321 // 322 var constraintsByPath = {}; // 323 _.each(self._sortSpecParts, function (spec, i) { // 324 constraintsByPath[spec.path] = []; // 325 }); // 326 // 327 _.each(selector, function (subSelector, key) { // 328 // XXX support $and and $or // 329 // 330 var constraints = constraintsByPath[key]; // 331 if (!constraints) // 332 return; // 333 // 334 // XXX it looks like the real MongoDB implementation isn't "does the // 335 // regexp match" but "does the value fall into a range named by the // 336 // literal prefix of the regexp", ie "foo" in /^foo(bar|baz)+/ But // 337 // "does the regexp match" is a good approximation. // 338 if (subSelector instanceof RegExp) { // 339 // As far as we can tell, using either of the options that both we and // 340 // MongoDB support ('i' and 'm') disables use of the key filter. This // 341 // makes sense: MongoDB mostly appears to be calculating ranges of an // 342 // index to use, which means it only cares about regexps that match // 343 // one range (with a literal prefix), and both 'i' and 'm' prevent the // 344 // literal prefix of the regexp from actually meaning one range. // 345 if (subSelector.ignoreCase || subSelector.multiline) // 346 return; // 347 constraints.push(regexpElementMatcher(subSelector)); // 348 return; // 349 } // 350 // 351 if (isOperatorObject(subSelector)) { // 352 _.each(subSelector, function (operand, operator) { // 353 if (_.contains(['$lt', '$lte', '$gt', '$gte'], operator)) { // 354 // XXX this depends on us knowing that these operators don't use any // 355 // of the arguments to compileElementSelector other than operand. // 356 constraints.push( // 357 ELEMENT_OPERATORS[operator].compileElementSelector(operand)); // 358 } // 359 // 360 // See comments in the RegExp block above. // 361 if (operator === '$regex' && !subSelector.$options) { // 362 constraints.push( // 363 ELEMENT_OPERATORS.$regex.compileElementSelector( // 364 operand, subSelector)); // 365 } // 366 // 367 // XXX support {$exists: true}, $mod, $type, $in, $elemMatch // 368 }); // 369 return; // 370 } // 371 // 372 // OK, it's an equality thing. // 373 constraints.push(equalityElementMatcher(subSelector)); // 374 }); // 375 // 376 // It appears that the first sort field is treated differently from the // 377 // others; we shouldn't create a key filter unless the first sort field is // 378 // restricted, though after that point we can restrict the other sort fields // 379 // or not as we wish. // 380 if (_.isEmpty(constraintsByPath[self._sortSpecParts[0].path])) // 381 return; // 382 // 383 self._keyFilter = function (key) { // 384 return _.all(self._sortSpecParts, function (specPart, index) { // 385 return _.all(constraintsByPath[specPart.path], function (f) { // 386 return f(key[index]); // 387 }); // 388 }); // 389 }; // 390 } // 391 }); // 392 // 393 // Given an array of comparators // 394 // (functions (a,b)->(negative or positive or zero)), returns a single // 395 // comparator which uses each comparator in order and returns the first // 396 // non-zero value. // 397 var composeComparators = function (comparatorArray) { // 398 return function (a, b) { // 399 for (var i = 0; i < comparatorArray.length; ++i) { // 400 var compare = comparatorArray[i](a, b); // 401 if (compare !== 0) // 402 return compare; // 403 } // 404 return 0; // 405 }; // 406 }; // 407 // 408 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/projection.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Knows how to compile a fields projection to a predicate function. // 1 // @returns - Function: a closure that filters out an object according to the // 2 // fields projection rules: // 3 // @param obj - Object: MongoDB-styled document // 4 // @returns - Object: a document with the fields filtered out // 5 // according to projection rules. Doesn't retain subfields // 6 // of passed argument. // 7 LocalCollection._compileProjection = function (fields) { // 8 LocalCollection._checkSupportedProjection(fields); // 9 // 10 var _idProjection = _.isUndefined(fields._id) ? true : fields._id; // 11 var details = projectionDetails(fields); // 12 // 13 // returns transformed doc according to ruleTree // 14 var transform = function (doc, ruleTree) { // 15 // Special case for "sets" // 16 if (_.isArray(doc)) // 17 return _.map(doc, function (subdoc) { return transform(subdoc, ruleTree); }); // 18 // 19 var res = details.including ? {} : EJSON.clone(doc); // 20 _.each(ruleTree, function (rule, key) { // 21 if (!_.has(doc, key)) // 22 return; // 23 if (_.isObject(rule)) { // 24 // For sub-objects/subsets we branch // 25 if (_.isObject(doc[key])) // 26 res[key] = transform(doc[key], rule); // 27 // Otherwise we don't even touch this subfield // 28 } else if (details.including) // 29 res[key] = EJSON.clone(doc[key]); // 30 else // 31 delete res[key]; // 32 }); // 33 // 34 return res; // 35 }; // 36 // 37 return function (obj) { // 38 var res = transform(obj, details.tree); // 39 // 40 if (_idProjection && _.has(obj, '_id')) // 41 res._id = obj._id; // 42 if (!_idProjection && _.has(res, '_id')) // 43 delete res._id; // 44 return res; // 45 }; // 46 }; // 47 // 48 // Traverses the keys of passed projection and constructs a tree where all // 49 // leaves are either all True or all False // 50 // @returns Object: // 51 // - tree - Object - tree representation of keys involved in projection // 52 // (exception for '_id' as it is a special case handled separately) // 53 // - including - Boolean - "take only certain fields" type of projection // 54 projectionDetails = function (fields) { // 55 // Find the non-_id keys (_id is handled specially because it is included unless // 56 // explicitly excluded). Sort the keys, so that our code to detect overlaps // 57 // like 'foo' and 'foo.bar' can assume that 'foo' comes first. // 58 var fieldsKeys = _.keys(fields).sort(); // 59 // 60 // If there are other rules other than '_id', treat '_id' differently in a // 61 // separate case. If '_id' is the only rule, use it to understand if it is // 62 // including/excluding projection. // 63 if (fieldsKeys.length > 0 && !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id')) // 64 fieldsKeys = _.reject(fieldsKeys, function (key) { return key === '_id'; }); // 65 // 66 var including = null; // Unknown // 67 // 68 _.each(fieldsKeys, function (keyPath) { // 69 var rule = !!fields[keyPath]; // 70 if (including === null) // 71 including = rule; // 72 if (including !== rule) // 73 // This error message is copies from MongoDB shell // 74 throw MinimongoError("You cannot currently mix including and excluding fields."); // 75 }); // 76 // 77 // 78 var projectionRulesTree = pathsToTree( // 79 fieldsKeys, // 80 function (path) { return including; }, // 81 function (node, path, fullPath) { // 82 // Check passed projection fields' keys: If you have two rules such as // 83 // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If // 84 // that happens, there is a probability you are doing something wrong, // 85 // framework should notify you about such mistake earlier on cursor // 86 // compilation step than later during runtime. Note, that real mongo // 87 // doesn't do anything about it and the later rule appears in projection // 88 // project, more priority it takes. // 89 // // 90 // Example, assume following in mongo shell: // 91 // > db.coll.insert({ a: { b: 23, c: 44 } }) // 92 // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) // 93 // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } } // 94 // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) // 95 // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } // 96 // // 97 // Note, how second time the return set of keys is different. // 98 // 99 var currentPath = fullPath; // 100 var anotherPath = path; // 101 throw MinimongoError("both " + currentPath + " and " + anotherPath + // 102 " found in fields option, using both of them may trigger " + // 103 "unexpected behavior. Did you mean to use only one of them?"); // 104 }); // 105 // 106 return { // 107 tree: projectionRulesTree, // 108 including: including // 109 }; // 110 }; // 111 // 112 // paths - Array: list of mongo style paths // 113 // newLeafFn - Function: of form function(path) should return a scalar value to // 114 // put into list created for that path // 115 // conflictFn - Function: of form function(node, path, fullPath) is called // 116 // when building a tree path for 'fullPath' node on // 117 // 'path' was already a leaf with a value. Must return a // 118 // conflict resolution. // 119 // initial tree - Optional Object: starting tree. // 120 // @returns - Object: tree represented as a set of nested objects // 121 pathsToTree = function (paths, newLeafFn, conflictFn, tree) { // 122 tree = tree || {}; // 123 _.each(paths, function (keyPath) { // 124 var treePos = tree; // 125 var pathArr = keyPath.split('.'); // 126 // 127 // use _.all just for iteration with break // 128 var success = _.all(pathArr.slice(0, -1), function (key, idx) { // 129 if (!_.has(treePos, key)) // 130 treePos[key] = {}; // 131 else if (!_.isObject(treePos[key])) { // 132 treePos[key] = conflictFn(treePos[key], // 133 pathArr.slice(0, idx + 1).join('.'), // 134 keyPath); // 135 // break out of loop if we are failing for this path // 136 if (!_.isObject(treePos[key])) // 137 return false; // 138 } // 139 // 140 treePos = treePos[key]; // 141 return true; // 142 }); // 143 // 144 if (success) { // 145 var lastKey = _.last(pathArr); // 146 if (!_.has(treePos, lastKey)) // 147 treePos[lastKey] = newLeafFn(keyPath); // 148 else // 149 treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); // 150 } // 151 }); // 152 // 153 return tree; // 154 }; // 155 // 156 LocalCollection._checkSupportedProjection = function (fields) { // 157 if (!_.isObject(fields) || _.isArray(fields)) // 158 throw MinimongoError("fields option must be an object"); // 159 // 160 _.each(fields, function (val, keyPath) { // 161 if (_.contains(keyPath.split('.'), '$')) // 162 throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); // 163 if (_.indexOf([1, 0, true, false], val) === -1) // 164 throw MinimongoError("Projection values should be one of 1, 0, true, or false"); // 165 }); // 166 }; // 167 // 168 // 169 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/modify.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // XXX need a strategy for passing the binding of $ into this // 1 // function, from the compiled selector // 2 // // 3 // maybe just {key.up.to.just.before.dollarsign: array_index} // 4 // // 5 // XXX atomicity: if one modification fails, do we roll back the whole // 6 // change? // 7 // // 8 // options: // 9 // - isInsert is set when _modify is being called to compute the document to // 10 // insert as part of an upsert operation. We use this primarily to figure // 11 // out when to set the fields in $setOnInsert, if present. // 12 LocalCollection._modify = function (doc, mod, options) { // 13 options = options || {}; // 14 if (!isPlainObject(mod)) // 15 throw MinimongoError("Modifier must be an object"); // 16 var isModifier = isOperatorObject(mod); // 17 // 18 var newDoc; // 19 // 20 if (!isModifier) { // 21 if (mod._id && !EJSON.equals(doc._id, mod._id)) // 22 throw MinimongoError("Cannot change the _id of a document"); // 23 // 24 // replace the whole document // 25 for (var k in mod) { // 26 if (/\./.test(k)) // 27 throw MinimongoError( // 28 "When replacing document, field name may not contain '.'"); // 29 } // 30 newDoc = mod; // 31 } else { // 32 // apply modifiers to the doc. // 33 newDoc = EJSON.clone(doc); // 34 // 35 _.each(mod, function (operand, op) { // 36 var modFunc = MODIFIERS[op]; // 37 // Treat $setOnInsert as $set if this is an insert. // 38 if (options.isInsert && op === '$setOnInsert') // 39 modFunc = MODIFIERS['$set']; // 40 if (!modFunc) // 41 throw MinimongoError("Invalid modifier specified " + op); // 42 _.each(operand, function (arg, keypath) { // 43 if (keypath === '') { // 44 throw MinimongoError("An empty update path is not valid."); // 45 } // 46 // 47 if (keypath === '_id') { // 48 throw MinimongoError("Mod on _id not allowed"); // 49 } // 50 // 51 var keyparts = keypath.split('.'); // 52 // 53 if (! _.all(keyparts, _.identity)) { // 54 throw MinimongoError( // 55 "The update path '" + keypath + // 56 "' contains an empty field name, which is not allowed."); // 57 } // 58 // 59 var noCreate = _.has(NO_CREATE_MODIFIERS, op); // 60 var forbidArray = (op === "$rename"); // 61 var target = findModTarget(newDoc, keyparts, { // 62 noCreate: NO_CREATE_MODIFIERS[op], // 63 forbidArray: (op === "$rename"), // 64 arrayIndices: options.arrayIndices // 65 }); // 66 var field = keyparts.pop(); // 67 modFunc(target, field, arg, keypath, newDoc); // 68 }); // 69 }); // 70 } // 71 // 72 // move new document into place. // 73 _.each(_.keys(doc), function (k) { // 74 // Note: this used to be for (var k in doc) however, this does not // 75 // work right in Opera. Deleting from a doc while iterating over it // 76 // would sometimes cause opera to skip some keys. // 77 if (k !== '_id') // 78 delete doc[k]; // 79 }); // 80 _.each(newDoc, function (v, k) { // 81 doc[k] = v; // 82 }); // 83 }; // 84 // 85 // for a.b.c.2.d.e, keyparts should be ['a', 'b', 'c', '2', 'd', 'e'], // 86 // and then you would operate on the 'e' property of the returned // 87 // object. // 88 // // 89 // if options.noCreate is falsey, creates intermediate levels of // 90 // structure as necessary, like mkdir -p (and raises an exception if // 91 // that would mean giving a non-numeric property to an array.) if // 92 // options.noCreate is true, return undefined instead. // 93 // // 94 // may modify the last element of keyparts to signal to the caller that it needs // 95 // to use a different value to index into the returned object (for example, // 96 // ['a', '01'] -> ['a', 1]). // 97 // // 98 // if forbidArray is true, return null if the keypath goes through an array. // 99 // // 100 // if options.arrayIndices is set, use its first element for the (first) '$' in // 101 // the path. // 102 var findModTarget = function (doc, keyparts, options) { // 103 options = options || {}; // 104 var usedArrayIndex = false; // 105 for (var i = 0; i < keyparts.length; i++) { // 106 var last = (i === keyparts.length - 1); // 107 var keypart = keyparts[i]; // 108 var indexable = isIndexable(doc); // 109 if (!indexable) { // 110 if (options.noCreate) // 111 return undefined; // 112 var e = MinimongoError( // 113 "cannot use the part '" + keypart + "' to traverse " + doc); // 114 e.setPropertyError = true; // 115 throw e; // 116 } // 117 if (doc instanceof Array) { // 118 if (options.forbidArray) // 119 return null; // 120 if (keypart === '$') { // 121 if (usedArrayIndex) // 122 throw MinimongoError("Too many positional (i.e. '$') elements"); // 123 if (!options.arrayIndices || !options.arrayIndices.length) { // 124 throw MinimongoError("The positional operator did not find the " + // 125 "match needed from the query"); // 126 } // 127 keypart = options.arrayIndices[0]; // 128 usedArrayIndex = true; // 129 } else if (isNumericKey(keypart)) { // 130 keypart = parseInt(keypart); // 131 } else { // 132 if (options.noCreate) // 133 return undefined; // 134 throw MinimongoError( // 135 "can't append to array using string field name [" // 136 + keypart + "]"); // 137 } // 138 if (last) // 139 // handle 'a.01' // 140 keyparts[i] = keypart; // 141 if (options.noCreate && keypart >= doc.length) // 142 return undefined; // 143 while (doc.length < keypart) // 144 doc.push(null); // 145 if (!last) { // 146 if (doc.length === keypart) // 147 doc.push({}); // 148 else if (typeof doc[keypart] !== "object") // 149 throw MinimongoError("can't modify field '" + keyparts[i + 1] + // 150 "' of list value " + JSON.stringify(doc[keypart])); // 151 } // 152 } else { // 153 if (keypart.length && keypart.substr(0, 1) === '$') // 154 throw MinimongoError("can't set field named " + keypart); // 155 if (!(keypart in doc)) { // 156 if (options.noCreate) // 157 return undefined; // 158 if (!last) // 159 doc[keypart] = {}; // 160 } // 161 } // 162 // 163 if (last) // 164 return doc; // 165 doc = doc[keypart]; // 166 } // 167 // 168 // notreached // 169 }; // 170 // 171 var NO_CREATE_MODIFIERS = { // 172 $unset: true, // 173 $pop: true, // 174 $rename: true, // 175 $pull: true, // 176 $pullAll: true // 177 }; // 178 // 179 var MODIFIERS = { // 180 $inc: function (target, field, arg) { // 181 if (typeof arg !== "number") // 182 throw MinimongoError("Modifier $inc allowed for numbers only"); // 183 if (field in target) { // 184 if (typeof target[field] !== "number") // 185 throw MinimongoError("Cannot apply $inc modifier to non-number"); // 186 target[field] += arg; // 187 } else { // 188 target[field] = arg; // 189 } // 190 }, // 191 $set: function (target, field, arg) { // 192 if (!_.isObject(target)) { // not an array or an object // 193 var e = MinimongoError("Cannot set property on non-object field"); // 194 e.setPropertyError = true; // 195 throw e; // 196 } // 197 if (target === null) { // 198 var e = MinimongoError("Cannot set property on null"); // 199 e.setPropertyError = true; // 200 throw e; // 201 } // 202 target[field] = EJSON.clone(arg); // 203 }, // 204 $setOnInsert: function (target, field, arg) { // 205 // converted to `$set` in `_modify` // 206 }, // 207 $unset: function (target, field, arg) { // 208 if (target !== undefined) { // 209 if (target instanceof Array) { // 210 if (field in target) // 211 target[field] = null; // 212 } else // 213 delete target[field]; // 214 } // 215 }, // 216 $push: function (target, field, arg) { // 217 if (target[field] === undefined) // 218 target[field] = []; // 219 if (!(target[field] instanceof Array)) // 220 throw MinimongoError("Cannot apply $push modifier to non-array"); // 221 // 222 if (!(arg && arg.$each)) { // 223 // Simple mode: not $each // 224 target[field].push(EJSON.clone(arg)); // 225 return; // 226 } // 227 // 228 // Fancy mode: $each (and maybe $slice and $sort) // 229 var toPush = arg.$each; // 230 if (!(toPush instanceof Array)) // 231 throw MinimongoError("$each must be an array"); // 232 // 233 // Parse $slice. // 234 var slice = undefined; // 235 if ('$slice' in arg) { // 236 if (typeof arg.$slice !== "number") // 237 throw MinimongoError("$slice must be a numeric value"); // 238 // XXX should check to make sure integer // 239 if (arg.$slice > 0) // 240 throw MinimongoError("$slice in $push must be zero or negative"); // 241 slice = arg.$slice; // 242 } // 243 // 244 // Parse $sort. // 245 var sortFunction = undefined; // 246 if (arg.$sort) { // 247 if (slice === undefined) // 248 throw MinimongoError("$sort requires $slice to be present"); // 249 // XXX this allows us to use a $sort whose value is an array, but that's // 250 // actually an extension of the Node driver, so it won't work // 251 // server-side. Could be confusing! // 252 // XXX is it correct that we don't do geo-stuff here? // 253 sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); // 254 for (var i = 0; i < toPush.length; i++) { // 255 if (LocalCollection._f._type(toPush[i]) !== 3) { // 256 throw MinimongoError("$push like modifiers using $sort " + // 257 "require all elements to be objects"); // 258 } // 259 } // 260 } // 261 // 262 // Actually push. // 263 for (var j = 0; j < toPush.length; j++) // 264 target[field].push(EJSON.clone(toPush[j])); // 265 // 266 // Actually sort. // 267 if (sortFunction) // 268 target[field].sort(sortFunction); // 269 // 270 // Actually slice. // 271 if (slice !== undefined) { // 272 if (slice === 0) // 273 target[field] = []; // differs from Array.slice! // 274 else // 275 target[field] = target[field].slice(slice); // 276 } // 277 }, // 278 $pushAll: function (target, field, arg) { // 279 if (!(typeof arg === "object" && arg instanceof Array)) // 280 throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); // 281 var x = target[field]; // 282 if (x === undefined) // 283 target[field] = arg; // 284 else if (!(x instanceof Array)) // 285 throw MinimongoError("Cannot apply $pushAll modifier to non-array"); // 286 else { // 287 for (var i = 0; i < arg.length; i++) // 288 x.push(arg[i]); // 289 } // 290 }, // 291 $addToSet: function (target, field, arg) { // 292 var isEach = false; // 293 if (typeof arg === "object") { // 294 //check if first key is '$each' // 295 for (var k in arg) { // 296 if (k === "$each") // 297 isEach = true; // 298 break; // 299 } // 300 } // 301 var values = isEach ? arg["$each"] : [arg]; // 302 var x = target[field]; // 303 if (x === undefined) // 304 target[field] = values; // 305 else if (!(x instanceof Array)) // 306 throw MinimongoError("Cannot apply $addToSet modifier to non-array"); // 307 else { // 308 _.each(values, function (value) { // 309 for (var i = 0; i < x.length; i++) // 310 if (LocalCollection._f._equal(value, x[i])) // 311 return; // 312 x.push(EJSON.clone(value)); // 313 }); // 314 } // 315 }, // 316 $pop: function (target, field, arg) { // 317 if (target === undefined) // 318 return; // 319 var x = target[field]; // 320 if (x === undefined) // 321 return; // 322 else if (!(x instanceof Array)) // 323 throw MinimongoError("Cannot apply $pop modifier to non-array"); // 324 else { // 325 if (typeof arg === 'number' && arg < 0) // 326 x.splice(0, 1); // 327 else // 328 x.pop(); // 329 } // 330 }, // 331 $pull: function (target, field, arg) { // 332 if (target === undefined) // 333 return; // 334 var x = target[field]; // 335 if (x === undefined) // 336 return; // 337 else if (!(x instanceof Array)) // 338 throw MinimongoError("Cannot apply $pull/pullAll modifier to non-array"); // 339 else { // 340 var out = []; // 341 if (typeof arg === "object" && !(arg instanceof Array)) { // 342 // XXX would be much nicer to compile this once, rather than // 343 // for each document we modify.. but usually we're not // 344 // modifying that many documents, so we'll let it slide for // 345 // now // 346 // 347 // XXX Minimongo.Matcher isn't up for the job, because we need // 348 // to permit stuff like {$pull: {a: {$gt: 4}}}.. something // 349 // like {$gt: 4} is not normally a complete selector. // 350 // same issue as $elemMatch possibly? // 351 var matcher = new Minimongo.Matcher(arg); // 352 for (var i = 0; i < x.length; i++) // 353 if (!matcher.documentMatches(x[i]).result) // 354 out.push(x[i]); // 355 } else { // 356 for (var i = 0; i < x.length; i++) // 357 if (!LocalCollection._f._equal(x[i], arg)) // 358 out.push(x[i]); // 359 } // 360 target[field] = out; // 361 } // 362 }, // 363 $pullAll: function (target, field, arg) { // 364 if (!(typeof arg === "object" && arg instanceof Array)) // 365 throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); // 366 if (target === undefined) // 367 return; // 368 var x = target[field]; // 369 if (x === undefined) // 370 return; // 371 else if (!(x instanceof Array)) // 372 throw MinimongoError("Cannot apply $pull/pullAll modifier to non-array"); // 373 else { // 374 var out = []; // 375 for (var i = 0; i < x.length; i++) { // 376 var exclude = false; // 377 for (var j = 0; j < arg.length; j++) { // 378 if (LocalCollection._f._equal(x[i], arg[j])) { // 379 exclude = true; // 380 break; // 381 } // 382 } // 383 if (!exclude) // 384 out.push(x[i]); // 385 } // 386 target[field] = out; // 387 } // 388 }, // 389 $rename: function (target, field, arg, keypath, doc) { // 390 if (keypath === arg) // 391 // no idea why mongo has this restriction.. // 392 throw MinimongoError("$rename source must differ from target"); // 393 if (target === null) // 394 throw MinimongoError("$rename source field invalid"); // 395 if (typeof arg !== "string") // 396 throw MinimongoError("$rename target must be a string"); // 397 if (target === undefined) // 398 return; // 399 var v = target[field]; // 400 delete target[field]; // 401 // 402 var keyparts = arg.split('.'); // 403 var target2 = findModTarget(doc, keyparts, {forbidArray: true}); // 404 if (target2 === null) // 405 throw MinimongoError("$rename target field invalid"); // 406 var field2 = keyparts.pop(); // 407 target2[field2] = v; // 408 }, // 409 $bit: function (target, field, arg) { // 410 // XXX mongo only supports $bit on integers, and we only support // 411 // native javascript numbers (doubles) so far, so we can't support $bit // 412 throw MinimongoError("$bit is not supported"); // 413 } // 414 }; // 415 // 416 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/diff.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // ordered: bool. // 1 // old_results and new_results: collections of documents. // 2 // if ordered, they are arrays. // 3 // if unordered, they are IdMaps // 4 LocalCollection._diffQueryChanges = function (ordered, oldResults, newResults, // 5 observer, options) { // 6 if (ordered) // 7 LocalCollection._diffQueryOrderedChanges( // 8 oldResults, newResults, observer, options); // 9 else // 10 LocalCollection._diffQueryUnorderedChanges( // 11 oldResults, newResults, observer, options); // 12 }; // 13 // 14 LocalCollection._diffQueryUnorderedChanges = function (oldResults, newResults, // 15 observer, options) { // 16 options = options || {}; // 17 var projectionFn = options.projectionFn || EJSON.clone; // 18 // 19 if (observer.movedBefore) { // 20 throw new Error("_diffQueryUnordered called with a movedBefore observer!"); // 21 } // 22 // 23 newResults.forEach(function (newDoc, id) { // 24 var oldDoc = oldResults.get(id); // 25 if (oldDoc) { // 26 if (observer.changed && !EJSON.equals(oldDoc, newDoc)) { // 27 var projectedNew = projectionFn(newDoc); // 28 var projectedOld = projectionFn(oldDoc); // 29 var changedFields = // 30 LocalCollection._makeChangedFields(projectedNew, projectedOld); // 31 if (! _.isEmpty(changedFields)) { // 32 observer.changed(id, changedFields); // 33 } // 34 } // 35 } else if (observer.added) { // 36 var fields = projectionFn(newDoc); // 37 delete fields._id; // 38 observer.added(newDoc._id, fields); // 39 } // 40 }); // 41 // 42 if (observer.removed) { // 43 oldResults.forEach(function (oldDoc, id) { // 44 if (!newResults.has(id)) // 45 observer.removed(id); // 46 }); // 47 } // 48 }; // 49 // 50 // 51 LocalCollection._diffQueryOrderedChanges = function (old_results, new_results, // 52 observer, options) { // 53 options = options || {}; // 54 var projectionFn = options.projectionFn || EJSON.clone; // 55 // 56 var new_presence_of_id = {}; // 57 _.each(new_results, function (doc) { // 58 if (new_presence_of_id[doc._id]) // 59 Meteor._debug("Duplicate _id in new_results"); // 60 new_presence_of_id[doc._id] = true; // 61 }); // 62 // 63 var old_index_of_id = {}; // 64 _.each(old_results, function (doc, i) { // 65 if (doc._id in old_index_of_id) // 66 Meteor._debug("Duplicate _id in old_results"); // 67 old_index_of_id[doc._id] = i; // 68 }); // 69 // 70 // ALGORITHM: // 71 // // 72 // To determine which docs should be considered "moved" (and which // 73 // merely change position because of other docs moving) we run // 74 // a "longest common subsequence" (LCS) algorithm. The LCS of the // 75 // old doc IDs and the new doc IDs gives the docs that should NOT be // 76 // considered moved. // 77 // 78 // To actually call the appropriate callbacks to get from the old state to the // 79 // new state: // 80 // 81 // First, we call removed() on all the items that only appear in the old // 82 // state. // 83 // 84 // Then, once we have the items that should not move, we walk through the new // 85 // results array group-by-group, where a "group" is a set of items that have // 86 // moved, anchored on the end by an item that should not move. One by one, we // 87 // move each of those elements into place "before" the anchoring end-of-group // 88 // item, and fire changed events on them if necessary. Then we fire a changed // 89 // event on the anchor, and move on to the next group. There is always at // 90 // least one group; the last group is anchored by a virtual "null" id at the // 91 // end. // 92 // 93 // Asymptotically: O(N k) where k is number of ops, or potentially // 94 // O(N log N) if inner loop of LCS were made to be binary search. // 95 // 96 // 97 //////// LCS (longest common sequence, with respect to _id) // 98 // (see Wikipedia article on Longest Increasing Subsequence, // 99 // where the LIS is taken of the sequence of old indices of the // 100 // docs in new_results) // 101 // // 102 // unmoved: the output of the algorithm; members of the LCS, // 103 // in the form of indices into new_results // 104 var unmoved = []; // 105 // max_seq_len: length of LCS found so far // 106 var max_seq_len = 0; // 107 // seq_ends[i]: the index into new_results of the last doc in a // 108 // common subsequence of length of i+1 <= max_seq_len // 109 var N = new_results.length; // 110 var seq_ends = new Array(N); // 111 // ptrs: the common subsequence ending with new_results[n] extends // 112 // a common subsequence ending with new_results[ptr[n]], unless // 113 // ptr[n] is -1. // 114 var ptrs = new Array(N); // 115 // virtual sequence of old indices of new results // 116 var old_idx_seq = function(i_new) { // 117 return old_index_of_id[new_results[i_new]._id]; // 118 }; // 119 // for each item in new_results, use it to extend a common subsequence // 120 // of length j <= max_seq_len // 121 for(var i=0; i 0) { // 130 if (old_idx_seq(seq_ends[j-1]) < old_idx_seq(i)) // 131 break; // 132 j--; // 133 } // 134 // 135 ptrs[i] = (j === 0 ? -1 : seq_ends[j-1]); // 136 seq_ends[j] = i; // 137 if (j+1 > max_seq_len) // 138 max_seq_len = j+1; // 139 } // 140 } // 141 // 142 // pull out the LCS/LIS into unmoved // 143 var idx = (max_seq_len === 0 ? -1 : seq_ends[max_seq_len-1]); // 144 while (idx >= 0) { // 145 unmoved.push(idx); // 146 idx = ptrs[idx]; // 147 } // 148 // the unmoved item list is built backwards, so fix that // 149 unmoved.reverse(); // 150 // 151 // the last group is always anchored by the end of the result list, which is // 152 // an id of "null" // 153 unmoved.push(new_results.length); // 154 // 155 _.each(old_results, function (doc) { // 156 if (!new_presence_of_id[doc._id]) // 157 observer.removed && observer.removed(doc._id); // 158 }); // 159 // for each group of things in the new_results that is anchored by an unmoved // 160 // element, iterate through the things before it. // 161 var startOfGroup = 0; // 162 _.each(unmoved, function (endOfGroup) { // 163 var groupId = new_results[endOfGroup] ? new_results[endOfGroup]._id : null; // 164 var oldDoc, newDoc, fields, projectedNew, projectedOld; // 165 for (var i = startOfGroup; i < endOfGroup; i++) { // 166 newDoc = new_results[i]; // 167 if (!_.has(old_index_of_id, newDoc._id)) { // 168 fields = projectionFn(newDoc); // 169 delete fields._id; // 170 observer.addedBefore && observer.addedBefore(newDoc._id, fields, groupId); // 171 observer.added && observer.added(newDoc._id, fields); // 172 } else { // 173 // moved // 174 oldDoc = old_results[old_index_of_id[newDoc._id]]; // 175 projectedNew = projectionFn(newDoc); // 176 projectedOld = projectionFn(oldDoc); // 177 fields = LocalCollection._makeChangedFields(projectedNew, projectedOld); // 178 if (!_.isEmpty(fields)) { // 179 observer.changed && observer.changed(newDoc._id, fields); // 180 } // 181 observer.movedBefore && observer.movedBefore(newDoc._id, groupId); // 182 } // 183 } // 184 if (groupId) { // 185 newDoc = new_results[endOfGroup]; // 186 oldDoc = old_results[old_index_of_id[newDoc._id]]; // 187 projectedNew = projectionFn(newDoc); // 188 projectedOld = projectionFn(oldDoc); // 189 fields = LocalCollection._makeChangedFields(projectedNew, projectedOld); // 190 if (!_.isEmpty(fields)) { // 191 observer.changed && observer.changed(newDoc._id, fields); // 192 } // 193 } // 194 startOfGroup = endOfGroup+1; // 195 }); // 196 // 197 // 198 }; // 199 // 200 // 201 // General helper for diff-ing two objects. // 202 // callbacks is an object like so: // 203 // { leftOnly: function (key, leftValue) {...}, // 204 // rightOnly: function (key, rightValue) {...}, // 205 // both: function (key, leftValue, rightValue) {...}, // 206 // } // 207 LocalCollection._diffObjects = function (left, right, callbacks) { // 208 _.each(left, function (leftValue, key) { // 209 if (_.has(right, key)) // 210 callbacks.both && callbacks.both(key, leftValue, right[key]); // 211 else // 212 callbacks.leftOnly && callbacks.leftOnly(key, leftValue); // 213 }); // 214 if (callbacks.rightOnly) { // 215 _.each(right, function(rightValue, key) { // 216 if (!_.has(left, key)) // 217 callbacks.rightOnly(key, rightValue); // 218 }); // 219 } // 220 }; // 221 // 222 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/id_map.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // LocalCollection._IdMap = function () { // 1 var self = this; // 2 IdMap.call(self, LocalCollection._idStringify, LocalCollection._idParse); // 3 }; // 4 // 5 Meteor._inherits(LocalCollection._IdMap, IdMap); // 6 // 7 // 8 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/observe.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // XXX maybe move these into another ObserveHelpers package or something // 1 // 2 // _CachingChangeObserver is an object which receives observeChanges callbacks // 3 // and keeps a cache of the current cursor state up to date in self.docs. Users // 4 // of this class should read the docs field but not modify it. You should pass // 5 // the "applyChange" field as the callbacks to the underlying observeChanges // 6 // call. Optionally, you can specify your own observeChanges callbacks which are // 7 // invoked immediately before the docs field is updated; this object is made // 8 // available as `this` to those callbacks. // 9 LocalCollection._CachingChangeObserver = function (options) { // 10 var self = this; // 11 options = options || {}; // 12 // 13 var orderedFromCallbacks = options.callbacks && // 14 LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); // 15 if (_.has(options, 'ordered')) { // 16 self.ordered = options.ordered; // 17 if (options.callbacks && options.ordered !== orderedFromCallbacks) // 18 throw Error("ordered option doesn't match callbacks"); // 19 } else if (options.callbacks) { // 20 self.ordered = orderedFromCallbacks; // 21 } else { // 22 throw Error("must provide ordered or callbacks"); // 23 } // 24 var callbacks = options.callbacks || {}; // 25 // 26 if (self.ordered) { // 27 self.docs = new OrderedDict(LocalCollection._idStringify); // 28 self.applyChange = { // 29 addedBefore: function (id, fields, before) { // 30 var doc = EJSON.clone(fields); // 31 doc._id = id; // 32 callbacks.addedBefore && callbacks.addedBefore.call( // 33 self, id, fields, before); // 34 // This line triggers if we provide added with movedBefore. // 35 callbacks.added && callbacks.added.call(self, id, fields); // 36 // XXX could `before` be a falsy ID? Technically // 37 // idStringify seems to allow for them -- though // 38 // OrderedDict won't call stringify on a falsy arg. // 39 self.docs.putBefore(id, doc, before || null); // 40 }, // 41 movedBefore: function (id, before) { // 42 var doc = self.docs.get(id); // 43 callbacks.movedBefore && callbacks.movedBefore.call(self, id, before); // 44 self.docs.moveBefore(id, before || null); // 45 } // 46 }; // 47 } else { // 48 self.docs = new LocalCollection._IdMap; // 49 self.applyChange = { // 50 added: function (id, fields) { // 51 var doc = EJSON.clone(fields); // 52 callbacks.added && callbacks.added.call(self, id, fields); // 53 doc._id = id; // 54 self.docs.set(id, doc); // 55 } // 56 }; // 57 } // 58 // 59 // The methods in _IdMap and OrderedDict used by these callbacks are // 60 // identical. // 61 self.applyChange.changed = function (id, fields) { // 62 var doc = self.docs.get(id); // 63 if (!doc) // 64 throw new Error("Unknown id for changed: " + id); // 65 callbacks.changed && callbacks.changed.call( // 66 self, id, EJSON.clone(fields)); // 67 LocalCollection._applyChanges(doc, fields); // 68 }; // 69 self.applyChange.removed = function (id) { // 70 callbacks.removed && callbacks.removed.call(self, id); // 71 self.docs.remove(id); // 72 }; // 73 }; // 74 // 75 LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) { // 76 var transform = cursor.getTransform() || function (doc) {return doc;}; // 77 var suppressed = !!observeCallbacks._suppress_initial; // 78 // 79 var observeChangesCallbacks; // 80 if (LocalCollection._observeCallbacksAreOrdered(observeCallbacks)) { // 81 // The "_no_indices" option sets all index arguments to -1 and skips the // 82 // linear scans required to generate them. This lets observers that don't // 83 // need absolute indices benefit from the other features of this API -- // 84 // relative order, transforms, and applyChanges -- without the speed hit. // 85 var indices = !observeCallbacks._no_indices; // 86 observeChangesCallbacks = { // 87 addedBefore: function (id, fields, before) { // 88 var self = this; // 89 if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) // 90 return; // 91 var doc = transform(_.extend(fields, {_id: id})); // 92 if (observeCallbacks.addedAt) { // 93 var index = indices // 94 ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; // 95 observeCallbacks.addedAt(doc, index, before); // 96 } else { // 97 observeCallbacks.added(doc); // 98 } // 99 }, // 100 changed: function (id, fields) { // 101 var self = this; // 102 if (!(observeCallbacks.changedAt || observeCallbacks.changed)) // 103 return; // 104 var doc = EJSON.clone(self.docs.get(id)); // 105 if (!doc) // 106 throw new Error("Unknown id for changed: " + id); // 107 var oldDoc = transform(EJSON.clone(doc)); // 108 LocalCollection._applyChanges(doc, fields); // 109 doc = transform(doc); // 110 if (observeCallbacks.changedAt) { // 111 var index = indices ? self.docs.indexOf(id) : -1; // 112 observeCallbacks.changedAt(doc, oldDoc, index); // 113 } else { // 114 observeCallbacks.changed(doc, oldDoc); // 115 } // 116 }, // 117 movedBefore: function (id, before) { // 118 var self = this; // 119 if (!observeCallbacks.movedTo) // 120 return; // 121 var from = indices ? self.docs.indexOf(id) : -1; // 122 // 123 var to = indices // 124 ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; // 125 // When not moving backwards, adjust for the fact that removing the // 126 // document slides everything back one slot. // 127 if (to > from) // 128 --to; // 129 observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))), // 130 from, to, before || null); // 131 }, // 132 removed: function (id) { // 133 var self = this; // 134 if (!(observeCallbacks.removedAt || observeCallbacks.removed)) // 135 return; // 136 // technically maybe there should be an EJSON.clone here, but it's about // 137 // to be removed from self.docs! // 138 var doc = transform(self.docs.get(id)); // 139 if (observeCallbacks.removedAt) { // 140 var index = indices ? self.docs.indexOf(id) : -1; // 141 observeCallbacks.removedAt(doc, index); // 142 } else { // 143 observeCallbacks.removed(doc); // 144 } // 145 } // 146 }; // 147 } else { // 148 observeChangesCallbacks = { // 149 added: function (id, fields) { // 150 if (!suppressed && observeCallbacks.added) { // 151 var doc = _.extend(fields, {_id: id}); // 152 observeCallbacks.added(transform(doc)); // 153 } // 154 }, // 155 changed: function (id, fields) { // 156 var self = this; // 157 if (observeCallbacks.changed) { // 158 var oldDoc = self.docs.get(id); // 159 var doc = EJSON.clone(oldDoc); // 160 LocalCollection._applyChanges(doc, fields); // 161 observeCallbacks.changed(transform(doc), // 162 transform(EJSON.clone(oldDoc))); // 163 } // 164 }, // 165 removed: function (id) { // 166 var self = this; // 167 if (observeCallbacks.removed) { // 168 observeCallbacks.removed(transform(self.docs.get(id))); // 169 } // 170 } // 171 }; // 172 } // 173 // 174 var changeObserver = new LocalCollection._CachingChangeObserver( // 175 {callbacks: observeChangesCallbacks}); // 176 var handle = cursor.observeChanges(changeObserver.applyChange); // 177 suppressed = false; // 178 // 179 return handle; // 180 }; // 181 // 182 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/objectid.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // LocalCollection._looksLikeObjectID = function (str) { // 1 return str.length === 24 && str.match(/^[0-9a-f]*$/); // 2 }; // 3 // 4 LocalCollection._ObjectID = function (hexString) { // 5 //random-based impl of Mongo ObjectID // 6 var self = this; // 7 if (hexString) { // 8 hexString = hexString.toLowerCase(); // 9 if (!LocalCollection._looksLikeObjectID(hexString)) { // 10 throw new Error("Invalid hexadecimal string for creating an ObjectID"); // 11 } // 12 // meant to work with _.isEqual(), which relies on structural equality // 13 self._str = hexString; // 14 } else { // 15 self._str = Random.hexString(24); // 16 } // 17 }; // 18 // 19 LocalCollection._ObjectID.prototype.toString = function () { // 20 var self = this; // 21 return "ObjectID(\"" + self._str + "\")"; // 22 }; // 23 // 24 LocalCollection._ObjectID.prototype.equals = function (other) { // 25 var self = this; // 26 return other instanceof LocalCollection._ObjectID && // 27 self.valueOf() === other.valueOf(); // 28 }; // 29 // 30 LocalCollection._ObjectID.prototype.clone = function () { // 31 var self = this; // 32 return new LocalCollection._ObjectID(self._str); // 33 }; // 34 // 35 LocalCollection._ObjectID.prototype.typeName = function() { // 36 return "oid"; // 37 }; // 38 // 39 LocalCollection._ObjectID.prototype.getTimestamp = function() { // 40 var self = this; // 41 return parseInt(self._str.substr(0, 8), 16); // 42 }; // 43 // 44 LocalCollection._ObjectID.prototype.valueOf = // 45 LocalCollection._ObjectID.prototype.toJSONValue = // 46 LocalCollection._ObjectID.prototype.toHexString = // 47 function () { return this._str; }; // 48 // 49 // Is this selector just shorthand for lookup by _id? // 50 LocalCollection._selectorIsId = function (selector) { // 51 return (typeof selector === "string") || // 52 (typeof selector === "number") || // 53 selector instanceof LocalCollection._ObjectID; // 54 }; // 55 // 56 // Is the selector just lookup by _id (shorthand or not)? // 57 LocalCollection._selectorIsIdPerhapsAsObject = function (selector) { // 58 return LocalCollection._selectorIsId(selector) || // 59 (selector && typeof selector === "object" && // 60 selector._id && LocalCollection._selectorIsId(selector._id) && // 61 _.size(selector) === 1); // 62 }; // 63 // 64 // If this is a selector which explicitly constrains the match by ID to a finite // 65 // number of documents, returns a list of their IDs. Otherwise returns // 66 // null. Note that the selector may have other restrictions so it may not even // 67 // match those document! We care about $in and $and since those are generated // 68 // access-controlled update and remove. // 69 LocalCollection._idsMatchedBySelector = function (selector) { // 70 // Is the selector just an ID? // 71 if (LocalCollection._selectorIsId(selector)) // 72 return [selector]; // 73 if (!selector) // 74 return null; // 75 // 76 // Do we have an _id clause? // 77 if (_.has(selector, '_id')) { // 78 // Is the _id clause just an ID? // 79 if (LocalCollection._selectorIsId(selector._id)) // 80 return [selector._id]; // 81 // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? // 82 if (selector._id && selector._id.$in // 83 && _.isArray(selector._id.$in) // 84 && !_.isEmpty(selector._id.$in) // 85 && _.all(selector._id.$in, LocalCollection._selectorIsId)) { // 86 return selector._id.$in; // 87 } // 88 return null; // 89 } // 90 // 91 // If this is a top-level $and, and any of the clauses constrain their // 92 // documents, then the whole selector is constrained by any one clause's // 93 // constraint. (Well, by their intersection, but that seems unlikely.) // 94 if (selector.$and && _.isArray(selector.$and)) { // 95 for (var i = 0; i < selector.$and.length; ++i) { // 96 var subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); // 97 if (subIds) // 98 return subIds; // 99 } // 100 } // 101 // 102 return null; // 103 }; // 104 // 105 EJSON.addType("oid", function (str) { // 106 return new LocalCollection._ObjectID(str); // 107 }); // 108 // 109 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/selector_projection.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Knows how to combine a mongo selector and a fields projection to a new fields // 1 // projection taking into account active fields from the passed selector. // 2 // @returns Object - projection object (same as fields option of mongo cursor) // 3 Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { // 4 var self = this; // 5 var selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); // 6 // 7 // Special case for $where operator in the selector - projection should depend // 8 // on all fields of the document. getSelectorPaths returns a list of paths // 9 // selector depends on. If one of the paths is '' (empty string) representing // 10 // the root or the whole document, complete projection should be returned. // 11 if (_.contains(selectorPaths, '')) // 12 return {}; // 13 // 14 return combineImportantPathsIntoProjection(selectorPaths, projection); // 15 }; // 16 // 17 Minimongo._pathsElidingNumericKeys = function (paths) { // 18 var self = this; // 19 return _.map(paths, function (path) { // 20 return _.reject(path.split('.'), isNumericKey).join('.'); // 21 }); // 22 }; // 23 // 24 combineImportantPathsIntoProjection = function (paths, projection) { // 25 var prjDetails = projectionDetails(projection); // 26 var tree = prjDetails.tree; // 27 var mergedProjection = {}; // 28 // 29 // merge the paths to include // 30 tree = pathsToTree(paths, // 31 function (path) { return true; }, // 32 function (node, path, fullPath) { return true; }, // 33 tree); // 34 mergedProjection = treeToPaths(tree); // 35 if (prjDetails.including) { // 36 // both selector and projection are pointing on fields to include // 37 // so we can just return the merged tree // 38 return mergedProjection; // 39 } else { // 40 // selector is pointing at fields to include // 41 // projection is pointing at fields to exclude // 42 // make sure we don't exclude important paths // 43 var mergedExclProjection = {}; // 44 _.each(mergedProjection, function (incl, path) { // 45 if (!incl) // 46 mergedExclProjection[path] = false; // 47 }); // 48 // 49 return mergedExclProjection; // 50 } // 51 }; // 52 // 53 // Returns a set of key paths similar to // 54 // { 'foo.bar': 1, 'a.b.c': 1 } // 55 var treeToPaths = function (tree, prefix) { // 56 prefix = prefix || ''; // 57 var result = {}; // 58 // 59 _.each(tree, function (val, key) { // 60 if (_.isObject(val)) // 61 _.extend(result, treeToPaths(val, prefix + key + '.')); // 62 else // 63 result[prefix + key] = val; // 64 }); // 65 // 66 return result; // 67 }; // 68 // 69 // 70 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/selector_modifier.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Returns true if the modifier applied to some document may change the result // 1 // of matching the document by selector // 2 // The modifier is always in a form of Object: // 3 // - $set // 4 // - 'a.b.22.z': value // 5 // - 'foo.bar': 42 // 6 // - $unset // 7 // - 'abc.d': 1 // 8 Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { // 9 var self = this; // 10 // safe check for $set/$unset being objects // 11 modifier = _.extend({ $set: {}, $unset: {} }, modifier); // 12 var modifiedPaths = _.keys(modifier.$set).concat(_.keys(modifier.$unset)); // 13 var meaningfulPaths = self._getPaths(); // 14 // 15 return _.any(modifiedPaths, function (path) { // 16 var mod = path.split('.'); // 17 return _.any(meaningfulPaths, function (meaningfulPath) { // 18 var sel = meaningfulPath.split('.'); // 19 var i = 0, j = 0; // 20 // 21 while (i < sel.length && j < mod.length) { // 22 if (isNumericKey(sel[i]) && isNumericKey(mod[j])) { // 23 // foo.4.bar selector affected by foo.4 modifier // 24 // foo.3.bar selector unaffected by foo.4 modifier // 25 if (sel[i] === mod[j]) // 26 i++, j++; // 27 else // 28 return false; // 29 } else if (isNumericKey(sel[i])) { // 30 // foo.4.bar selector unaffected by foo.bar modifier // 31 return false; // 32 } else if (isNumericKey(mod[j])) { // 33 j++; // 34 } else if (sel[i] === mod[j]) // 35 i++, j++; // 36 else // 37 return false; // 38 } // 39 // 40 // One is a prefix of another, taking numeric fields into account // 41 return true; // 42 }); // 43 }); // 44 }; // 45 // 46 // Minimongo.Sorter gets a similar method, which delegates to a Matcher it made // 47 // for this exact purpose. // 48 Minimongo.Sorter.prototype.affectedByModifier = function (modifier) { // 49 var self = this; // 50 return self._selectorForAffectedByModifier.affectedByModifier(modifier); // 51 }; // 52 // 53 // @param modifier - Object: MongoDB-styled modifier with `$set`s and `$unsets` // 54 // only. (assumed to come from oplog) // 55 // @returns - Boolean: if after applying the modifier, selector can start // 56 // accepting the modified value. // 57 // NOTE: assumes that document affected by modifier didn't match this Matcher // 58 // before, so if modifier can't convince selector in a positive change it would // 59 // stay 'false'. // 60 // Currently doesn't support $-operators and numeric indices precisely. // 61 Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // 62 var self = this; // 63 if (!this.affectedByModifier(modifier)) // 64 return false; // 65 // 66 modifier = _.extend({$set:{}, $unset:{}}, modifier); // 67 var modifierPaths = _.keys(modifier.$set).concat(_.keys(modifier.$unset)); // 68 // 69 if (!self.isSimple()) // 70 return true; // 71 // 72 if (_.any(self._getPaths(), pathHasNumericKeys) || // 73 _.any(modifierPaths, pathHasNumericKeys)) // 74 return true; // 75 // 76 // check if there is a $set or $unset that indicates something is an // 77 // object rather than a scalar in the actual object where we saw $-operator // 78 // NOTE: it is correct since we allow only scalars in $-operators // 79 // Example: for selector {'a.b': {$gt: 5}} the modifier {'a.b.c':7} would // 80 // definitely set the result to false as 'a.b' appears to be an object. // 81 var expectedScalarIsObject = _.any(self._selector, function (sel, path) { // 82 if (! isOperatorObject(sel)) // 83 return false; // 84 return _.any(modifierPaths, function (modifierPath) { // 85 return startsWith(modifierPath, path + '.'); // 86 }); // 87 }); // 88 // 89 if (expectedScalarIsObject) // 90 return false; // 91 // 92 // See if we can apply the modifier on the ideally matching object. If it // 93 // still matches the selector, then the modifier could have turned the real // 94 // object in the database into something matching. // 95 var matchingDocument = EJSON.clone(self.matchingDocument()); // 96 // 97 // The selector is too complex, anything can happen. // 98 if (matchingDocument === null) // 99 return true; // 100 // 101 try { // 102 LocalCollection._modify(matchingDocument, modifier); // 103 } catch (e) { // 104 // Couldn't set a property on a field which is a scalar or null in the // 105 // selector. // 106 // Example: // 107 // real document: { 'a.b': 3 } // 108 // selector: { 'a': 12 } // 109 // converted selector (ideal document): { 'a': 12 } // 110 // modifier: { $set: { 'a.b': 4 } } // 111 // We don't know what real document was like but from the error raised by // 112 // $set on a scalar field we can reason that the structure of real document // 113 // is completely different. // 114 if (e.name === "MinimongoError" && e.setPropertyError) // 115 return false; // 116 throw e; // 117 } // 118 // 119 return self.documentMatches(matchingDocument).result; // 120 }; // 121 // 122 // Returns an object that would match the selector if possible or null if the // 123 // selector is too complex for us to analyze // 124 // { 'a.b': { ans: 42 }, 'foo.bar': null, 'foo.baz': "something" } // 125 // => { a: { b: { ans: 42 } }, foo: { bar: null, baz: "something" } } // 126 Minimongo.Matcher.prototype.matchingDocument = function () { // 127 var self = this; // 128 // 129 // check if it was computed before // 130 if (self._matchingDocument !== undefined) // 131 return self._matchingDocument; // 132 // 133 // If the analysis of this selector is too hard for our implementation // 134 // fallback to "YES" // 135 var fallback = false; // 136 self._matchingDocument = pathsToTree(self._getPaths(), // 137 function (path) { // 138 var valueSelector = self._selector[path]; // 139 if (isOperatorObject(valueSelector)) { // 140 // if there is a strict equality, there is a good // 141 // chance we can use one of those as "matching" // 142 // dummy value // 143 if (valueSelector.$in) { // 144 var matcher = new Minimongo.Matcher({ placeholder: valueSelector }); // 145 // 146 // Return anything from $in that matches the whole selector for this // 147 // path. If nothing matches, returns `undefined` as nothing can make // 148 // this selector into `true`. // 149 return _.find(valueSelector.$in, function (x) { // 150 return matcher.documentMatches({ placeholder: x }).result; // 151 }); // 152 } else if (onlyContainsKeys(valueSelector, ['$gt', '$gte', '$lt', '$lte'])) { // 153 var lowerBound = -Infinity, upperBound = Infinity; // 154 _.each(['$lte', '$lt'], function (op) { // 155 if (_.has(valueSelector, op) && valueSelector[op] < upperBound) // 156 upperBound = valueSelector[op]; // 157 }); // 158 _.each(['$gte', '$gt'], function (op) { // 159 if (_.has(valueSelector, op) && valueSelector[op] > lowerBound) // 160 lowerBound = valueSelector[op]; // 161 }); // 162 // 163 var middle = (lowerBound + upperBound) / 2; // 164 var matcher = new Minimongo.Matcher({ placeholder: valueSelector }); // 165 if (!matcher.documentMatches({ placeholder: middle }).result && // 166 (middle === lowerBound || middle === upperBound)) // 167 fallback = true; // 168 // 169 return middle; // 170 } else if (onlyContainsKeys(valueSelector, ['$nin',' $ne'])) { // 171 // Since self._isSimple makes sure $nin and $ne are not combined with // 172 // objects or arrays, we can confidently return an empty object as it // 173 // never matches any scalar. // 174 return {}; // 175 } else { // 176 fallback = true; // 177 } // 178 } // 179 return self._selector[path]; // 180 }, // 181 _.identity /*conflict resolution is no resolution*/); // 182 // 183 if (fallback) // 184 self._matchingDocument = null; // 185 // 186 return self._matchingDocument; // 187 }; // 188 // 189 var getPaths = function (sel) { // 190 return _.keys(new Minimongo.Matcher(sel)._paths); // 191 return _.chain(sel).map(function (v, k) { // 192 // we don't know how to handle $where because it can be anything // 193 if (k === "$where") // 194 return ''; // matches everything // 195 // we branch from $or/$and/$nor operator // 196 if (_.contains(['$or', '$and', '$nor'], k)) // 197 return _.map(v, getPaths); // 198 // the value is a literal or some comparison operator // 199 return k; // 200 }).flatten().uniq().value(); // 201 }; // 202 // 203 // A helper to ensure object has only certain keys // 204 var onlyContainsKeys = function (obj, keys) { // 205 return _.all(obj, function (v, k) { // 206 return _.contains(keys, k); // 207 }); // 208 }; // 209 // 210 var pathHasNumericKeys = function (path) { // 211 return _.any(path.split('.'), isNumericKey); // 212 } // 213 // 214 // XXX from Underscore.String (http://epeli.github.com/underscore.string/) // 215 var startsWith = function(str, starts) { // 216 return str.length >= starts.length && // 217 str.substring(0, starts.length) === starts; // 218 }; // 219 // 220 // 221 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function () { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/minimongo/sorter_projection.js // // // /////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Minimongo.Sorter.prototype.combineIntoProjection = function (projection) { // 1 var self = this; // 2 var specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); // 3 return combineImportantPathsIntoProjection(specPaths, projection); // 4 }; // 5 // 6 /////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); /* Exports */ if (typeof Package === 'undefined') Package = {}; Package.minimongo = { LocalCollection: LocalCollection, Minimongo: Minimongo, MinimongoTest: MinimongoTest }; })(); //# sourceMappingURL=minimongo.js.map