From 53b3e59b3e61ff2e83a5d0cf5510a7ed336f0185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Mon, 10 Jul 2017 20:03:34 +0200 Subject: [PATCH 01/28] Removed underscore from minimongo. --- packages/minimongo/helpers.js | 6 +- packages/minimongo/minimongo.js | 58 +++++++-------- packages/minimongo/minimongo_tests.js | 87 +++++++++++----------- packages/minimongo/modify.js | 24 +++--- packages/minimongo/objectid.js | 12 +-- packages/minimongo/observe.js | 6 +- packages/minimongo/package.js | 2 - packages/minimongo/projection.js | 54 +++++++------- packages/minimongo/selector.js | 68 ++++++++--------- packages/minimongo/selector_modifier.js | 52 +++++++------ packages/minimongo/selector_projection.js | 16 ++-- packages/minimongo/sort.js | 57 +++++++------- packages/minimongo/validation.js | 4 +- packages/minimongo/wrap_transform.js | 4 +- packages/minimongo/wrap_transform_tests.js | 4 +- 15 files changed, 234 insertions(+), 220 deletions(-) diff --git a/packages/minimongo/helpers.js b/packages/minimongo/helpers.js index a21c1dc49c..aa76b1f57b 100644 --- a/packages/minimongo/helpers.js +++ b/packages/minimongo/helpers.js @@ -2,7 +2,7 @@ // arrays. // XXX maybe this should be EJSON.isArray isArray = function (x) { - return _.isArray(x) && !EJSON.isBinary(x); + return Array.isArray(x) && !EJSON.isBinary(x); }; // XXX maybe this should be EJSON.isObject, though EJSON doesn't know about @@ -24,7 +24,7 @@ isOperatorObject = function (valueSelector, inconsistentOK) { return false; var theseAreOperators = undefined; - _.each(valueSelector, function (value, selKey) { + Object.keys(valueSelector).forEach(function (selKey) { var thisIsOperator = selKey.substr(0, 1) === '$'; if (theseAreOperators === undefined) { theseAreOperators = thisIsOperator; @@ -42,4 +42,4 @@ isOperatorObject = function (valueSelector, inconsistentOK) { // string can be converted to integer isNumericKey = function (s) { return /^[0-9]+$/.test(s); -}; \ No newline at end of file +}; diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index b97663a8a7..d02bcd40a3 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -170,7 +170,7 @@ LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) { movedBefore: true}); } - _.each(objects, function (elt, i) { + objects.forEach(function (elt, i) { // This doubles as a clone operation. elt = self._projectionFn(elt); @@ -298,7 +298,7 @@ LocalCollection.ObserveHandle = function () {}; // XXX maybe callbacks should take a list of objects, to expose transactions? // XXX maybe support field limiting (to limit what you're notified on) -_.extend(LocalCollection.Cursor.prototype, { +Object.assign(LocalCollection.Cursor.prototype, { /** * @summary Watch a query. Receive callbacks as the result set changes. * @locus Anywhere @@ -388,11 +388,9 @@ _.extend(LocalCollection.Cursor.prototype, { } if (!options._suppress_initial && !self.collection.paused) { - // XXX unify ordered and unordered interface - var each = ordered - ? _.bind(_.each, null, query.results) - : _.bind(query.results.forEach, query.results); - each(function (doc) { + var results = query.results._map || query.results; + Object.keys(results).forEach(function (key) { + var doc = results[key]; var fields = EJSON.clone(doc); delete fields._id; @@ -403,7 +401,7 @@ _.extend(LocalCollection.Cursor.prototype, { } var handle = new LocalCollection.ObserveHandle; - _.extend(handle, { + Object.assign(handle, { collection: self.collection, stop: function () { if (self.reactive) @@ -525,17 +523,16 @@ LocalCollection.Cursor.prototype._depend = function (changers, _allow_unordered) if (Tracker.active) { var v = new Tracker.Dependency; v.depend(); - var notifyChange = _.bind(v.changed, v); + var notifyChange = v.changed.bind(v); var options = { _suppress_initial: true, _allow_unordered: _allow_unordered }; - _.each(['added', 'changed', 'removed', 'addedBefore', 'movedBefore'], - function (fnName) { - if (changers[fnName]) - options[fnName] = notifyChange; - }); + ['added', 'changed', 'removed', 'addedBefore', 'movedBefore'].forEach(function (fnName) { + if (changers[fnName]) + options[fnName] = notifyChange; + }); // observeChanges will stop() when this computation is invalidated self.observeChanges(options); @@ -550,7 +547,7 @@ LocalCollection.prototype.insert = function (doc, callback) { assertHasValidFieldNames(doc); - if (!_.has(doc, '_id')) { + if (!doc.hasOwnProperty('_id')) { // if you really want to use ObjectIDs, set this global. // Mongo.Collection specifies its own ids and does not use this code. doc._id = LocalCollection._useOID ? new MongoID.ObjectID() @@ -580,7 +577,7 @@ LocalCollection.prototype.insert = function (doc, callback) { } } - _.each(queriesToRecompute, function (qid) { + queriesToRecompute.forEach(function (qid) { if (self.queries[qid]) self._recomputeResults(self.queries[qid]); }); @@ -626,7 +623,8 @@ LocalCollection.prototype.remove = function (selector, callback) { if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) { var result = self._docs.size(); self._docs.clear(); - _.each(self.queries, function (query) { + Object.keys(self.queries).forEach(function (qid) { + var query = self.queries[qid]; if (query.ordered) { query.results = []; } else { @@ -653,7 +651,8 @@ LocalCollection.prototype.remove = function (selector, callback) { for (var i = 0; i < remove.length; i++) { var removeId = remove[i]; var removeDoc = self._docs.get(removeId); - _.each(self.queries, function (query, qid) { + Object.keys(self.queries).forEach(function (qid) { + var query = self.queries[qid]; if (query.dirty) return; if (query.matcher.documentMatches(removeDoc).result) { @@ -668,14 +667,14 @@ LocalCollection.prototype.remove = function (selector, callback) { } // run live query callbacks _after_ we've removed the documents. - _.each(queryRemove, function (remove) { + queryRemove.forEach(function (remove) { var query = self.queries[remove.qid]; if (query) { query.distances && query.distances.remove(remove.doc._id); LocalCollection._removeFromResults(query, remove.doc); } }); - _.each(queriesToRecompute, function (qid) { + queriesToRecompute.forEach(function (qid) { var query = self.queries[qid]; if (query) self._recomputeResults(query); @@ -711,7 +710,8 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { var docMap = new LocalCollection._IdMap; var idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector); - _.each(self.queries, function (query, qid) { + Object.keys(self.queries).forEach(function (qid) { + var query = self.queries[qid]; if ((query.cursor.skip || query.cursor.limit) && ! self.paused) { // Catch the case of a reactive `count()` on a cursor with skip // or limit, which registers an unordered observe. This is a @@ -737,7 +737,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { } else { var docToMemoize; - if (idsMatchedBySelector && !_.any(idsMatchedBySelector, function(id) { + if (idsMatchedBySelector && !idsMatchedBySelector.some(function(id) { return EJSON.equals(id, doc._id); })) { docToMemoize = doc; @@ -770,7 +770,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { return true; }); - _.each(recomputeQids, function (dummy, qid) { + Object.keys(recomputeQids).forEach(function (qid) { var query = self.queries[qid]; if (query) self._recomputeResults(query, qidToOriginalResults[qid]); @@ -783,8 +783,8 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { var insertedId; if (updateCount === 0 && options.upsert) { - let selectorModifier = LocalCollection._selectorIsId(selector) - ? { _id: selector } + let selectorModifier = LocalCollection._selectorIsId(selector) + ? { _id: selector } : selector; selectorModifier = LocalCollection._removeDollarOperators(selectorModifier); @@ -795,7 +795,7 @@ LocalCollection.prototype.update = function (selector, mod, options, callback) { delete selectorModifier._id; } - // This double _modify call is made to help work around an issue where collection + // This double _modify call is made to help work around an issue where collection // upserts won't work properly, with nested properties (see issue #8631). LocalCollection._modify(newDoc, {$set: selectorModifier}); LocalCollection._modify(newDoc, mod, {isInsert: true}); @@ -836,7 +836,7 @@ LocalCollection.prototype.upsert = function (selector, mod, options, callback) { callback = options; options = {}; } - return self.update(selector, mod, _.extend({}, options, { + return self.update(selector, mod, Object.assign({}, options, { upsert: true, _returnObject: true }), callback); @@ -945,7 +945,7 @@ LocalCollection._updateInResults = function (query, doc, old_doc) { projectionFn(doc), projectionFn(old_doc)); if (!query.ordered) { - if (!_.isEmpty(changedFields)) { + if (Object.keys(changedFields).length) { query.changed(doc._id, changedFields); query.results.set(doc._id, doc); } @@ -954,7 +954,7 @@ LocalCollection._updateInResults = function (query, doc, old_doc) { var orig_idx = LocalCollection._findInOrderedResults(query, doc); - if (!_.isEmpty(changedFields)) + if (Object.keys(changedFields).length) query.changed(doc._id, changedFields); if (!query.sorter) return; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 8e85819d29..c3081b22a1 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -241,7 +241,11 @@ Tinytest.add("minimongo - transform", function (test) { }); // transformed documents get _id field transplanted if not present - var transformWithoutId = function (doc) { return _.omit(doc, '_id'); }; + var transformWithoutId = function (doc) { + var docWithoutId = Object.assign({}, doc); + delete docWithoutId._id; + return docWithoutId; + }; test.equal(c.findOne({}, {transform: transformWithoutId})._id, c.findOne()._id); }); @@ -333,8 +337,8 @@ Tinytest.add("minimongo - selector_compiler", function (test) { } }; - var match = _.bind(matches, null, true); - var nomatch = _.bind(matches, null, false); + var match = matches.bind(null, true); + var nomatch = matches.bind(null, false); // XXX blog post about what I learned while writing these tests (weird // mongo edge cases) @@ -513,14 +517,14 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: {$mod: [10, 1]}}, {a: 12}); match({a: {$mod: [10, 1]}}, {a: [10, 11, 12]}); nomatch({a: {$mod: [10, 1]}}, {a: [10, 12]}); - _.each([ + [ 5, [10], [10, 1, 2], "foo", {bar: 1}, [] - ], function (badMod) { + ].forEach(function (badMod) { test.throws(function () { match({a: {$mod: badMod}}, {a: 11}); }); @@ -866,9 +870,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: {$bitsAllSet: 1}}, {a: ['a', 'b']}) nomatch({a: {$bitsAllSet: 1}}, {a: {foo: 'bar'}}) nomatch({a: {$bitsAllSet: 1}}, {a: 1.2}) - nomatch({a: {$bitsAllSet: 1}}, {a: "1"}) + nomatch({a: {$bitsAllSet: 1}}, {a: "1"}); - _.each([ + [ false, NaN, Infinity, @@ -879,7 +883,7 @@ Tinytest.add("minimongo - selector_compiler", function (test) { 1.2, "1", [0, -1] - ], function (badValue) { + ].forEach(function (badValue) { test.throws(function () { match({a: {$bitsAllSet: badValue}}, {a: 42}); }); @@ -1441,10 +1445,10 @@ Tinytest.add("minimongo - projection_compiler", function (test) { var testProjection = function (projection, tests) { var projection_f = LocalCollection._compileProjection(projection); var equalNonStrict = function (a, b, desc) { - test.isTrue(_.isEqual(a, b), desc); + test.isTrue(EJSON.equals(a, b), desc); }; - _.each(tests, function (testCase) { + tests.forEach(function (testCase) { equalNonStrict(projection_f(testCase[0]), testCase[1], testCase[2]); }); }; @@ -1570,7 +1574,7 @@ Tinytest.add("minimongo - projection_compiler", function (test) { Tinytest.add("minimongo - fetch with fields", function (test) { var c = new LocalCollection(); - _.times(30, function (i) { + Array.from({length: 30}, function (_, i) { c.insert({ something: Random.id(), anything: { @@ -1588,14 +1592,14 @@ Tinytest.add("minimongo - fetch with fields", function (test) { 'anything.foo': 1 } }).fetch(); - test.isTrue(_.all(fetchResults, function (x) { + test.isTrue(fetchResults.every(function (x) { return x && x.something && x.anything && x.anything.foo && x.anything.foo === "bar" && - !_.has(x, 'nothing') && - !_.has(x.anything, 'cool'); + !x.hasOwnProperty('nothing') && + !x.anything.hasOwnProperty('cool'); })); // Test with a selector, even field used in the selector is excluded in the @@ -1606,13 +1610,13 @@ Tinytest.add("minimongo - fetch with fields", function (test) { fields: { nothing: 0 } }).fetch(); - test.isTrue(_.all(fetchResults, function (x) { + test.isTrue(fetchResults.every(function (x) { return x && x.something && x.anything && x.anything.foo === "bar" && x.anything.cool === "hot" && - !_.has(x, 'nothing') && + !x.hasOwnProperty('nothing') && x.i && x.i >= 5; })); @@ -1634,13 +1638,13 @@ Tinytest.add("minimongo - fetch with fields", function (test) { } }).fetch(); - test.isTrue(_.all(fetchResults, function (x) { + test.isTrue(fetchResults.every(function (x) { return x && x.something && x.i >= 10 && x.i < 20; })); - _.each(fetchResults, function (x, i, arr) { + fetchResults.forEach(function (x, i, arr) { if (!i) return; test.isTrue(x.i === arr[i-1].i + 1); }); @@ -1685,7 +1689,7 @@ Tinytest.add("minimongo - fetch with projection, subarrays", function (test) { }); var equalNonStrict = function (a, b, desc) { - test.isTrue(_.isEqual(a, b), desc); + test.isTrue(EJSON.equals(a, b), desc); }; var testForProjection = function (projection, expected) { @@ -1796,7 +1800,7 @@ Tinytest.add("minimongo - observe ordered with projection", function (test) { handle.stop(); // test _suppress_initial - handle = c.find({}, {sort: {a: -1}, fields: { a: 1 }}).observe(_.extend(cbs, {_suppress_initial: true})); + handle = c.find({}, {sort: {a: -1}, fields: { a: 1 }}).observe(Object.assign(cbs, {_suppress_initial: true})); test.equal(operations.shift(), undefined); c.insert({a:100, b: { foo: "bar" }}); test.equal(operations.shift(), ['added', {a:100}, 0, idA2]); @@ -1826,7 +1830,7 @@ Tinytest.add("minimongo - observe ordered with projection", function (test) { // test _no_indices c.remove({}); - handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(_.extend(cbs, {_no_indices: true})); + handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(Object.assign(cbs, {_no_indices: true})); c.insert({_id: 'foo', a:1, zoo: "crazy"}); test.equal(operations.shift(), ['added', {a:1}, -1, null]); c.update({a:1}, {$set: {a: 2, foobar: "player"}}); @@ -1875,7 +1879,7 @@ Tinytest.add("minimongo - ordering", function (test) { // document ordering under a sort specification var verify = function (sorts, docs) { - _.each(_.isArray(sorts) ? sorts : [sorts], function (sort) { + (Array.isArray(sorts) ? sorts : [sorts]).forEach(function (sort) { var sorter = new Minimongo.Sorter(sort); assert_ordering(test, sorter.getComparator(), docs); }); @@ -2050,25 +2054,25 @@ Tinytest.add("minimongo - subkey sort", function (test) { c.insert({a: {b: 1}}); c.insert({a: {b: 3}}); test.equal( - _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), + c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), [{b: 3}, {b: 2}, {b: 1}]); // isn't an object c.insert({a: 1}); test.equal( - _.pluck(c.find({}, {sort: {'a.b': 1}}).fetch(), 'a'), + c.find({}, {sort: {'a.b': 1}}).fetch().map(function (doc) { return doc.a; }), [1, {b: 1}, {b: 2}, {b: 3}]); // complex object c.insert({a: {b: {c: 1}}}); test.equal( - _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), + c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1]); // no such top level prop c.insert({c: 1}); test.equal( - _.pluck(c.find({}, {sort: {'a.b': -1}}).fetch(), 'a'), + c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1, undefined]); // no such mid level prop. just test that it doesn't throw. @@ -2099,11 +2103,11 @@ Tinytest.add("minimongo - array sort", function (test) { var testCursorMatchesField = function (cursor, field) { var fieldValues = []; c.find().forEach(function (doc) { - if (_.has(doc, field)) + if (doc.hasOwnProperty(field)) fieldValues.push(doc[field]); }); - test.equal(_.pluck(cursor.fetch(), field), - _.range(_.max(fieldValues) + 1)); + test.equal(cursor.fetch().map(function (doc) { return doc[field]; }), + Array.from({length: Math.max.apply(null, fieldValues) + 1}, function (_, i) { return i; })); }; testCursorMatchesField(c.find({}, {sort: {'a.x': 1}}), 'up'); @@ -2115,7 +2119,7 @@ Tinytest.add("minimongo - array sort", function (test) { Tinytest.add("minimongo - sort keys", function (test) { var keyListToObject = function (keyList) { var obj = {}; - _.each(keyList, function (key) { + keyList.forEach(function (key) { obj[EJSON.stringify(key)] = true; }); return obj; @@ -2961,7 +2965,7 @@ Tinytest.add("minimongo - observe ordered", function (test) { handle.stop(); // test _suppress_initial - handle = c.find({}, {sort: {a: -1}}).observe(_.extend({ + handle = c.find({}, {sort: {a: -1}}).observe(Object.assign({ _suppress_initial: true}, cbs)); test.equal(operations.shift(), undefined); c.insert({a:100}); @@ -3007,7 +3011,7 @@ Tinytest.add("minimongo - observe ordered", function (test) { // test _no_indices c.remove({}); - handle = c.find({}, {sort: {a: 1}}).observe(_.extend(cbs, {_no_indices: true})); + handle = c.find({}, {sort: {a: 1}}).observe(Object.assign(cbs, {_no_indices: true})); c.insert({_id: 'foo', a:1}); test.equal(operations.shift(), ['added', {a:1}, -1, null]); c.update({a:1}, {$set: {a: 2}}); @@ -3027,14 +3031,14 @@ Tinytest.add("minimongo - observe ordered", function (test) { handle.stop(); }); -_.each([true, false], function (ordered) { +[true, false].forEach(function (ordered) { Tinytest.add("minimongo - observe ordered: " + ordered, function (test) { var c = new LocalCollection(); var ev = ""; var makecb = function (tag) { var ret = {}; - _.each(["added", "changed", "removed"], function (fn) { + ["added", "changed", "removed"].forEach(function (fn) { var fnName = ordered ? fn + "At" : fn; ret[fnName] = function (doc) { ev = (ev + fn.substr(0, 1) + tag + doc._id + "_"); @@ -3112,8 +3116,8 @@ Tinytest.add("minimongo - saveOriginals", function (test) { // Verify the originals. var originals = c.retrieveOriginals(); var affected = ['bar', 'baz', 'quux', 'whoa', 'hooray']; - test.equal(originals.size(), _.size(affected)); - _.each(affected, function (id) { + test.equal(originals.size(), affected.length); + affected.forEach(function (id) { test.isTrue(originals.has(id)); }); test.equal(originals.get('bar'), {_id: 'bar', x: 'updateme'}); @@ -3378,7 +3382,7 @@ Tinytest.add("minimongo - $near operator tests", function (test) { test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 30 } }).count(), 3); test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 1); var points = coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 6 } }).fetch(); - _.each(points, function (point, i, points) { + points.forEach(function (point, i, points) { test.isTrue(!i || distance([0, 0], point.rest.loc) >= distance([0, 0], points[i - 1].rest.loc)); }); @@ -3397,7 +3401,7 @@ Tinytest.add("minimongo - $near operator tests", function (test) { { "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879734156 ] } } ]; - _.each(data, function (x, i) { coll.insert(_.extend(x, { x: i })); }); + data.forEach(function (x, i) { coll.insert(Object.assign(x, { x: i })); }); var close15 = coll.find({ location: { $near: { $geometry: { type: "Point", @@ -3478,8 +3482,7 @@ Tinytest.add("minimongo - $near operator tests", function (test) { a: {b: [5, 5]}}); var testNear = function (near, md, expected) { test.equal( - _.pluck( - coll.find({'a.b': {$near: near, $maxDistance: md}}).fetch(), '_id'), + coll.find({'a.b': {$near: near, $maxDistance: md}}).fetch().map(function (doc) { return doc._id }), expected); }; testNear([149, 149], 4, ['x']); @@ -3492,10 +3495,10 @@ Tinytest.add("minimongo - $near operator tests", function (test) { // issue #3599 // Ensure that distance is not used as a tie-breaker for sort. test.equal( - _.pluck(coll.find({'a.b': {$near: [1, 1]}}, {sort: {k: 1}}).fetch(), '_id'), + coll.find({'a.b': {$near: [1, 1]}}, {sort: {k: 1}}).fetch().map(function (doc) { return doc._id; }), ['x', 'y']); test.equal( - _.pluck(coll.find({'a.b': {$near: [5, 5]}}, {sort: {k: 1}}).fetch(), '_id'), + coll.find({'a.b': {$near: [5, 5]}}, {sort: {k: 1}}).fetch().map(function (doc) { return doc._id; }), ['x', 'y']); var operations = []; diff --git a/packages/minimongo/modify.js b/packages/minimongo/modify.js index e0df49a274..5c5e025f34 100644 --- a/packages/minimongo/modify.js +++ b/packages/minimongo/modify.js @@ -35,14 +35,16 @@ LocalCollection._modify = function (doc, mod, options) { // apply modifiers to the doc. newDoc = EJSON.clone(doc); - _.each(mod, function (operand, op) { + Object.keys(mod).forEach(function (op) { + var operand = mod[op]; var modFunc = MODIFIERS[op]; // Treat $setOnInsert as $set if this is an insert. if (options.isInsert && op === '$setOnInsert') modFunc = MODIFIERS['$set']; if (!modFunc) throw MinimongoError("Invalid modifier specified " + op); - _.each(operand, function (arg, keypath) { + Object.keys(operand).forEach(function (keypath) { + var arg = operand[keypath]; if (keypath === '') { throw MinimongoError("An empty update path is not valid."); } @@ -53,13 +55,13 @@ LocalCollection._modify = function (doc, mod, options) { var keyparts = keypath.split('.'); - if (! _.all(keyparts, _.identity)) { + if (!keyparts.every(Boolean)) { throw MinimongoError( "The update path '" + keypath + "' contains an empty field name, which is not allowed."); } - var noCreate = _.has(NO_CREATE_MODIFIERS, op); + var noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); var forbidArray = (op === "$rename"); var target = findModTarget(newDoc, keyparts, { noCreate: NO_CREATE_MODIFIERS[op], @@ -73,15 +75,15 @@ LocalCollection._modify = function (doc, mod, options) { } // move new document into place. - _.each(_.keys(doc), function (k) { + Object.keys(doc).forEach(function (k) { // Note: this used to be for (var k in doc) however, this does not // work right in Opera. Deleting from a doc while iterating over it // would sometimes cause opera to skip some keys. if (k !== '_id') delete doc[k]; }); - _.each(newDoc, function (v, k) { - doc[k] = v; + Object.keys(newDoc).forEach(function (k) { + doc[k] = newDoc[k]; }); }; @@ -237,7 +239,7 @@ var MODIFIERS = { } }, $set: function (target, field, arg) { - if (!_.isObject(target)) { // not an array or an object + if (target !== Object(target)) { // not an array or an object var e = MinimongoError( "Cannot set property on non-object field", { field }); e.setPropertyError = true; @@ -343,8 +345,8 @@ var MODIFIERS = { target[field] = []; // differs from Array.slice! else if (slice < 0) target[field] = target[field].slice(slice); - else - target[field] = target[field].slice(0, slice); + else + target[field] = target[field].slice(0, slice); } }, $pushAll: function (target, field, arg) { @@ -380,7 +382,7 @@ var MODIFIERS = { throw MinimongoError( "Cannot apply $addToSet modifier to non-array", { field }); else { - _.each(values, function (value) { + values.forEach(function (value) { for (var i = 0; i < x.length; i++) if (LocalCollection._f._equal(value, x[i])) return; diff --git a/packages/minimongo/objectid.js b/packages/minimongo/objectid.js index 3acf04e502..fdf529a356 100644 --- a/packages/minimongo/objectid.js +++ b/packages/minimongo/objectid.js @@ -10,7 +10,7 @@ LocalCollection._selectorIsIdPerhapsAsObject = function (selector) { return LocalCollection._selectorIsId(selector) || (selector && typeof selector === "object" && selector._id && LocalCollection._selectorIsId(selector._id) && - _.size(selector) === 1); + Object.keys(selector).length === 1); }; // If this is a selector which explicitly constrains the match by ID to a finite @@ -26,15 +26,15 @@ LocalCollection._idsMatchedBySelector = function (selector) { return null; // Do we have an _id clause? - if (_.has(selector, '_id')) { + if (selector.hasOwnProperty('_id')) { // Is the _id clause just an ID? if (LocalCollection._selectorIsId(selector._id)) return [selector._id]; // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? if (selector._id && selector._id.$in - && _.isArray(selector._id.$in) - && !_.isEmpty(selector._id.$in) - && _.all(selector._id.$in, LocalCollection._selectorIsId)) { + && Array.isArray(selector._id.$in) + && selector._id.$in.length + && selector._id.$in.every(LocalCollection._selectorIsId)) { return selector._id.$in; } return null; @@ -43,7 +43,7 @@ LocalCollection._idsMatchedBySelector = function (selector) { // If this is a top-level $and, and any of the clauses constrain their // documents, then the whole selector is constrained by any one clause's // constraint. (Well, by their intersection, but that seems unlikely.) - if (selector.$and && _.isArray(selector.$and)) { + if (selector.$and && Array.isArray(selector.$and)) { for (var i = 0; i < selector.$and.length; ++i) { var subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); if (subIds) diff --git a/packages/minimongo/observe.js b/packages/minimongo/observe.js index bbc5f9eb42..6733bf08f7 100644 --- a/packages/minimongo/observe.js +++ b/packages/minimongo/observe.js @@ -13,7 +13,7 @@ LocalCollection._CachingChangeObserver = function (options) { var orderedFromCallbacks = options.callbacks && LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); - if (_.has(options, 'ordered')) { + if (options.hasOwnProperty('ordered')) { self.ordered = options.ordered; if (options.callbacks && options.ordered !== orderedFromCallbacks) throw Error("ordered option doesn't match callbacks"); @@ -89,7 +89,7 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) var self = this; if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) return; - var doc = transform(_.extend(fields, {_id: id})); + var doc = transform(Object.assign(fields, {_id: id})); if (observeCallbacks.addedAt) { var index = indices ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; @@ -149,7 +149,7 @@ LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) observeChangesCallbacks = { added: function (id, fields) { if (!suppressed && observeCallbacks.added) { - var doc = _.extend(fields, {_id: id}); + var doc = Object.assign(fields, {_id: id}); observeCallbacks.added(transform(doc)); } }, diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index c389236fd5..17b63b9036 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -9,7 +9,6 @@ Package.onUse(function (api) { api.export('MinimongoTest', { testOnly: true }); api.export('MinimongoError', { testOnly: true }); api.use([ - 'underscore', 'ejson', 'id-map', 'ordered-dict', @@ -51,7 +50,6 @@ Package.onTest(function (api) { api.use('test-helpers', 'client'); api.use([ 'tinytest', - 'underscore', 'ejson', 'ordered-dict', 'random', diff --git a/packages/minimongo/projection.js b/packages/minimongo/projection.js index 863f01f71d..d586acbec8 100644 --- a/packages/minimongo/projection.js +++ b/packages/minimongo/projection.js @@ -8,22 +8,23 @@ LocalCollection._compileProjection = function (fields) { LocalCollection._checkSupportedProjection(fields); - var _idProjection = _.isUndefined(fields._id) ? true : fields._id; + var _idProjection = fields._id === undefined ? true : fields._id; var details = projectionDetails(fields); // returns transformed doc according to ruleTree var transform = function (doc, ruleTree) { // Special case for "sets" - if (_.isArray(doc)) - return _.map(doc, function (subdoc) { return transform(subdoc, ruleTree); }); + if (Array.isArray(doc)) + return doc.map(function (subdoc) { return transform(subdoc, ruleTree); }); var res = details.including ? {} : EJSON.clone(doc); - _.each(ruleTree, function (rule, key) { - if (!_.has(doc, key)) + Object.keys(ruleTree).forEach(function (key) { + var rule = ruleTree[key]; + if (!doc.hasOwnProperty(key)) return; - if (_.isObject(rule)) { + if (rule === Object(rule)) { // For sub-objects/subsets we branch - if (_.isObject(doc[key])) + if (doc[key] === Object(doc[key])) res[key] = transform(doc[key], rule); // Otherwise we don't even touch this subfield } else if (details.including) @@ -38,9 +39,9 @@ LocalCollection._compileProjection = function (fields) { return function (obj) { var res = transform(obj, details.tree); - if (_idProjection && _.has(obj, '_id')) + if (_idProjection && obj.hasOwnProperty('_id')) res._id = obj._id; - if (!_idProjection && _.has(res, '_id')) + if (!_idProjection && res.hasOwnProperty('_id')) delete res._id; return res; }; @@ -56,7 +57,7 @@ projectionDetails = function (fields) { // Find the non-_id keys (_id is handled specially because it is included unless // explicitly excluded). Sort the keys, so that our code to detect overlaps // like 'foo' and 'foo.bar' can assume that 'foo' comes first. - var fieldsKeys = _.keys(fields).sort(); + var fieldsKeys = Object.keys(fields).sort(); // If _id is the only field in the projection, do not remove it, since it is // required to determine if this is an exclusion or exclusion. Also keep an @@ -66,12 +67,12 @@ projectionDetails = function (fields) { // special case, since exclusive _id is always allowed. if (fieldsKeys.length > 0 && !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && - !(_.contains(fieldsKeys, '_id') && fields['_id'])) - fieldsKeys = _.reject(fieldsKeys, function (key) { return key === '_id'; }); + !(fieldsKeys.includes('_id') && fields['_id'])) + fieldsKeys = fieldsKeys.filter(function (key) { return key !== '_id'; }); var including = null; // Unknown - _.each(fieldsKeys, function (keyPath) { + fieldsKeys.forEach(function (keyPath) { var rule = !!fields[keyPath]; if (including === null) including = rule; @@ -126,20 +127,20 @@ projectionDetails = function (fields) { // @returns - Object: tree represented as a set of nested objects pathsToTree = function (paths, newLeafFn, conflictFn, tree) { tree = tree || {}; - _.each(paths, function (keyPath) { + paths.forEach(function (keyPath) { var treePos = tree; var pathArr = keyPath.split('.'); - // use _.all just for iteration with break - var success = _.all(pathArr.slice(0, -1), function (key, idx) { - if (!_.has(treePos, key)) + // use .every just for iteration with break + var success = pathArr.slice(0, -1).every(function (key, idx) { + if (!treePos.hasOwnProperty(key)) treePos[key] = {}; - else if (!_.isObject(treePos[key])) { + else if (treePos[key] !== Object(treePos[key])) { treePos[key] = conflictFn(treePos[key], pathArr.slice(0, idx + 1).join('.'), keyPath); // break out of loop if we are failing for this path - if (!_.isObject(treePos[key])) + if (treePos[key] !== Object(treePos[key])) return false; } @@ -148,8 +149,8 @@ pathsToTree = function (paths, newLeafFn, conflictFn, tree) { }); if (success) { - var lastKey = _.last(pathArr); - if (!_.has(treePos, lastKey)) + var lastKey = pathArr[pathArr.length - 1]; + if (!treePos.hasOwnProperty(lastKey)) treePos[lastKey] = newLeafFn(keyPath); else treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); @@ -160,15 +161,16 @@ pathsToTree = function (paths, newLeafFn, conflictFn, tree) { }; LocalCollection._checkSupportedProjection = function (fields) { - if (!_.isObject(fields) || _.isArray(fields)) + if (fields !== Object(fields) || Array.isArray(fields)) throw MinimongoError("fields option must be an object"); - _.each(fields, function (val, keyPath) { - if (_.contains(keyPath.split('.'), '$')) + Object.keys(fields).forEach(function (keyPath) { + var val = fields[keyPath]; + if (keyPath.split('.').includes('$')) throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); - if (typeof val === 'object' && _.intersection(['$elemMatch', '$meta', '$slice'], _.keys(val)).length > 0) + if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) throw MinimongoError("Minimongo doesn't support operators in projections yet."); - if (_.indexOf([1, 0, true, false], val) === -1) + if (![1, 0, true, false].includes(val)) throw MinimongoError("Projection values should be one of 1, 0, true, or false"); }); }; diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index affe510ebf..a56af7008a 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -47,7 +47,7 @@ Minimongo.Matcher = function (selector, isUpdate = false) { self._isUpdate = isUpdate; }; -_.extend(Minimongo.Matcher.prototype, { +Object.assign(Minimongo.Matcher.prototype, { documentMatches: function (doc) { if (!doc || typeof doc !== "object") { throw Error("documentMatches needs a document"); @@ -109,7 +109,7 @@ _.extend(Minimongo.Matcher.prototype, { // Returns a list of key paths the given selector is looking for. It includes // the empty string if there is a $where. _getPaths: function () { - return _.keys(this._paths); + return Object.keys(this._paths); } }); @@ -124,11 +124,12 @@ _.extend(Minimongo.Matcher.prototype, { var compileDocumentSelector = function (docSelector, matcher, options) { options = options || {}; var docMatchers = []; - _.each(docSelector, function (subSelector, key) { + Object.keys(docSelector).forEach(function (key) { + var subSelector = docSelector[key]; if (key.substr(0, 1) === '$') { // Outer operators are either logical operators (they recurse back into // this function), or $where. - if (!_.has(LOGICAL_OPERATORS, key)) + if (!LOGICAL_OPERATORS.hasOwnProperty(key)) throw new Error("Unrecognized logical operator: " + key); matcher._isSimple = false; docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, @@ -182,7 +183,7 @@ var convertElementMatcherToBranchedMatcher = function ( branches, options.dontIncludeLeafArrays); } var ret = {}; - ret.result = _.any(expanded, function (element) { + ret.result = expanded.some(function (element) { var matched = elementMatcher(element.value); // Special case for $elemMatch: it means "true, and use this as an array @@ -211,9 +212,7 @@ var convertElementMatcherToBranchedMatcher = function ( regexpElementMatcher = function (regexp) { return function (value) { if (value instanceof RegExp) { - // Comparing two regexps means seeing if the regexps are identical - // (really!). Underscore knows how. - return _.isEqual(value, regexp); + return value.toString() === regexp.toString(); } // Regexps only work against strings. if (typeof value !== 'string') @@ -258,21 +257,22 @@ var operatorBranchedMatcher = function (valueSelector, matcher, isRoot) { // is OK. var operatorMatchers = []; - _.each(valueSelector, function (operand, operator) { - var simpleRange = _.contains(['$lt', '$lte', '$gt', '$gte'], operator) && - _.isNumber(operand); - var simpleEquality = _.contains(['$ne', '$eq'], operator) && !_.isObject(operand); - var simpleInclusion = _.contains(['$in', '$nin'], operator) && - _.isArray(operand) && !_.any(operand, _.isObject); + Object.keys(valueSelector).forEach(function (operator) { + var operand = valueSelector[operator]; + var simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && + typeof operand === 'number'; + var simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); + var simpleInclusion = ['$in', '$nin'].includes(operator) && + Array.isArray(operand) && !operand.some(function (x) { return x === Object(x); }); if (! (simpleRange || simpleInclusion || simpleEquality)) { matcher._isSimple = false; } - if (_.has(VALUE_OPERATORS, operator)) { + if (VALUE_OPERATORS.hasOwnProperty(operator)) { operatorMatchers.push( VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); - } else if (_.has(ELEMENT_OPERATORS, operator)) { + } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { var options = ELEMENT_OPERATORS[operator]; operatorMatchers.push( convertElementMatcherToBranchedMatcher( @@ -289,9 +289,9 @@ var operatorBranchedMatcher = function (valueSelector, matcher, isRoot) { var compileArrayOfDocumentSelectors = function ( selectors, matcher, inElemMatch) { - if (!isArray(selectors) || _.isEmpty(selectors)) + if (!isArray(selectors) || selectors.length === 0) throw Error("$and/$or/$nor must be nonempty array"); - return _.map(selectors, function (subSelector) { + return selectors.map(function (subSelector) { if (!isPlainObject(subSelector)) throw Error("$or/$and/$nor entries need to be full objects"); return compileDocumentSelector( @@ -317,7 +317,7 @@ var LOGICAL_OPERATORS = { return matchers[0]; return function (doc) { - var result = _.any(matchers, function (f) { + var result = matchers.some(function (f) { return f(doc).result; }); // $or does NOT set arrayIndices when it has multiple @@ -330,7 +330,7 @@ var LOGICAL_OPERATORS = { var matchers = compileArrayOfDocumentSelectors( subSelector, matcher, inElemMatch); return function (doc) { - var result = _.all(matchers, function (f) { + var result = matchers.every(function (f) { return !f(doc).result; }); // Never set arrayIndices, because we only match if nothing in particular @@ -405,7 +405,7 @@ var VALUE_OPERATORS = { }, // $options just provides options for $regex; its logic is inside $regex $options: function (operand, valueSelector) { - if (!_.has(valueSelector, '$regex')) + if (!valueSelector.hasOwnProperty('$regex')) throw Error("$options needs a $regex"); return everythingMatcher; }, @@ -419,11 +419,11 @@ var VALUE_OPERATORS = { if (!isArray(operand)) throw Error("$all requires array"); // Not sure why, but this seems to be what MongoDB does. - if (_.isEmpty(operand)) + if (operand.length === 0) return nothingMatcher; var branchedMatchers = []; - _.each(operand, function (criterion) { + operand.forEach(function (criterion) { // XXX handle $all/$elemMatch combination if (isOperatorObject(criterion)) throw Error("no $ expressions in $all"); @@ -445,7 +445,7 @@ var VALUE_OPERATORS = { // matched using $geometry. var maxDistance, point, distance; - if (isPlainObject(operand) && _.has(operand, '$geometry')) { + if (isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { // GeoJSON "2dsphere" mode. maxDistance = operand.$maxDistance; point = operand.$geometry; @@ -488,7 +488,7 @@ var VALUE_OPERATORS = { // each within-$maxDistance branching point. branchedValues = expandArraysInBranches(branchedValues); var result = {result: false}; - _.every(branchedValues, function (branch) { + branchedValues.every(function (branch) { // if operation is an update, don't skip branches, just return the first one (#3599) if (!matcher._isUpdate){ if (!(typeof branch.value === "object")){ @@ -523,7 +523,7 @@ var distanceCoordinatePairs = function (a, b) { b = pointToArray(b); var x = a[0] - b[0]; var y = a[1] - b[1]; - if (_.isNaN(x) || _.isNaN(y)) + if (Number.isNaN(x) || Number.isNaN(y)) return null; return Math.sqrt(x * x + y * y); }; @@ -531,7 +531,7 @@ var distanceCoordinatePairs = function (a, b) { // the second one to y no matter what user passes. // In case user passes { lon: x, lat: y } returns [x, y] var pointToArray = function (point) { - return _.map(point, _.identity); + return Array.isArray(point) ? point.slice() : [point.x, point.y]; }; // Helper for $lt/$gt/$lte/$gte. @@ -671,7 +671,7 @@ ELEMENT_OPERATORS = { throw Error("$in needs an array"); var elementMatchers = []; - _.each(operand, function (option) { + operand.forEach(function (option) { if (option instanceof RegExp) elementMatchers.push(regexpElementMatcher(option)); else if (isOperatorObject(option)) @@ -684,7 +684,7 @@ ELEMENT_OPERATORS = { // Allow {a: {$in: [null]}} to match when 'a' does not exist. if (value === undefined) value = null; - return _.any(elementMatchers, function (e) { + return elementMatchers.some(function (e) { return e(value); }); }; @@ -801,7 +801,7 @@ ELEMENT_OPERATORS = { throw Error("$elemMatch need an object"); var subMatcher, isDocMatcher; - if (isOperatorObject(_.omit(operand, _.keys(LOGICAL_OPERATORS)), true)) { + if (isOperatorObject(Object.keys(operand).filter(key => !Object.keys(LOGICAL_OPERATORS).includes(key)).reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { subMatcher = compileValueSelector(operand, matcher); isDocMatcher = false; } else { @@ -993,7 +993,7 @@ makeLookupFunction = function (key, options) { // "look up this index" in that case, not "also look up this index in all // the elements of the array". if (isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { - _.each(firstLevel, function (branch, arrayIndex) { + firstLevel.forEach(function (branch, arrayIndex) { if (isPlainObject(branch)) { appendToResult(lookupRest( branch, @@ -1009,7 +1009,7 @@ MinimongoTest.makeLookupFunction = makeLookupFunction; expandArraysInBranches = function (branches, skipTheArrays) { var branchesOut = []; - _.each(branches, function (branch) { + branches.forEach(function (branch) { var thisIsArray = isArray(branch.value); // We include the branch itself, *UNLESS* we it's an array that we're going // to iterate and we're told to skip arrays. (That's right, we include some @@ -1022,7 +1022,7 @@ expandArraysInBranches = function (branches, skipTheArrays) { }); } if (thisIsArray && !branch.dontIterate) { - _.each(branch.value, function (leaf, i) { + branch.value.forEach(function (leaf, i) { branchesOut.push({ value: leaf, arrayIndices: (branch.arrayIndices || []).concat(i) @@ -1054,7 +1054,7 @@ var andSomeMatchers = function (subMatchers) { return function (docOrBranches) { var ret = {}; - ret.result = _.all(subMatchers, function (f) { + ret.result = subMatchers.every(function (f) { var subResult = f(docOrBranches); // Copy a 'distance' number out of the first sub-matcher that has // one. Yes, this means that if there are multiple $near fields in a diff --git a/packages/minimongo/selector_modifier.js b/packages/minimongo/selector_modifier.js index 54b37df68b..32c9f79a6f 100644 --- a/packages/minimongo/selector_modifier.js +++ b/packages/minimongo/selector_modifier.js @@ -9,13 +9,13 @@ Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { var self = this; // safe check for $set/$unset being objects - modifier = _.extend({ $set: {}, $unset: {} }, modifier); - var modifiedPaths = _.keys(modifier.$set).concat(_.keys(modifier.$unset)); + modifier = Object.assign({ $set: {}, $unset: {} }, modifier); + var modifiedPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); var meaningfulPaths = self._getPaths(); - return _.any(modifiedPaths, function (path) { + return modifiedPaths.some(function (path) { var mod = path.split('.'); - return _.any(meaningfulPaths, function (meaningfulPath) { + return meaningfulPaths.some(function (meaningfulPath) { var sel = meaningfulPath.split('.'); var i = 0, j = 0; @@ -64,14 +64,14 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { if (!this.affectedByModifier(modifier)) return false; - modifier = _.extend({$set:{}, $unset:{}}, modifier); - var modifierPaths = _.keys(modifier.$set).concat(_.keys(modifier.$unset)); + modifier = Object.assign({$set:{}, $unset:{}}, modifier); + var modifierPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); if (!self.isSimple()) return true; - if (_.any(self._getPaths(), pathHasNumericKeys) || - _.any(modifierPaths, pathHasNumericKeys)) + if (self._getPaths().some(pathHasNumericKeys) || + modifierPaths.some(pathHasNumericKeys)) return true; // check if there is a $set or $unset that indicates something is an @@ -79,10 +79,11 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // NOTE: it is correct since we allow only scalars in $-operators // Example: for selector {'a.b': {$gt: 5}} the modifier {'a.b.c':7} would // definitely set the result to false as 'a.b' appears to be an object. - var expectedScalarIsObject = _.any(self._selector, function (sel, path) { + var expectedScalarIsObject = Object.keys(self._selector).some(function (path) { + var sel = self._selector[path]; if (! isOperatorObject(sel)) return false; - return _.any(modifierPaths, function (modifierPath) { + return modifierPaths.some(function (modifierPath) { return startsWith(modifierPath, path + '.'); }); }); @@ -149,17 +150,17 @@ Minimongo.Matcher.prototype.matchingDocument = function () { // Return anything from $in that matches the whole selector for this // path. If nothing matches, returns `undefined` as nothing can make // this selector into `true`. - return _.find(valueSelector.$in, function (x) { + return valueSelector.$in.find(function (x) { return matcher.documentMatches({ placeholder: x }).result; }); } else if (onlyContainsKeys(valueSelector, ['$gt', '$gte', '$lt', '$lte'])) { var lowerBound = -Infinity, upperBound = Infinity; - _.each(['$lte', '$lt'], function (op) { - if (_.has(valueSelector, op) && valueSelector[op] < upperBound) + ['$lte', '$lt'].forEach(function (op) { + if (valueSelector.hasOwnProperty(op) && valueSelector[op] < upperBound) upperBound = valueSelector[op]; }); - _.each(['$gte', '$gt'], function (op) { - if (_.has(valueSelector, op) && valueSelector[op] > lowerBound) + ['$gte', '$gt'].forEach(function (op) { + if (valueSelector.hasOwnProperty(op) && valueSelector[op] > lowerBound) lowerBound = valueSelector[op]; }); @@ -181,7 +182,7 @@ Minimongo.Matcher.prototype.matchingDocument = function () { } return self._selector[path]; }, - _.identity /*conflict resolution is no resolution*/); + function (x) { return x; } /*conflict resolution is no resolution*/); if (fallback) self._matchingDocument = null; @@ -190,28 +191,31 @@ Minimongo.Matcher.prototype.matchingDocument = function () { }; var getPaths = function (sel) { - return _.keys(new Minimongo.Matcher(sel)._paths); - return _.chain(sel).map(function (v, k) { + return Object.keys(new Minimongo.Matcher(sel)._paths); + return Object.keys(sel).map(function (k) { + var v = sel[k]; // we don't know how to handle $where because it can be anything if (k === "$where") return ''; // matches everything // we branch from $or/$and/$nor operator - if (_.contains(['$or', '$and', '$nor'], k)) - return _.map(v, getPaths); + if (['$or', '$and', '$nor'].includes(k)) + return v.map(getPaths); // the value is a literal or some comparison operator return k; - }).flatten().uniq().value(); + }) + .reduce(function (a, b) { return a.concat(b); }, []) + .filter(function (a, b, c) { return c.indexOf(a) === b; }); }; // A helper to ensure object has only certain keys var onlyContainsKeys = function (obj, keys) { - return _.all(obj, function (v, k) { - return _.contains(keys, k); + return Object.keys(obj).every(function (k) { + return keys.includes(k); }); }; var pathHasNumericKeys = function (path) { - return _.any(path.split('.'), isNumericKey); + return path.split('.').some(isNumericKey); } // XXX from Underscore.String (http://epeli.github.com/underscore.string/) diff --git a/packages/minimongo/selector_projection.js b/packages/minimongo/selector_projection.js index 5f6e101b5b..670c529384 100644 --- a/packages/minimongo/selector_projection.js +++ b/packages/minimongo/selector_projection.js @@ -9,7 +9,7 @@ Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { // on all fields of the document. getSelectorPaths returns a list of paths // selector depends on. If one of the paths is '' (empty string) representing // the root or the whole document, complete projection should be returned. - if (_.contains(selectorPaths, '')) + if (selectorPaths.includes('')) return {}; return combineImportantPathsIntoProjection(selectorPaths, projection); @@ -17,8 +17,8 @@ Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { Minimongo._pathsElidingNumericKeys = function (paths) { var self = this; - return _.map(paths, function (path) { - return _.reject(path.split('.'), isNumericKey).join('.'); + return paths.map(function (path) { + return path.split('.').filter(function (part) { return !isNumericKey(part); }).join('.'); }); }; @@ -42,7 +42,8 @@ combineImportantPathsIntoProjection = function (paths, projection) { // projection is pointing at fields to exclude // make sure we don't exclude important paths var mergedExclProjection = {}; - _.each(mergedProjection, function (incl, path) { + Object.keys(mergedProjection).forEach(function (path) { + var incl = mergedProjection[path]; if (!incl) mergedExclProjection[path] = false; }); @@ -57,9 +58,10 @@ var treeToPaths = function (tree, prefix) { prefix = prefix || ''; var result = {}; - _.each(tree, function (val, key) { - if (_.isObject(val)) - _.extend(result, treeToPaths(val, prefix + key + '.')); + Object.keys(tree).forEach(function (key) { + var val = tree[key]; + if (val === Object(val)) + Object.assign(result, treeToPaths(val, prefix + key + '.')); else result[prefix + key] = val; }); diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js index aebbcc2d4f..95ac434379 100644 --- a/packages/minimongo/sort.js +++ b/packages/minimongo/sort.js @@ -39,7 +39,8 @@ Minimongo.Sorter = function (spec, options) { } } } else if (typeof spec === "object") { - _.each(spec, function (value, key) { + Object.keys(spec).forEach(function (key) { + var value = spec[key]; addSpecPart(key, value >= 0); }); } else if (typeof spec === "function") { @@ -57,14 +58,14 @@ Minimongo.Sorter = function (spec, options) { // modifiers as this sort order. This is only implemented on the server. if (self.affectedByModifier) { var selector = {}; - _.each(self._sortSpecParts, function (spec) { + self._sortSpecParts.forEach(function (spec) { selector[spec.path] = 1; }); self._selectorForAffectedByModifier = new Minimongo.Matcher(selector); } self._keyComparator = composeComparators( - _.map(self._sortSpecParts, function (spec, i) { + self._sortSpecParts.map(function (spec, i) { return self._keyFieldComparator(i); })); @@ -77,11 +78,11 @@ Minimongo.Sorter = function (spec, options) { // In addition to these methods, sorter_project.js defines combineIntoProjection // on the server only. -_.extend(Minimongo.Sorter.prototype, { +Object.assign(Minimongo.Sorter.prototype, { getComparator: function (options) { var self = this; - // If sort is specified or have no distances, just use the comparator from + // If sort is specified or have no distances, just use the comparator from // the source specification (which defaults to "everything is equal". // issue #3599 // https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation @@ -104,7 +105,7 @@ _.extend(Minimongo.Sorter.prototype, { _getPaths: function () { var self = this; - return _.pluck(self._sortSpecParts, 'path'); + return self._sortSpecParts.map(function (part) { return part.path; }); }, // Finds the minimum key from the doc, according to the sort specs. (We say @@ -163,7 +164,7 @@ _.extend(Minimongo.Sorter.prototype, { var knownPaths = null; - _.each(self._sortSpecParts, function (spec, whichField) { + self._sortSpecParts.forEach(function (spec, whichField) { // Expand any leaf arrays that we find, and ignore those arrays // themselves. (We never sort based on an array itself.) var branches = expandArraysInBranches(spec.lookup(doc), true); @@ -175,7 +176,7 @@ _.extend(Minimongo.Sorter.prototype, { var usedPaths = false; valuesByIndexAndPath[whichField] = {}; - _.each(branches, function (branch) { + branches.forEach(function (branch) { if (!branch.arrayIndices) { // If there are no array indices for a branch, then it must be the // only branch, because the only thing that produces multiple branches @@ -188,7 +189,7 @@ _.extend(Minimongo.Sorter.prototype, { usedPaths = true; var path = pathFromIndices(branch.arrayIndices); - if (_.has(valuesByIndexAndPath[whichField], path)) + if (valuesByIndexAndPath[whichField].hasOwnProperty(path)) throw Error("duplicate path: " + path); valuesByIndexAndPath[whichField][path] = branch.value; @@ -202,7 +203,7 @@ _.extend(Minimongo.Sorter.prototype, { // and 'a.x.y' are both arrays, but we don't allow this for now. // #NestedArraySort // XXX achieve full compatibility here - if (knownPaths && !_.has(knownPaths, path)) { + if (knownPaths && !knownPaths.hasOwnProperty(path)) { throw Error("cannot index parallel arrays"); } }); @@ -210,13 +211,13 @@ _.extend(Minimongo.Sorter.prototype, { if (knownPaths) { // Similarly to above, paths must match everywhere, unless this is a // non-array field. - if (!_.has(valuesByIndexAndPath[whichField], '') && - _.size(knownPaths) !== _.size(valuesByIndexAndPath[whichField])) { + if (!valuesByIndexAndPath[whichField].hasOwnProperty('') && + Object.keys(knownPaths).length !== Object.keys(valuesByIndexAndPath[whichField]).length) { throw Error("cannot index parallel arrays!"); } } else if (usedPaths) { knownPaths = {}; - _.each(valuesByIndexAndPath[whichField], function (x, path) { + Object.keys(valuesByIndexAndPath[whichField]).forEach(function (path) { knownPaths[path] = true; }); } @@ -224,8 +225,8 @@ _.extend(Minimongo.Sorter.prototype, { if (!knownPaths) { // Easy case: no use of arrays. - var soleKey = _.map(valuesByIndexAndPath, function (values) { - if (!_.has(values, '')) + var soleKey = valuesByIndexAndPath.map(function (values) { + if (!values.hasOwnProperty('')) throw Error("no value in sole key case?"); return values['']; }); @@ -233,11 +234,11 @@ _.extend(Minimongo.Sorter.prototype, { return; } - _.each(knownPaths, function (x, path) { - var key = _.map(valuesByIndexAndPath, function (values) { - if (_.has(values, '')) + Object.keys(knownPaths).forEach(function (path) { + var key = valuesByIndexAndPath.map(function (values) { + if (values.hasOwnProperty('')) return values['']; - if (!_.has(values, path)) + if (!values.hasOwnProperty(path)) throw Error("missing path?"); return values[path]; }); @@ -322,7 +323,7 @@ _.extend(Minimongo.Sorter.prototype, { // If we are only sorting by distance, then we're not going to bother to // build a key filter. // XXX figure out how geoqueries interact with this stuff - if (_.isEmpty(self._sortSpecParts)) + if (!self._sortSpecParts.length) return; var selector = matcher._selector; @@ -333,11 +334,12 @@ _.extend(Minimongo.Sorter.prototype, { return; var constraintsByPath = {}; - _.each(self._sortSpecParts, function (spec, i) { + self._sortSpecParts.forEach(function (spec, i) { constraintsByPath[spec.path] = []; }); - _.each(selector, function (subSelector, key) { + Object.keys(selector).forEach(function (key) { + var subSelector = selector[key]; // XXX support $and and $or var constraints = constraintsByPath[key]; @@ -362,8 +364,9 @@ _.extend(Minimongo.Sorter.prototype, { } if (isOperatorObject(subSelector)) { - _.each(subSelector, function (operand, operator) { - if (_.contains(['$lt', '$lte', '$gt', '$gte'], operator)) { + Object.keys(subSelector).forEach(function (operator) { + var operand = subSelector[operator]; + if (['$lt', '$lte', '$gt', '$gte'].includes(operator)) { // XXX this depends on us knowing that these operators don't use any // of the arguments to compileElementSelector other than operand. constraints.push( @@ -390,12 +393,12 @@ _.extend(Minimongo.Sorter.prototype, { // others; we shouldn't create a key filter unless the first sort field is // restricted, though after that point we can restrict the other sort fields // or not as we wish. - if (_.isEmpty(constraintsByPath[self._sortSpecParts[0].path])) + if (!constraintsByPath[self._sortSpecParts[0].path].length) return; self._keyFilter = function (key) { - return _.all(self._sortSpecParts, function (specPart, index) { - return _.all(constraintsByPath[specPart.path], function (f) { + return self._sortSpecParts.every(function (specPart, index) { + return constraintsByPath[specPart.path].every(function (f) { return f(key[index]); }); }); diff --git a/packages/minimongo/validation.js b/packages/minimongo/validation.js index ef73560a72..7728361f76 100644 --- a/packages/minimongo/validation.js +++ b/packages/minimongo/validation.js @@ -8,7 +8,7 @@ const invalidCharMsg = { }; export function assertIsValidFieldName(key) { let match; - if (_.isString(key) && (match = key.match(/^\$|\.|\0/))) { + if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); } }; @@ -21,4 +21,4 @@ export function assertHasValidFieldNames(doc){ return value; }); } -}; \ No newline at end of file +}; diff --git a/packages/minimongo/wrap_transform.js b/packages/minimongo/wrap_transform.js index 561931e378..797fd3241c 100644 --- a/packages/minimongo/wrap_transform.js +++ b/packages/minimongo/wrap_transform.js @@ -16,7 +16,7 @@ LocalCollection.wrapTransform = function (transform) { return transform; var wrapped = function (doc) { - if (!_.has(doc, '_id')) { + if (!doc.hasOwnProperty('_id')) { // XXX do we ever have a transform on the oplog's collection? because that // collection has no _id. throw new Error("can only transform documents with _id"); @@ -32,7 +32,7 @@ LocalCollection.wrapTransform = function (transform) { throw new Error("transform must return object"); } - if (_.has(transformed, '_id')) { + if (transformed.hasOwnProperty('_id')) { if (!EJSON.equals(transformed._id, id)) { throw new Error("transformed document can't have different _id"); } diff --git a/packages/minimongo/wrap_transform_tests.js b/packages/minimongo/wrap_transform_tests.js index 6385a29120..3c1f84196a 100644 --- a/packages/minimongo/wrap_transform_tests.js +++ b/packages/minimongo/wrap_transform_tests.js @@ -13,7 +13,7 @@ Tinytest.add("minimongo - wrapTransform", function (test) { return doc; }; var transformed = wrap(validTransform)({_id: "asdf", x: 54}); - test.equal(_.keys(transformed), ['_id', 'y', 'z']); + test.equal(Object.keys(transformed), ['_id', 'y', 'z']); test.equal(transformed.y, 42); test.equal(transformed.z(), 43); @@ -28,7 +28,7 @@ Tinytest.add("minimongo - wrapTransform", function (test) { "asdf", new MongoID.ObjectID(), false, null, true, 27, [123], /adsf/, new Date, function () {}, undefined ]; - _.each(invalidObjects, function (invalidObject) { + invalidObjects.forEach(function (invalidObject) { var wrapped = wrap(function () { return invalidObject; }); test.throws(function () { wrapped({_id: "asdf"}); From cdf25619b9886e74338905f6b1c9e714ed3e3e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Tue, 11 Jul 2017 18:31:17 +0200 Subject: [PATCH 02/28] Styling fixes. --- packages/minimongo/minimongo_tests.js | 4 ++-- packages/minimongo/selector.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index c3081b22a1..8bc09c95d0 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1574,7 +1574,7 @@ Tinytest.add("minimongo - projection_compiler", function (test) { Tinytest.add("minimongo - fetch with fields", function (test) { var c = new LocalCollection(); - Array.from({length: 30}, function (_, i) { + Array.from({length: 30}, function (x, i) { c.insert({ something: Random.id(), anything: { @@ -2107,7 +2107,7 @@ Tinytest.add("minimongo - array sort", function (test) { fieldValues.push(doc[field]); }); test.equal(cursor.fetch().map(function (doc) { return doc[field]; }), - Array.from({length: Math.max.apply(null, fieldValues) + 1}, function (_, i) { return i; })); + Array.from({length: Math.max.apply(null, fieldValues) + 1}, function (x, i) { return i; })); }; testCursorMatchesField(c.find({}, {sort: {'a.x': 1}}), 'up'); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index a56af7008a..89aceab829 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -801,7 +801,9 @@ ELEMENT_OPERATORS = { throw Error("$elemMatch need an object"); var subMatcher, isDocMatcher; - if (isOperatorObject(Object.keys(operand).filter(key => !Object.keys(LOGICAL_OPERATORS).includes(key)).reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { + if (isOperatorObject(Object.keys(operand) + .filter(function (key) { return !Object.keys(LOGICAL_OPERATORS).includes(key); }) + .reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { subMatcher = compileValueSelector(operand, matcher); isDocMatcher = false; } else { From 7275e1d0801fcf0e3565017b79269f7cec103a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Tue, 11 Jul 2017 20:42:15 +0200 Subject: [PATCH 03/28] Combined files. --- packages/minimongo/diff.js | 21 - packages/minimongo/helpers.js | 45 - packages/minimongo/id_map.js | 7 - packages/minimongo/minimongo.js | 2770 +++++++++++- ...lector_modifier.js => minimongo_server.js} | 76 + packages/minimongo/minimongo_tests.js | 3769 +---------------- packages/minimongo/minimongo_tests_client.js | 3735 ++++++++++++++++ ...ver_tests.js => minimongo_tests_server.js} | 0 packages/minimongo/modify.js | 502 --- packages/minimongo/objectid.js | 57 - packages/minimongo/observe.js | 181 - packages/minimongo/package.js | 62 +- packages/minimongo/projection.js | 176 - packages/minimongo/selector.js | 1284 ------ packages/minimongo/selector_projection.js | 71 - packages/minimongo/sort.js | 422 -- packages/minimongo/sorter_projection.js | 5 - packages/minimongo/validation.js | 24 - packages/minimongo/wrap_transform.js | 46 - packages/minimongo/wrap_transform_tests.js | 58 - 20 files changed, 6648 insertions(+), 6663 deletions(-) delete mode 100644 packages/minimongo/diff.js delete mode 100644 packages/minimongo/helpers.js delete mode 100644 packages/minimongo/id_map.js rename packages/minimongo/{selector_modifier.js => minimongo_server.js} (75%) create mode 100644 packages/minimongo/minimongo_tests_client.js rename packages/minimongo/{minimongo_server_tests.js => minimongo_tests_server.js} (100%) delete mode 100644 packages/minimongo/modify.js delete mode 100644 packages/minimongo/objectid.js delete mode 100644 packages/minimongo/observe.js delete mode 100644 packages/minimongo/projection.js delete mode 100644 packages/minimongo/selector.js delete mode 100644 packages/minimongo/selector_projection.js delete mode 100644 packages/minimongo/sort.js delete mode 100644 packages/minimongo/sorter_projection.js delete mode 100644 packages/minimongo/validation.js delete mode 100644 packages/minimongo/wrap_transform.js delete mode 100644 packages/minimongo/wrap_transform_tests.js diff --git a/packages/minimongo/diff.js b/packages/minimongo/diff.js deleted file mode 100644 index 68bfca9c0c..0000000000 --- a/packages/minimongo/diff.js +++ /dev/null @@ -1,21 +0,0 @@ -// ordered: bool. -// old_results and new_results: collections of documents. -// if ordered, they are arrays. -// if unordered, they are IdMaps -LocalCollection._diffQueryChanges = function (ordered, oldResults, newResults, observer, options) { - return DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options); -}; - -LocalCollection._diffQueryUnorderedChanges = function (oldResults, newResults, observer, options) { - return DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options); -}; - - -LocalCollection._diffQueryOrderedChanges = - function (oldResults, newResults, observer, options) { - return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options); -}; - -LocalCollection._diffObjects = function (left, right, callbacks) { - return DiffSequence.diffObjects(left, right, callbacks); -}; diff --git a/packages/minimongo/helpers.js b/packages/minimongo/helpers.js deleted file mode 100644 index aa76b1f57b..0000000000 --- a/packages/minimongo/helpers.js +++ /dev/null @@ -1,45 +0,0 @@ -// Like _.isArray, but doesn't regard polyfilled Uint8Arrays on old browsers as -// arrays. -// XXX maybe this should be EJSON.isArray -isArray = function (x) { - return Array.isArray(x) && !EJSON.isBinary(x); -}; - -// XXX maybe this should be EJSON.isObject, though EJSON doesn't know about -// RegExp -// XXX note that _type(undefined) === 3!!!! -isPlainObject = LocalCollection._isPlainObject = function (x) { - return x && LocalCollection._f._type(x) === 3; -}; - -isIndexable = function (x) { - return isArray(x) || isPlainObject(x); -}; - -// Returns true if this is an object with at least one key and all keys begin -// with $. Unless inconsistentOK is set, throws if some keys begin with $ and -// others don't. -isOperatorObject = function (valueSelector, inconsistentOK) { - if (!isPlainObject(valueSelector)) - return false; - - var theseAreOperators = undefined; - Object.keys(valueSelector).forEach(function (selKey) { - var thisIsOperator = selKey.substr(0, 1) === '$'; - if (theseAreOperators === undefined) { - theseAreOperators = thisIsOperator; - } else if (theseAreOperators !== thisIsOperator) { - if (!inconsistentOK) - throw new Error("Inconsistent operator: " + - JSON.stringify(valueSelector)); - theseAreOperators = false; - } - }); - return !!theseAreOperators; // {} has no operators -}; - - -// string can be converted to integer -isNumericKey = function (s) { - return /^[0-9]+$/.test(s); -}; diff --git a/packages/minimongo/id_map.js b/packages/minimongo/id_map.js deleted file mode 100644 index ad51ab1ba2..0000000000 --- a/packages/minimongo/id_map.js +++ /dev/null @@ -1,7 +0,0 @@ -LocalCollection._IdMap = function () { - var self = this; - IdMap.call(self, MongoID.idStringify, MongoID.idParse); -}; - -Meteor._inherits(LocalCollection._IdMap, IdMap); - diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index d02bcd40a3..027de20b11 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1,4 +1,28 @@ -import { assertHasValidFieldNames } from './validation.js'; +// Make sure field names do not contain Mongo restricted +// characters ('.', '$', '\0'). +// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names +const invalidCharMsg = { + '.': "contain '.'", + '$': "start with '$'", + '\0': "contain null bytes", +}; +function assertIsValidFieldName(key) { + let match; + if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { + throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); + } +}; + +// checks if all field names in an object are valid +function assertHasValidFieldNames(doc){ + if (doc && typeof doc === "object") { + JSON.stringify(doc, (key, value) => { + assertIsValidFieldName(key); + return value; + }); + } +}; + // XXX type checking on selectors (graceful error if malformed) @@ -1132,3 +1156,2747 @@ LocalCollection.prototype.resumeObservers = function () { } self._observeQueue.drain(); }; + +// Wrap a transform function to return objects that have the _id field +// of the untransformed document. This ensures that subsystems such as +// the observe-sequence package that call `observe` can keep track of +// the documents identities. +// +// - Require that it returns objects +// - If the return value has an _id field, verify that it matches the +// original _id field +// - If the return value doesn't have an _id field, add it back. +LocalCollection.wrapTransform = function (transform) { + if (! transform) + return null; + + // No need to doubly-wrap transforms. + if (transform.__wrappedTransform__) + return transform; + + var wrapped = function (doc) { + if (!doc.hasOwnProperty('_id')) { + // XXX do we ever have a transform on the oplog's collection? because that + // collection has no _id. + throw new Error("can only transform documents with _id"); + } + + var id = doc._id; + // XXX consider making tracker a weak dependency and checking Package.tracker here + var transformed = Tracker.nonreactive(function () { + return transform(doc); + }); + + if (!isPlainObject(transformed)) { + throw new Error("transform must return object"); + } + + if (transformed.hasOwnProperty('_id')) { + if (!EJSON.equals(transformed._id, id)) { + throw new Error("transformed document can't have different _id"); + } + } else { + transformed._id = id; + } + return transformed; + }; + wrapped.__wrappedTransform__ = true; + return wrapped; +}; + +// Like _.isArray, but doesn't regard polyfilled Uint8Arrays on old browsers as +// arrays. +// XXX maybe this should be EJSON.isArray +isArray = function (x) { + return Array.isArray(x) && !EJSON.isBinary(x); +}; + +// XXX maybe this should be EJSON.isObject, though EJSON doesn't know about +// RegExp +// XXX note that _type(undefined) === 3!!!! +isPlainObject = LocalCollection._isPlainObject = function (x) { + return x && LocalCollection._f._type(x) === 3; +}; + +isIndexable = function (x) { + return isArray(x) || isPlainObject(x); +}; + +// Returns true if this is an object with at least one key and all keys begin +// with $. Unless inconsistentOK is set, throws if some keys begin with $ and +// others don't. +isOperatorObject = function (valueSelector, inconsistentOK) { + if (!isPlainObject(valueSelector)) + return false; + + var theseAreOperators = undefined; + Object.keys(valueSelector).forEach(function (selKey) { + var thisIsOperator = selKey.substr(0, 1) === '$'; + if (theseAreOperators === undefined) { + theseAreOperators = thisIsOperator; + } else if (theseAreOperators !== thisIsOperator) { + if (!inconsistentOK) + throw new Error("Inconsistent operator: " + + JSON.stringify(valueSelector)); + theseAreOperators = false; + } + }); + return !!theseAreOperators; // {} has no operators +}; + + +// string can be converted to integer +isNumericKey = function (s) { + return /^[0-9]+$/.test(s); +}; + +// The minimongo selector compiler! + +// Terminology: +// - a "selector" is the EJSON object representing a selector +// - a "matcher" is its compiled form (whether a full Minimongo.Matcher +// object or one of the component lambdas that matches parts of it) +// - a "result object" is an object with a "result" field and maybe +// distance and arrayIndices. +// - a "branched value" is an object with a "value" field and maybe +// "dontIterate" and "arrayIndices". +// - a "document" is a top-level object that can be stored in a collection. +// - a "lookup function" is a function that takes in a document and returns +// an array of "branched values". +// - a "branched matcher" maps from an array of branched values to a result +// object. +// - an "element matcher" maps from a single value to a bool. + +// Main entry point. +// var matcher = new Minimongo.Matcher({a: {$gt: 5}}); +// if (matcher.documentMatches({a: 7})) ... +Minimongo.Matcher = function (selector, isUpdate = false) { + var self = this; + // A set (object mapping string -> *) of all of the document paths looked + // at by the selector. Also includes the empty string if it may look at any + // path (eg, $where). + self._paths = {}; + // Set to true if compilation finds a $near. + self._hasGeoQuery = false; + // Set to true if compilation finds a $where. + self._hasWhere = false; + // Set to false if compilation finds anything other than a simple equality or + // one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used with + // scalars as operands. + self._isSimple = true; + // Set to a dummy document which always matches this Matcher. Or set to null + // if such document is too hard to find. + self._matchingDocument = undefined; + // A clone of the original selector. It may just be a function if the user + // passed in a function; otherwise is definitely an object (eg, IDs are + // translated into {_id: ID} first. Used by canBecomeTrueByModifier and + // Sorter._useWithMatcher. + self._selector = null; + self._docMatcher = self._compileSelector(selector); + // Set to true if selection is done for an update operation + // Default is false + // Used for $near array update (issue #3599) + self._isUpdate = isUpdate; +}; + +Object.assign(Minimongo.Matcher.prototype, { + documentMatches: function (doc) { + if (!doc || typeof doc !== "object") { + throw Error("documentMatches needs a document"); + } + return this._docMatcher(doc); + }, + hasGeoQuery: function () { + return this._hasGeoQuery; + }, + hasWhere: function () { + return this._hasWhere; + }, + isSimple: function () { + return this._isSimple; + }, + + // Given a selector, return a function that takes one argument, a + // document. It returns a result object. + _compileSelector: function (selector) { + var self = this; + // you can pass a literal function instead of a selector + if (selector instanceof Function) { + self._isSimple = false; + self._selector = selector; + self._recordPathUsed(''); + return function (doc) { + return {result: !!selector.call(doc)}; + }; + } + + // shorthand -- scalars match _id + if (LocalCollection._selectorIsId(selector)) { + self._selector = {_id: selector}; + self._recordPathUsed('_id'); + return function (doc) { + return {result: EJSON.equals(doc._id, selector)}; + }; + } + + // protect against dangerous selectors. falsey and {_id: falsey} are both + // likely programmer error, and not what you want, particularly for + // destructive operations. + if (!selector || (('_id' in selector) && !selector._id)) { + self._isSimple = false; + return nothingMatcher; + } + + // Top level can't be an array or true or binary. + if (typeof(selector) === 'boolean' || isArray(selector) || + EJSON.isBinary(selector)) + throw new Error("Invalid selector: " + selector); + + self._selector = EJSON.clone(selector); + return compileDocumentSelector(selector, self, {isRoot: true}); + }, + _recordPathUsed: function (path) { + this._paths[path] = true; + }, + // Returns a list of key paths the given selector is looking for. It includes + // the empty string if there is a $where. + _getPaths: function () { + return Object.keys(this._paths); + } +}); + + +// Takes in a selector that could match a full document (eg, the original +// selector). Returns a function mapping document->result object. +// +// matcher is the Matcher object we are compiling. +// +// If this is the root document selector (ie, not wrapped in $and or the like), +// then isRoot is true. (This is used by $near.) +var compileDocumentSelector = function (docSelector, matcher, options) { + options = options || {}; + var docMatchers = []; + Object.keys(docSelector).forEach(function (key) { + var subSelector = docSelector[key]; + if (key.substr(0, 1) === '$') { + // Outer operators are either logical operators (they recurse back into + // this function), or $where. + if (!LOGICAL_OPERATORS.hasOwnProperty(key)) + throw new Error("Unrecognized logical operator: " + key); + matcher._isSimple = false; + docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, + options.inElemMatch)); + } else { + // Record this path, but only if we aren't in an elemMatcher, since in an + // elemMatch this is a path inside an object in an array, not in the doc + // root. + if (!options.inElemMatch) + matcher._recordPathUsed(key); + var lookUpByIndex = makeLookupFunction(key); + var valueMatcher = + compileValueSelector(subSelector, matcher, options.isRoot); + docMatchers.push(function (doc) { + var branchValues = lookUpByIndex(doc); + return valueMatcher(branchValues); + }); + } + }); + + return andDocumentMatchers(docMatchers); +}; + +// Takes in a selector that could match a key-indexed value in a document; eg, +// {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to +// indicate equality). Returns a branched matcher: a function mapping +// [branched value]->result object. +var compileValueSelector = function (valueSelector, matcher, isRoot) { + if (valueSelector instanceof RegExp) { + matcher._isSimple = false; + return convertElementMatcherToBranchedMatcher( + regexpElementMatcher(valueSelector)); + } else if (isOperatorObject(valueSelector)) { + return operatorBranchedMatcher(valueSelector, matcher, isRoot); + } else { + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(valueSelector)); + } +}; + +// Given an element matcher (which evaluates a single value), returns a branched +// value (which evaluates the element matcher on all the branches and returns a +// more structured return value possibly including arrayIndices). +var convertElementMatcherToBranchedMatcher = function ( + elementMatcher, options) { + options = options || {}; + return function (branches) { + var expanded = branches; + if (!options.dontExpandLeafArrays) { + expanded = expandArraysInBranches( + branches, options.dontIncludeLeafArrays); + } + var ret = {}; + ret.result = expanded.some(function (element) { + var matched = elementMatcher(element.value); + + // Special case for $elemMatch: it means "true, and use this as an array + // index if I didn't already have one". + if (typeof matched === 'number') { + // XXX This code dates from when we only stored a single array index + // (for the outermost array). Should we be also including deeper array + // indices from the $elemMatch match? + if (!element.arrayIndices) + element.arrayIndices = [matched]; + matched = true; + } + + // If some element matched, and it's tagged with array indices, include + // those indices in our result object. + if (matched && element.arrayIndices) + ret.arrayIndices = element.arrayIndices; + + return matched; + }); + return ret; + }; +}; + +// Takes a RegExp object and returns an element matcher. +regexpElementMatcher = function (regexp) { + return function (value) { + if (value instanceof RegExp) { + return value.toString() === regexp.toString(); + } + // Regexps only work against strings. + if (typeof value !== 'string') + return false; + + // Reset regexp's state to avoid inconsistent matching for objects with the + // same value on consecutive calls of regexp.test. This happens only if the + // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for + // which we should *not* change the lastIndex but MongoDB doesn't support + // either of these flags. + regexp.lastIndex = 0; + + return regexp.test(value); + }; +}; + +// Takes something that is not an operator object and returns an element matcher +// for equality with that thing. +equalityElementMatcher = function (elementSelector) { + if (isOperatorObject(elementSelector)) + throw Error("Can't create equalityValueSelector for operator object"); + + // Special-case: null and undefined are equal (if you got undefined in there + // somewhere, or if you got it due to some branch being non-existent in the + // weird special case), even though they aren't with EJSON.equals. + if (elementSelector == null) { // undefined or null + return function (value) { + return value == null; // undefined or null + }; + } + + return function (value) { + return LocalCollection._f._equal(elementSelector, value); + }; +}; + +// Takes an operator object (an object with $ keys) and returns a branched +// matcher for it. +var operatorBranchedMatcher = function (valueSelector, matcher, isRoot) { + // Each valueSelector works separately on the various branches. So one + // operator can match one branch and another can match another branch. This + // is OK. + + var operatorMatchers = []; + Object.keys(valueSelector).forEach(function (operator) { + var operand = valueSelector[operator]; + var simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && + typeof operand === 'number'; + var simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); + var simpleInclusion = ['$in', '$nin'].includes(operator) && + Array.isArray(operand) && !operand.some(function (x) { return x === Object(x); }); + + if (! (simpleRange || simpleInclusion || simpleEquality)) { + matcher._isSimple = false; + } + + if (VALUE_OPERATORS.hasOwnProperty(operator)) { + operatorMatchers.push( + VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); + } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { + var options = ELEMENT_OPERATORS[operator]; + operatorMatchers.push( + convertElementMatcherToBranchedMatcher( + options.compileElementSelector( + operand, valueSelector, matcher), + options)); + } else { + throw new Error("Unrecognized operator: " + operator); + } + }); + + return andBranchedMatchers(operatorMatchers); +}; + +var compileArrayOfDocumentSelectors = function ( + selectors, matcher, inElemMatch) { + if (!isArray(selectors) || selectors.length === 0) + throw Error("$and/$or/$nor must be nonempty array"); + return selectors.map(function (subSelector) { + if (!isPlainObject(subSelector)) + throw Error("$or/$and/$nor entries need to be full objects"); + return compileDocumentSelector( + subSelector, matcher, {inElemMatch: inElemMatch}); + }); +}; + +// Operators that appear at the top level of a document selector. +var LOGICAL_OPERATORS = { + $and: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + return andDocumentMatchers(matchers); + }, + + $or: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + + // Special case: if there is only one matcher, use it directly, *preserving* + // any arrayIndices it returns. + if (matchers.length === 1) + return matchers[0]; + + return function (doc) { + var result = matchers.some(function (f) { + return f(doc).result; + }); + // $or does NOT set arrayIndices when it has multiple + // sub-expressions. (Tested against MongoDB.) + return {result: result}; + }; + }, + + $nor: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + return function (doc) { + var result = matchers.every(function (f) { + return !f(doc).result; + }); + // Never set arrayIndices, because we only match if nothing in particular + // "matched" (and because this is consistent with MongoDB). + return {result: result}; + }; + }, + + $where: function (selectorValue, matcher) { + // Record that *any* path may be used. + matcher._recordPathUsed(''); + matcher._hasWhere = true; + if (!(selectorValue instanceof Function)) { + // XXX MongoDB seems to have more complex logic to decide where or or not + // to add "return"; not sure exactly what it is. + selectorValue = Function("obj", "return " + selectorValue); + } + return function (doc) { + // We make the document available as both `this` and `obj`. + // XXX not sure what we should do if this throws + return {result: selectorValue.call(doc, doc)}; + }; + }, + + // This is just used as a comment in the query (in MongoDB, it also ends up in + // query logs); it has no effect on the actual selection. + $comment: function () { + return function () { + return {result: true}; + }; + } +}; + +// Returns a branched matcher that matches iff the given matcher does not. +// Note that this implicitly "deMorganizes" the wrapped function. ie, it +// means that ALL branch values need to fail to match innerBranchedMatcher. +var invertBranchedMatcher = function (branchedMatcher) { + return function (branchValues) { + var invertMe = branchedMatcher(branchValues); + // We explicitly choose to strip arrayIndices here: it doesn't make sense to + // say "update the array element that does not match something", at least + // in mongo-land. + return {result: !invertMe.result}; + }; +}; + +// Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a +// document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as +// "match each branched value independently and combine with +// convertElementMatcherToBranchedMatcher". +var VALUE_OPERATORS = { + $eq: function (operand) { + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(operand)); + }, + $not: function (operand, valueSelector, matcher) { + return invertBranchedMatcher(compileValueSelector(operand, matcher)); + }, + $ne: function (operand) { + return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( + equalityElementMatcher(operand))); + }, + $nin: function (operand) { + return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( + ELEMENT_OPERATORS.$in.compileElementSelector(operand))); + }, + $exists: function (operand) { + var exists = convertElementMatcherToBranchedMatcher(function (value) { + return value !== undefined; + }); + return operand ? exists : invertBranchedMatcher(exists); + }, + // $options just provides options for $regex; its logic is inside $regex + $options: function (operand, valueSelector) { + if (!valueSelector.hasOwnProperty('$regex')) + throw Error("$options needs a $regex"); + return everythingMatcher; + }, + // $maxDistance is basically an argument to $near + $maxDistance: function (operand, valueSelector) { + if (!valueSelector.$near) + throw Error("$maxDistance needs a $near"); + return everythingMatcher; + }, + $all: function (operand, valueSelector, matcher) { + if (!isArray(operand)) + throw Error("$all requires array"); + // Not sure why, but this seems to be what MongoDB does. + if (operand.length === 0) + return nothingMatcher; + + var branchedMatchers = []; + operand.forEach(function (criterion) { + // XXX handle $all/$elemMatch combination + if (isOperatorObject(criterion)) + throw Error("no $ expressions in $all"); + // This is always a regexp or equality selector. + branchedMatchers.push(compileValueSelector(criterion, matcher)); + }); + // andBranchedMatchers does NOT require all selectors to return true on the + // SAME branch. + return andBranchedMatchers(branchedMatchers); + }, + $near: function (operand, valueSelector, matcher, isRoot) { + if (!isRoot) + throw Error("$near can't be inside another $ operator"); + matcher._hasGeoQuery = true; + + // There are two kinds of geodata in MongoDB: legacy coordinate pairs and + // GeoJSON. They use different distance metrics, too. GeoJSON queries are + // marked with a $geometry property, though legacy coordinates can be + // matched using $geometry. + + var maxDistance, point, distance; + if (isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { + // GeoJSON "2dsphere" mode. + maxDistance = operand.$maxDistance; + point = operand.$geometry; + distance = function (value) { + // XXX: for now, we don't calculate the actual distance between, say, + // polygon and circle. If people care about this use-case it will get + // a priority. + if (!value) + return null; + if(!value.type) + return GeoJSON.pointDistance(point, + { type: "Point", coordinates: pointToArray(value) }); + if (value.type === "Point") { + return GeoJSON.pointDistance(point, value); + } else { + return GeoJSON.geometryWithinRadius(value, point, maxDistance) + ? 0 : maxDistance + 1; + } + }; + } else { + maxDistance = valueSelector.$maxDistance; + if (!isArray(operand) && !isPlainObject(operand)) + throw Error("$near argument must be coordinate pair or GeoJSON"); + point = pointToArray(operand); + distance = function (value) { + if (!isArray(value) && !isPlainObject(value)) + return null; + return distanceCoordinatePairs(point, value); + }; + } + + return function (branchedValues) { + // There might be multiple points in the document that match the given + // field. Only one of them needs to be within $maxDistance, but we need to + // evaluate all of them and use the nearest one for the implicit sort + // specifier. (That's why we can't just use ELEMENT_OPERATORS here.) + // + // Note: This differs from MongoDB's implementation, where a document will + // actually show up *multiple times* in the result set, with one entry for + // each within-$maxDistance branching point. + branchedValues = expandArraysInBranches(branchedValues); + var result = {result: false}; + branchedValues.every(function (branch) { + // if operation is an update, don't skip branches, just return the first one (#3599) + if (!matcher._isUpdate){ + if (!(typeof branch.value === "object")){ + return true; + } + var curDistance = distance(branch.value); + // Skip branches that aren't real points or are too far away. + if (curDistance === null || curDistance > maxDistance) + return true; + // Skip anything that's a tie. + if (result.distance !== undefined && result.distance <= curDistance) + return true; + } + result.result = true; + result.distance = curDistance; + if (!branch.arrayIndices) + delete result.arrayIndices; + else + result.arrayIndices = branch.arrayIndices; + if (matcher._isUpdate) + return false; + return true; + }); + return result; + }; + } +}; + +// Helpers for $near. +var distanceCoordinatePairs = function (a, b) { + a = pointToArray(a); + b = pointToArray(b); + var x = a[0] - b[0]; + var y = a[1] - b[1]; + if (Number.isNaN(x) || Number.isNaN(y)) + return null; + return Math.sqrt(x * x + y * y); +}; +// Makes sure we get 2 elements array and assume the first one to be x and +// the second one to y no matter what user passes. +// In case user passes { lon: x, lat: y } returns [x, y] +var pointToArray = function (point) { + return Array.isArray(point) ? point.slice() : [point.x, point.y]; +}; + +// Helper for $lt/$gt/$lte/$gte. +var makeInequality = function (cmpValueComparator) { + return { + compileElementSelector: function (operand) { + // Arrays never compare false with non-arrays for any inequality. + // XXX This was behavior we observed in pre-release MongoDB 2.5, but + // it seems to have been reverted. + // See https://jira.mongodb.org/browse/SERVER-11444 + if (isArray(operand)) { + return function () { + return false; + }; + } + + // Special case: consider undefined and null the same (so true with + // $gte/$lte). + if (operand === undefined) + operand = null; + + var operandType = LocalCollection._f._type(operand); + + return function (value) { + if (value === undefined) + value = null; + // Comparisons are never true among things of different type (except + // null vs undefined). + if (LocalCollection._f._type(value) !== operandType) + return false; + return cmpValueComparator(LocalCollection._f._cmp(value, operand)); + }; + } + }; +}; + +// Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear. +var getOperandBitmask = function(operand, selector) { + // numeric bitmask + // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. + // Otherwise, $bitsAllSet will return an error. + if (Number.isInteger(operand) && operand >= 0) { + return new Uint8Array(new Int32Array([operand]).buffer) + } + // bindata bitmask + // You can also use an arbitrarily large BinData instance as a bitmask. + else if (EJSON.isBinary(operand)) { + return new Uint8Array(operand.buffer) + } + // position list + // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. + else if (isArray(operand) && operand.every(function (e) { + return Number.isInteger(e) && e >= 0 + })) { + var buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1) + var view = new Uint8Array(buffer) + operand.forEach(function (x) { + view[x >> 3] |= (1 << (x & 0x7)) + }) + return view + } + // bad operand + else { + throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`) + } +} +var getValueBitmask = function (value, length) { + // The field value must be either numerical or a BinData instance. Otherwise, $bits... will not match the current document. + // numerical + if (Number.isSafeInteger(value)) { + // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer + // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. + var buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); + var view = new Uint32Array(buffer, 0, 2) + view[0] = (value % ((1 << 16) * (1 << 16))) | 0 + view[1] = (value / ((1 << 16) * (1 << 16))) | 0 + // sign extension + if (value < 0) { + view = new Uint8Array(buffer, 2) + view.forEach(function (byte, idx) { + view[idx] = 0xff + }) + } + return new Uint8Array(buffer) + } + // bindata + else if (EJSON.isBinary(value)) { + return new Uint8Array(value.buffer) + } + // no match + return false +} + +// Each element selector contains: +// - compileElementSelector, a function with args: +// - operand - the "right hand side" of the operator +// - valueSelector - the "context" for the operator (so that $regex can find +// $options) +// - matcher - the Matcher this is going into (so that $elemMatch can compile +// more things) +// returning a function mapping a single value to bool. +// - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from +// being called +// - dontIncludeLeafArrays, a bool which causes an argument to be passed to +// expandArraysInBranches if it is called +ELEMENT_OPERATORS = { + $lt: makeInequality(function (cmpValue) { + return cmpValue < 0; + }), + $gt: makeInequality(function (cmpValue) { + return cmpValue > 0; + }), + $lte: makeInequality(function (cmpValue) { + return cmpValue <= 0; + }), + $gte: makeInequality(function (cmpValue) { + return cmpValue >= 0; + }), + $mod: { + compileElementSelector: function (operand) { + if (!(isArray(operand) && operand.length === 2 + && typeof(operand[0]) === 'number' + && typeof(operand[1]) === 'number')) { + throw Error("argument to $mod must be an array of two numbers"); + } + // XXX could require to be ints or round or something + var divisor = operand[0]; + var remainder = operand[1]; + return function (value) { + return typeof value === 'number' && value % divisor === remainder; + }; + } + }, + $in: { + compileElementSelector: function (operand) { + if (!isArray(operand)) + throw Error("$in needs an array"); + + var elementMatchers = []; + operand.forEach(function (option) { + if (option instanceof RegExp) + elementMatchers.push(regexpElementMatcher(option)); + else if (isOperatorObject(option)) + throw Error("cannot nest $ under $in"); + else + elementMatchers.push(equalityElementMatcher(option)); + }); + + return function (value) { + // Allow {a: {$in: [null]}} to match when 'a' does not exist. + if (value === undefined) + value = null; + return elementMatchers.some(function (e) { + return e(value); + }); + }; + } + }, + $size: { + // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we + // don't want to consider the element [5,5] in the leaf array [[5,5]] as a + // possible value. + dontExpandLeafArrays: true, + compileElementSelector: function (operand) { + if (typeof operand === 'string') { + // Don't ask me why, but by experimentation, this seems to be what Mongo + // does. + operand = 0; + } else if (typeof operand !== 'number') { + throw Error("$size needs a number"); + } + return function (value) { + return isArray(value) && value.length === operand; + }; + } + }, + $type: { + // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should + // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: + // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but + // should *not* include it itself. + dontIncludeLeafArrays: true, + compileElementSelector: function (operand) { + if (typeof operand !== 'number') + throw Error("$type needs a number"); + return function (value) { + return value !== undefined + && LocalCollection._f._type(value) === operand; + }; + } + }, + $bitsAllSet: { + compileElementSelector: function (operand) { + var op = getOperandBitmask(operand, '$bitsAllSet') + return function (value) { + var bitmask = getValueBitmask(value, op.length) + return bitmask && op.every(function (byte, idx) { + return ((bitmask[idx] & byte) == byte) + }) + } + } + }, + $bitsAnySet: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAnySet') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.some(function (byte, idx) { + return ((~bitmask[idx] & byte) !== byte) + }) + } + } + }, + $bitsAllClear: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAllClear') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.every(function (byte, idx) { + return !(bitmask[idx] & byte) + }) + } + } + }, + $bitsAnyClear: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAnyClear') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.some(function (byte, idx) { + return ((bitmask[idx] & byte) !== byte) + }) + } + } + }, + $regex: { + compileElementSelector: function (operand, valueSelector) { + if (!(typeof operand === 'string' || operand instanceof RegExp)) + throw Error("$regex has to be a string or RegExp"); + + var regexp; + if (valueSelector.$options !== undefined) { + // Options passed in $options (even the empty string) always overrides + // options in the RegExp object itself. (See also + // Mongo.Collection._rewriteSelector.) + + // Be clear that we only support the JS-supported options, not extended + // ones (eg, Mongo supports x and s). Ideally we would implement x and s + // by transforming the regexp, but not today... + if (/[^gim]/.test(valueSelector.$options)) + throw new Error("Only the i, m, and g regexp options are supported"); + + var regexSource = operand instanceof RegExp ? operand.source : operand; + regexp = new RegExp(regexSource, valueSelector.$options); + } else if (operand instanceof RegExp) { + regexp = operand; + } else { + regexp = new RegExp(operand); + } + return regexpElementMatcher(regexp); + } + }, + $elemMatch: { + dontExpandLeafArrays: true, + compileElementSelector: function (operand, valueSelector, matcher) { + if (!isPlainObject(operand)) + throw Error("$elemMatch need an object"); + + var subMatcher, isDocMatcher; + if (isOperatorObject(Object.keys(operand) + .filter(function (key) { return !Object.keys(LOGICAL_OPERATORS).includes(key); }) + .reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { + subMatcher = compileValueSelector(operand, matcher); + isDocMatcher = false; + } else { + // This is NOT the same as compileValueSelector(operand), and not just + // because of the slightly different calling convention. + // {$elemMatch: {x: 3}} means "an element has a field x:3", not + // "consists only of a field x:3". Also, regexps and sub-$ are allowed. + subMatcher = compileDocumentSelector(operand, matcher, + {inElemMatch: true}); + isDocMatcher = true; + } + + return function (value) { + if (!isArray(value)) + return false; + for (var i = 0; i < value.length; ++i) { + var arrayElement = value[i]; + var arg; + if (isDocMatcher) { + // We can only match {$elemMatch: {b: 3}} against objects. + // (We can also match against arrays, if there's numeric indices, + // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) + if (!isPlainObject(arrayElement) && !isArray(arrayElement)) + return false; + arg = arrayElement; + } else { + // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches + // {a: [8]} but not {a: [[8]]} + arg = [{value: arrayElement, dontIterate: true}]; + } + // XXX support $near in $elemMatch by propagating $distance? + if (subMatcher(arg).result) + return i; // specially understood to mean "use as arrayIndices" + } + return false; + }; + } + } +}; + +// makeLookupFunction(key) returns a lookup function. +// +// A lookup function takes in a document and returns an array of matching +// branches. If no arrays are found while looking up the key, this array will +// have exactly one branches (possibly 'undefined', if some segment of the key +// was not found). +// +// If arrays are found in the middle, this can have more than one element, since +// we "branch". When we "branch", if there are more key segments to look up, +// then we only pursue branches that are plain objects (not arrays or scalars). +// This means we can actually end up with no branches! +// +// We do *NOT* branch on arrays that are found at the end (ie, at the last +// dotted member of the key). We just return that array; if you want to +// effectively "branch" over the array's values, post-process the lookup +// function with expandArraysInBranches. +// +// Each branch is an object with keys: +// - value: the value at the branch +// - dontIterate: an optional bool; if true, it means that 'value' is an array +// that expandArraysInBranches should NOT expand. This specifically happens +// when there is a numeric index in the key, and ensures the +// perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT +// match {a: [[5]]}. +// - arrayIndices: if any array indexing was done during lookup (either due to +// explicit numeric indices or implicit branching), this will be an array of +// the array indices used, from outermost to innermost; it is falsey or +// absent if no array index is used. If an explicit numeric index is used, +// the index will be followed in arrayIndices by the string 'x'. +// +// Note: arrayIndices is used for two purposes. First, it is used to +// implement the '$' modifier feature, which only ever looks at its first +// element. +// +// Second, it is used for sort key generation, which needs to be able to tell +// the difference between different paths. Moreover, it needs to +// differentiate between explicit and implicit branching, which is why +// there's the somewhat hacky 'x' entry: this means that explicit and +// implicit array lookups will have different full arrayIndices paths. (That +// code only requires that different paths have different arrayIndices; it +// doesn't actually "parse" arrayIndices. As an alternative, arrayIndices +// could contain objects with flags like "implicit", but I think that only +// makes the code surrounding them more complex.) +// +// (By the way, this field ends up getting passed around a lot without +// cloning, so never mutate any arrayIndices field/var in this package!) +// +// +// At the top level, you may only pass in a plain object or array. +// +// See the test 'minimongo - lookup' for some examples of what lookup functions +// return. +makeLookupFunction = function (key, options) { + options = options || {}; + var parts = key.split('.'); + var firstPart = parts.length ? parts[0] : ''; + var firstPartIsNumeric = isNumericKey(firstPart); + var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); + var lookupRest; + if (parts.length > 1) { + lookupRest = makeLookupFunction(parts.slice(1).join('.')); + } + + var omitUnnecessaryFields = function (retVal) { + if (!retVal.dontIterate) + delete retVal.dontIterate; + if (retVal.arrayIndices && !retVal.arrayIndices.length) + delete retVal.arrayIndices; + return retVal; + }; + + // Doc will always be a plain object or an array. + // apply an explicit numeric index, an array. + return function (doc, arrayIndices) { + if (!arrayIndices) + arrayIndices = []; + + if (isArray(doc)) { + // If we're being asked to do an invalid lookup into an array (non-integer + // or out-of-bounds), return no results (which is different from returning + // a single undefined result, in that `null` equality checks won't match). + if (!(firstPartIsNumeric && firstPart < doc.length)) + return []; + + // Remember that we used this array index. Include an 'x' to indicate that + // the previous index came from being considered as an explicit array + // index (not branching). + arrayIndices = arrayIndices.concat(+firstPart, 'x'); + } + + // Do our first lookup. + var firstLevel = doc[firstPart]; + + // If there is no deeper to dig, return what we found. + // + // If what we found is an array, most value selectors will choose to treat + // the elements of the array as matchable values in their own right, but + // that's done outside of the lookup function. (Exceptions to this are $size + // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a: + // [[1, 2]]}.) + // + // That said, if we just did an *explicit* array lookup (on doc) to find + // firstLevel, and firstLevel is an array too, we do NOT want value + // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}. + // So in that case, we mark the return value as "don't iterate". + if (!lookupRest) { + return [omitUnnecessaryFields({ + value: firstLevel, + dontIterate: isArray(doc) && isArray(firstLevel), + arrayIndices: arrayIndices})]; + } + + // We need to dig deeper. But if we can't, because what we've found is not + // an array or plain object, we're done. If we just did a numeric index into + // an array, we return nothing here (this is a change in Mongo 2.5 from + // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise, + // return a single `undefined` (which can, for example, match via equality + // with `null`). + if (!isIndexable(firstLevel)) { + if (isArray(doc)) + return []; + return [omitUnnecessaryFields({value: undefined, + arrayIndices: arrayIndices})]; + } + + var result = []; + var appendToResult = function (more) { + Array.prototype.push.apply(result, more); + }; + + // Dig deeper: look up the rest of the parts on whatever we've found. + // (lookupRest is smart enough to not try to do invalid lookups into + // firstLevel if it's an array.) + appendToResult(lookupRest(firstLevel, arrayIndices)); + + // If we found an array, then in *addition* to potentially treating the next + // part as a literal integer lookup, we should also "branch": try to look up + // the rest of the parts on each array element in parallel. + // + // In this case, we *only* dig deeper into array elements that are plain + // objects. (Recall that we only got this far if we have further to dig.) + // This makes sense: we certainly don't dig deeper into non-indexable + // objects. And it would be weird to dig into an array: it's simpler to have + // a rule that explicit integer indexes only apply to an outer array, not to + // an array you find after a branching search. + // + // In the special case of a numeric part in a *sort selector* (not a query + // selector), we skip the branching: we ONLY allow the numeric part to mean + // "look up this index" in that case, not "also look up this index in all + // the elements of the array". + if (isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { + firstLevel.forEach(function (branch, arrayIndex) { + if (isPlainObject(branch)) { + appendToResult(lookupRest( + branch, + arrayIndices.concat(arrayIndex))); + } + }); + } + + return result; + }; +}; +MinimongoTest.makeLookupFunction = makeLookupFunction; + +expandArraysInBranches = function (branches, skipTheArrays) { + var branchesOut = []; + branches.forEach(function (branch) { + var thisIsArray = isArray(branch.value); + // We include the branch itself, *UNLESS* we it's an array that we're going + // to iterate and we're told to skip arrays. (That's right, we include some + // arrays even skipTheArrays is true: these are arrays that were found via + // explicit numerical indices.) + if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { + branchesOut.push({ + value: branch.value, + arrayIndices: branch.arrayIndices + }); + } + if (thisIsArray && !branch.dontIterate) { + branch.value.forEach(function (leaf, i) { + branchesOut.push({ + value: leaf, + arrayIndices: (branch.arrayIndices || []).concat(i) + }); + }); + } + }); + return branchesOut; +}; + +var nothingMatcher = function (docOrBranchedValues) { + return {result: false}; +}; + +var everythingMatcher = function (docOrBranchedValues) { + return {result: true}; +}; + + +// NB: We are cheating and using this function to implement "AND" for both +// "document matchers" and "branched matchers". They both return result objects +// but the argument is different: for the former it's a whole doc, whereas for +// the latter it's an array of "branched values". +var andSomeMatchers = function (subMatchers) { + if (subMatchers.length === 0) + return everythingMatcher; + if (subMatchers.length === 1) + return subMatchers[0]; + + return function (docOrBranches) { + var ret = {}; + ret.result = subMatchers.every(function (f) { + var subResult = f(docOrBranches); + // Copy a 'distance' number out of the first sub-matcher that has + // one. Yes, this means that if there are multiple $near fields in a + // query, something arbitrary happens; this appears to be consistent with + // Mongo. + if (subResult.result && subResult.distance !== undefined + && ret.distance === undefined) { + ret.distance = subResult.distance; + } + // Similarly, propagate arrayIndices from sub-matchers... but to match + // MongoDB behavior, this time the *last* sub-matcher with arrayIndices + // wins. + if (subResult.result && subResult.arrayIndices) { + ret.arrayIndices = subResult.arrayIndices; + } + return subResult.result; + }); + + // If we didn't actually match, forget any extra metadata we came up with. + if (!ret.result) { + delete ret.distance; + delete ret.arrayIndices; + } + return ret; + }; +}; + +var andDocumentMatchers = andSomeMatchers; +var andBranchedMatchers = andSomeMatchers; + + +// helpers used by compiled selector code +LocalCollection._f = { + // XXX for _all and _in, consider building 'inquery' at compile time.. + + _type: function (v) { + if (typeof v === "number") + return 1; + if (typeof v === "string") + return 2; + if (typeof v === "boolean") + return 8; + if (isArray(v)) + return 4; + if (v === null) + return 10; + if (v instanceof RegExp) + // note that typeof(/x/) === "object" + return 11; + if (typeof v === "function") + return 13; + if (v instanceof Date) + return 9; + if (EJSON.isBinary(v)) + return 5; + if (v instanceof MongoID.ObjectID) + return 7; + return 3; // object + + // XXX support some/all of these: + // 14, symbol + // 15, javascript code with scope + // 16, 18: 32-bit/64-bit integer + // 17, timestamp + // 255, minkey + // 127, maxkey + }, + + // deep equality test: use for literal document and array matches + _equal: function (a, b) { + return EJSON.equals(a, b, {keyOrderSensitive: true}); + }, + + // maps a type code to a value that can be used to sort values of + // different types + _typeorder: function (t) { + // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types + // XXX what is the correct sort position for Javascript code? + // ('100' in the matrix below) + // XXX minkey/maxkey + return [-1, // (not a type) + 1, // number + 2, // string + 3, // object + 4, // array + 5, // binary + -1, // deprecated + 6, // ObjectID + 7, // bool + 8, // Date + 0, // null + 9, // RegExp + -1, // deprecated + 100, // JS code + 2, // deprecated (symbol) + 100, // JS code + 1, // 32-bit int + 8, // Mongo timestamp + 1 // 64-bit int + ][t]; + }, + + // compare two values of unknown type according to BSON ordering + // semantics. (as an extension, consider 'undefined' to be less than + // any other value.) return negative if a is less, positive if b is + // less, or 0 if equal + _cmp: function (a, b) { + if (a === undefined) + return b === undefined ? 0 : -1; + if (b === undefined) + return 1; + var ta = LocalCollection._f._type(a); + var tb = LocalCollection._f._type(b); + var oa = LocalCollection._f._typeorder(ta); + var ob = LocalCollection._f._typeorder(tb); + if (oa !== ob) + return oa < ob ? -1 : 1; + if (ta !== tb) + // XXX need to implement this if we implement Symbol or integers, or + // Timestamp + throw Error("Missing type coercion logic in _cmp"); + if (ta === 7) { // ObjectID + // Convert to string. + ta = tb = 2; + a = a.toHexString(); + b = b.toHexString(); + } + if (ta === 9) { // Date + // Convert to millis. + ta = tb = 1; + a = a.getTime(); + b = b.getTime(); + } + + if (ta === 1) // double + return a - b; + if (tb === 2) // string + return a < b ? -1 : (a === b ? 0 : 1); + if (ta === 3) { // Object + // this could be much more efficient in the expected case ... + var to_array = function (obj) { + var ret = []; + for (var key in obj) { + ret.push(key); + ret.push(obj[key]); + } + return ret; + }; + return LocalCollection._f._cmp(to_array(a), to_array(b)); + } + if (ta === 4) { // Array + for (var i = 0; ; i++) { + if (i === a.length) + return (i === b.length) ? 0 : -1; + if (i === b.length) + return 1; + var s = LocalCollection._f._cmp(a[i], b[i]); + if (s !== 0) + return s; + } + } + if (ta === 5) { // binary + // Surprisingly, a small binary blob is always less than a large one in + // Mongo. + if (a.length !== b.length) + return a.length - b.length; + for (i = 0; i < a.length; i++) { + if (a[i] < b[i]) + return -1; + if (a[i] > b[i]) + return 1; + } + return 0; + } + if (ta === 8) { // boolean + if (a) return b ? 0 : 1; + return b ? -1 : 0; + } + if (ta === 10) // null + return 0; + if (ta === 11) // regexp + throw Error("Sorting not supported on regular expression"); // XXX + // 13: javascript code + // 14: symbol + // 15: javascript code with scope + // 16: 32-bit integer + // 17: timestamp + // 18: 64-bit integer + // 255: minkey + // 127: maxkey + if (ta === 13) // javascript code + throw Error("Sorting not supported on Javascript code"); // XXX + throw Error("Unknown type to sort"); + } +}; + +const objectOnlyHasDollarKeys = (object) => { + const keys = Object.keys(object); + return keys.length > 0 && keys.every(key => key.charAt(0) === '$'); +}; + +// When performing an upsert, the incoming selector object can be re-used as +// the upsert modifier object, as long as Mongo query and projection +// operators (prefixed with a $ character) are removed from the newly +// created modifier object. This function attempts to strip all $ based Mongo +// operators when creating the upsert modifier object. +// NOTE: There is a known issue here in that some Mongo $ based opeartors +// should not actually be stripped. +// See https://github.com/meteor/meteor/issues/8806. +LocalCollection._removeDollarOperators = (selector) => { + let cleansed = {}; + Object.keys(selector).forEach((key) => { + const value = selector[key]; + if (key.charAt(0) !== '$' && !objectOnlyHasDollarKeys(value)) { + if (value !== null + && value.constructor + && Object.getPrototypeOf(value) === Object.prototype) { + cleansed[key] = LocalCollection._removeDollarOperators(value); + } else { + cleansed[key] = value; + } + } + }); + return cleansed; +}; + +// Give a sort spec, which can be in any of these forms: +// {"key1": 1, "key2": -1} +// [["key1", "asc"], ["key2", "desc"]] +// ["key1", ["key2", "desc"]] +// +// (.. with the first form being dependent on the key enumeration +// behavior of your javascript VM, which usually does what you mean in +// this case if the key names don't look like integers ..) +// +// return a function that takes two objects, and returns -1 if the +// first object comes first in order, 1 if the second object comes +// first, or 0 if neither object comes before the other. + +Minimongo.Sorter = function (spec, options) { + var self = this; + options = options || {}; + + self._sortSpecParts = []; + self._sortFunction = null; + + var addSpecPart = function (path, ascending) { + if (!path) + throw Error("sort keys must be non-empty"); + if (path.charAt(0) === '$') + throw Error("unsupported sort key: " + path); + self._sortSpecParts.push({ + path: path, + lookup: makeLookupFunction(path, {forSort: true}), + ascending: ascending + }); + }; + + if (spec instanceof Array) { + for (var i = 0; i < spec.length; i++) { + if (typeof spec[i] === "string") { + addSpecPart(spec[i], true); + } else { + addSpecPart(spec[i][0], spec[i][1] !== "desc"); + } + } + } else if (typeof spec === "object") { + Object.keys(spec).forEach(function (key) { + var value = spec[key]; + addSpecPart(key, value >= 0); + }); + } else if (typeof spec === "function") { + self._sortFunction = spec; + } else { + throw Error("Bad sort specification: " + JSON.stringify(spec)); + } + + // If a function is specified for sorting, we skip the rest. + if (self._sortFunction) + return; + + // To implement affectedByModifier, we piggy-back on top of Matcher's + // affectedByModifier code; we create a selector that is affected by the same + // modifiers as this sort order. This is only implemented on the server. + if (self.affectedByModifier) { + var selector = {}; + self._sortSpecParts.forEach(function (spec) { + selector[spec.path] = 1; + }); + self._selectorForAffectedByModifier = new Minimongo.Matcher(selector); + } + + self._keyComparator = composeComparators( + self._sortSpecParts.map(function (spec, i) { + return self._keyFieldComparator(i); + })); + + // If you specify a matcher for this Sorter, _keyFilter may be set to a + // function which selects whether or not a given "sort key" (tuple of values + // for the different sort spec fields) is compatible with the selector. + self._keyFilter = null; + options.matcher && self._useWithMatcher(options.matcher); +}; + +// In addition to these methods, sorter_project.js defines combineIntoProjection +// on the server only. +Object.assign(Minimongo.Sorter.prototype, { + getComparator: function (options) { + var self = this; + + // If sort is specified or have no distances, just use the comparator from + // the source specification (which defaults to "everything is equal". + // issue #3599 + // https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation + // sort effectively overrides $near + if (self._sortSpecParts.length || !options || !options.distances) { + return self._getBaseComparator(); + } + + var distances = options.distances; + + // Return a comparator which compares using $near distances. + return function (a, b) { + if (!distances.has(a._id)) + throw Error("Missing distance for " + a._id); + if (!distances.has(b._id)) + throw Error("Missing distance for " + b._id); + return distances.get(a._id) - distances.get(b._id); + }; + }, + + _getPaths: function () { + var self = this; + return self._sortSpecParts.map(function (part) { return part.path; }); + }, + + // Finds the minimum key from the doc, according to the sort specs. (We say + // "minimum" here but this is with respect to the sort spec, so "descending" + // sort fields mean we're finding the max for that field.) + // + // Note that this is NOT "find the minimum value of the first field, the + // minimum value of the second field, etc"... it's "choose the + // lexicographically minimum value of the key vector, allowing only keys which + // you can find along the same paths". ie, for a doc {a: [{x: 0, y: 5}, {x: + // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and + // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key. + _getMinKeyFromDoc: function (doc) { + var self = this; + var minKey = null; + + self._generateKeysFromDoc(doc, function (key) { + if (!self._keyCompatibleWithSelector(key)) + return; + + if (minKey === null) { + minKey = key; + return; + } + if (self._compareKeys(key, minKey) < 0) { + minKey = key; + } + }); + + // This could happen if our key filter somehow filters out all the keys even + // though somehow the selector matches. + if (minKey === null) + throw Error("sort selector found no keys in doc?"); + return minKey; + }, + + _keyCompatibleWithSelector: function (key) { + var self = this; + return !self._keyFilter || self._keyFilter(key); + }, + + // Iterates over each possible "key" from doc (ie, over each branch), calling + // 'cb' with the key. + _generateKeysFromDoc: function (doc, cb) { + var self = this; + + if (self._sortSpecParts.length === 0) + throw new Error("can't generate keys without a spec"); + + // maps index -> ({'' -> value} or {path -> value}) + var valuesByIndexAndPath = []; + + var pathFromIndices = function (indices) { + return indices.join(',') + ','; + }; + + var knownPaths = null; + + self._sortSpecParts.forEach(function (spec, whichField) { + // Expand any leaf arrays that we find, and ignore those arrays + // themselves. (We never sort based on an array itself.) + var branches = expandArraysInBranches(spec.lookup(doc), true); + + // If there are no values for a key (eg, key goes to an empty array), + // pretend we found one null value. + if (!branches.length) + branches = [{value: null}]; + + var usedPaths = false; + valuesByIndexAndPath[whichField] = {}; + branches.forEach(function (branch) { + if (!branch.arrayIndices) { + // If there are no array indices for a branch, then it must be the + // only branch, because the only thing that produces multiple branches + // is the use of arrays. + if (branches.length > 1) + throw Error("multiple branches but no array used?"); + valuesByIndexAndPath[whichField][''] = branch.value; + return; + } + + usedPaths = true; + var path = pathFromIndices(branch.arrayIndices); + if (valuesByIndexAndPath[whichField].hasOwnProperty(path)) + throw Error("duplicate path: " + path); + valuesByIndexAndPath[whichField][path] = branch.value; + + // If two sort fields both go into arrays, they have to go into the + // exact same arrays and we have to find the same paths. This is + // roughly the same condition that makes MongoDB throw this strange + // error message. eg, the main thing is that if sort spec is {a: 1, + // b:1} then a and b cannot both be arrays. + // + // (In MongoDB it seems to be OK to have {a: 1, 'a.x.y': 1} where 'a' + // and 'a.x.y' are both arrays, but we don't allow this for now. + // #NestedArraySort + // XXX achieve full compatibility here + if (knownPaths && !knownPaths.hasOwnProperty(path)) { + throw Error("cannot index parallel arrays"); + } + }); + + if (knownPaths) { + // Similarly to above, paths must match everywhere, unless this is a + // non-array field. + if (!valuesByIndexAndPath[whichField].hasOwnProperty('') && + Object.keys(knownPaths).length !== Object.keys(valuesByIndexAndPath[whichField]).length) { + throw Error("cannot index parallel arrays!"); + } + } else if (usedPaths) { + knownPaths = {}; + Object.keys(valuesByIndexAndPath[whichField]).forEach(function (path) { + knownPaths[path] = true; + }); + } + }); + + if (!knownPaths) { + // Easy case: no use of arrays. + var soleKey = valuesByIndexAndPath.map(function (values) { + if (!values.hasOwnProperty('')) + throw Error("no value in sole key case?"); + return values['']; + }); + cb(soleKey); + return; + } + + Object.keys(knownPaths).forEach(function (path) { + var key = valuesByIndexAndPath.map(function (values) { + if (values.hasOwnProperty('')) + return values['']; + if (!values.hasOwnProperty(path)) + throw Error("missing path?"); + return values[path]; + }); + cb(key); + }); + }, + + // Takes in two keys: arrays whose lengths match the number of spec + // parts. Returns negative, 0, or positive based on using the sort spec to + // compare fields. + _compareKeys: function (key1, key2) { + var self = this; + if (key1.length !== self._sortSpecParts.length || + key2.length !== self._sortSpecParts.length) { + throw Error("Key has wrong length"); + } + + return self._keyComparator(key1, key2); + }, + + // Given an index 'i', returns a comparator that compares two key arrays based + // on field 'i'. + _keyFieldComparator: function (i) { + var self = this; + var invert = !self._sortSpecParts[i].ascending; + return function (key1, key2) { + var compare = LocalCollection._f._cmp(key1[i], key2[i]); + if (invert) + compare = -compare; + return compare; + }; + }, + + // Returns a comparator that represents the sort specification (but not + // including a possible geoquery distance tie-breaker). + _getBaseComparator: function () { + var self = this; + + if (self._sortFunction) + return self._sortFunction; + + // If we're only sorting on geoquery distance and no specs, just say + // everything is equal. + if (!self._sortSpecParts.length) { + return function (doc1, doc2) { + return 0; + }; + } + + return function (doc1, doc2) { + var key1 = self._getMinKeyFromDoc(doc1); + var key2 = self._getMinKeyFromDoc(doc2); + return self._compareKeys(key1, key2); + }; + }, + + // In MongoDB, if you have documents + // {_id: 'x', a: [1, 10]} and + // {_id: 'y', a: [5, 15]}, + // then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5). + // But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not + // match the selector, and 5 comes before 10). + // + // The way this works is pretty subtle! For example, if the documents + // are instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and + // {_id: 'y', a: [{x: 5}, {x: 15}]}), + // then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and + // C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}}) + // both follow this rule (y before x). (ie, you do have to apply this + // through $elemMatch.) + // + // So if you pass a matcher to this sorter's constructor, we will attempt to + // skip sort keys that don't match the selector. The logic here is pretty + // subtle and undocumented; we've gotten as close as we can figure out based + // on our understanding of Mongo's behavior. + _useWithMatcher: function (matcher) { + var self = this; + + if (self._keyFilter) + throw Error("called _useWithMatcher twice?"); + + // If we are only sorting by distance, then we're not going to bother to + // build a key filter. + // XXX figure out how geoqueries interact with this stuff + if (!self._sortSpecParts.length) + return; + + var selector = matcher._selector; + + // If the user just passed a literal function to find(), then we can't get a + // key filter from it. + if (selector instanceof Function) + return; + + var constraintsByPath = {}; + self._sortSpecParts.forEach(function (spec, i) { + constraintsByPath[spec.path] = []; + }); + + Object.keys(selector).forEach(function (key) { + var subSelector = selector[key]; + // XXX support $and and $or + + var constraints = constraintsByPath[key]; + if (!constraints) + return; + + // XXX it looks like the real MongoDB implementation isn't "does the + // regexp match" but "does the value fall into a range named by the + // literal prefix of the regexp", ie "foo" in /^foo(bar|baz)+/ But + // "does the regexp match" is a good approximation. + if (subSelector instanceof RegExp) { + // As far as we can tell, using either of the options that both we and + // MongoDB support ('i' and 'm') disables use of the key filter. This + // makes sense: MongoDB mostly appears to be calculating ranges of an + // index to use, which means it only cares about regexps that match + // one range (with a literal prefix), and both 'i' and 'm' prevent the + // literal prefix of the regexp from actually meaning one range. + if (subSelector.ignoreCase || subSelector.multiline) + return; + constraints.push(regexpElementMatcher(subSelector)); + return; + } + + if (isOperatorObject(subSelector)) { + Object.keys(subSelector).forEach(function (operator) { + var operand = subSelector[operator]; + if (['$lt', '$lte', '$gt', '$gte'].includes(operator)) { + // XXX this depends on us knowing that these operators don't use any + // of the arguments to compileElementSelector other than operand. + constraints.push( + ELEMENT_OPERATORS[operator].compileElementSelector(operand)); + } + + // See comments in the RegExp block above. + if (operator === '$regex' && !subSelector.$options) { + constraints.push( + ELEMENT_OPERATORS.$regex.compileElementSelector( + operand, subSelector)); + } + + // XXX support {$exists: true}, $mod, $type, $in, $elemMatch + }); + return; + } + + // OK, it's an equality thing. + constraints.push(equalityElementMatcher(subSelector)); + }); + + // It appears that the first sort field is treated differently from the + // others; we shouldn't create a key filter unless the first sort field is + // restricted, though after that point we can restrict the other sort fields + // or not as we wish. + if (!constraintsByPath[self._sortSpecParts[0].path].length) + return; + + self._keyFilter = function (key) { + return self._sortSpecParts.every(function (specPart, index) { + return constraintsByPath[specPart.path].every(function (f) { + return f(key[index]); + }); + }); + }; + } +}); + +// Given an array of comparators +// (functions (a,b)->(negative or positive or zero)), returns a single +// comparator which uses each comparator in order and returns the first +// non-zero value. +var composeComparators = function (comparatorArray) { + return function (a, b) { + for (var i = 0; i < comparatorArray.length; ++i) { + var compare = comparatorArray[i](a, b); + if (compare !== 0) + return compare; + } + return 0; + }; +}; +// Knows how to compile a fields projection to a predicate function. +// @returns - Function: a closure that filters out an object according to the +// fields projection rules: +// @param obj - Object: MongoDB-styled document +// @returns - Object: a document with the fields filtered out +// according to projection rules. Doesn't retain subfields +// of passed argument. +LocalCollection._compileProjection = function (fields) { + LocalCollection._checkSupportedProjection(fields); + + var _idProjection = fields._id === undefined ? true : fields._id; + var details = projectionDetails(fields); + + // returns transformed doc according to ruleTree + var transform = function (doc, ruleTree) { + // Special case for "sets" + if (Array.isArray(doc)) + return doc.map(function (subdoc) { return transform(subdoc, ruleTree); }); + + var res = details.including ? {} : EJSON.clone(doc); + Object.keys(ruleTree).forEach(function (key) { + var rule = ruleTree[key]; + if (!doc.hasOwnProperty(key)) + return; + if (rule === Object(rule)) { + // For sub-objects/subsets we branch + if (doc[key] === Object(doc[key])) + res[key] = transform(doc[key], rule); + // Otherwise we don't even touch this subfield + } else if (details.including) + res[key] = EJSON.clone(doc[key]); + else + delete res[key]; + }); + + return res; + }; + + return function (obj) { + var res = transform(obj, details.tree); + + if (_idProjection && obj.hasOwnProperty('_id')) + res._id = obj._id; + if (!_idProjection && res.hasOwnProperty('_id')) + delete res._id; + return res; + }; +}; + +// Traverses the keys of passed projection and constructs a tree where all +// leaves are either all True or all False +// @returns Object: +// - tree - Object - tree representation of keys involved in projection +// (exception for '_id' as it is a special case handled separately) +// - including - Boolean - "take only certain fields" type of projection +projectionDetails = function (fields) { + // Find the non-_id keys (_id is handled specially because it is included unless + // explicitly excluded). Sort the keys, so that our code to detect overlaps + // like 'foo' and 'foo.bar' can assume that 'foo' comes first. + var fieldsKeys = Object.keys(fields).sort(); + + // If _id is the only field in the projection, do not remove it, since it is + // required to determine if this is an exclusion or exclusion. Also keep an + // inclusive _id, since inclusive _id follows the normal rules about mixing + // inclusive and exclusive fields. If _id is not the only field in the + // projection and is exclusive, remove it so it can be handled later by a + // special case, since exclusive _id is always allowed. + if (fieldsKeys.length > 0 && + !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && + !(fieldsKeys.includes('_id') && fields['_id'])) + fieldsKeys = fieldsKeys.filter(function (key) { return key !== '_id'; }); + + var including = null; // Unknown + + fieldsKeys.forEach(function (keyPath) { + var rule = !!fields[keyPath]; + if (including === null) + including = rule; + if (including !== rule) + // This error message is copied from MongoDB shell + throw MinimongoError("You cannot currently mix including and excluding fields."); + }); + + + var projectionRulesTree = pathsToTree( + fieldsKeys, + function (path) { return including; }, + function (node, path, fullPath) { + // Check passed projection fields' keys: If you have two rules such as + // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If + // that happens, there is a probability you are doing something wrong, + // framework should notify you about such mistake earlier on cursor + // compilation step than later during runtime. Note, that real mongo + // doesn't do anything about it and the later rule appears in projection + // project, more priority it takes. + // + // Example, assume following in mongo shell: + // > db.coll.insert({ a: { b: 23, c: 44 } }) + // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) + // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } } + // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) + // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } + // + // Note, how second time the return set of keys is different. + + var currentPath = fullPath; + var anotherPath = path; + throw MinimongoError("both " + currentPath + " and " + anotherPath + + " found in fields option, using both of them may trigger " + + "unexpected behavior. Did you mean to use only one of them?"); + }); + + return { + tree: projectionRulesTree, + including: including + }; +}; + +// paths - Array: list of mongo style paths +// newLeafFn - Function: of form function(path) should return a scalar value to +// put into list created for that path +// conflictFn - Function: of form function(node, path, fullPath) is called +// when building a tree path for 'fullPath' node on +// 'path' was already a leaf with a value. Must return a +// conflict resolution. +// initial tree - Optional Object: starting tree. +// @returns - Object: tree represented as a set of nested objects +pathsToTree = function (paths, newLeafFn, conflictFn, tree) { + tree = tree || {}; + paths.forEach(function (keyPath) { + var treePos = tree; + var pathArr = keyPath.split('.'); + + // use .every just for iteration with break + var success = pathArr.slice(0, -1).every(function (key, idx) { + if (!treePos.hasOwnProperty(key)) + treePos[key] = {}; + else if (treePos[key] !== Object(treePos[key])) { + treePos[key] = conflictFn(treePos[key], + pathArr.slice(0, idx + 1).join('.'), + keyPath); + // break out of loop if we are failing for this path + if (treePos[key] !== Object(treePos[key])) + return false; + } + + treePos = treePos[key]; + return true; + }); + + if (success) { + var lastKey = pathArr[pathArr.length - 1]; + if (!treePos.hasOwnProperty(lastKey)) + treePos[lastKey] = newLeafFn(keyPath); + else + treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); + } + }); + + return tree; +}; + +LocalCollection._checkSupportedProjection = function (fields) { + if (fields !== Object(fields) || Array.isArray(fields)) + throw MinimongoError("fields option must be an object"); + + Object.keys(fields).forEach(function (keyPath) { + var val = fields[keyPath]; + if (keyPath.split('.').includes('$')) + throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); + if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) + throw MinimongoError("Minimongo doesn't support operators in projections yet."); + if (![1, 0, true, false].includes(val)) + throw MinimongoError("Projection values should be one of 1, 0, true, or false"); + }); +}; + +// XXX need a strategy for passing the binding of $ into this +// function, from the compiled selector +// +// maybe just {key.up.to.just.before.dollarsign: array_index} +// +// XXX atomicity: if one modification fails, do we roll back the whole +// change? +// +// options: +// - isInsert is set when _modify is being called to compute the document to +// insert as part of an upsert operation. We use this primarily to figure +// out when to set the fields in $setOnInsert, if present. +LocalCollection._modify = function (doc, mod, options) { + options = options || {}; + if (!isPlainObject(mod)) + throw MinimongoError("Modifier must be an object"); + + // Make sure the caller can't mutate our data structures. + mod = EJSON.clone(mod); + + var isModifier = isOperatorObject(mod); + + var newDoc; + + if (!isModifier) { + if (mod._id && !EJSON.equals(doc._id, mod._id)) + throw MinimongoError("Cannot change the _id of a document"); + + // replace the whole document + assertHasValidFieldNames(mod); + newDoc = mod; + } else { + // apply modifiers to the doc. + newDoc = EJSON.clone(doc); + + Object.keys(mod).forEach(function (op) { + var operand = mod[op]; + var modFunc = MODIFIERS[op]; + // Treat $setOnInsert as $set if this is an insert. + if (options.isInsert && op === '$setOnInsert') + modFunc = MODIFIERS['$set']; + if (!modFunc) + throw MinimongoError("Invalid modifier specified " + op); + Object.keys(operand).forEach(function (keypath) { + var arg = operand[keypath]; + if (keypath === '') { + throw MinimongoError("An empty update path is not valid."); + } + + if (keypath === '_id' && op !== '$setOnInsert') { + throw MinimongoError("Mod on _id not allowed"); + } + + var keyparts = keypath.split('.'); + + if (!keyparts.every(Boolean)) { + throw MinimongoError( + "The update path '" + keypath + + "' contains an empty field name, which is not allowed."); + } + + var noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); + var forbidArray = (op === "$rename"); + var target = findModTarget(newDoc, keyparts, { + noCreate: NO_CREATE_MODIFIERS[op], + forbidArray: (op === "$rename"), + arrayIndices: options.arrayIndices + }); + var field = keyparts.pop(); + modFunc(target, field, arg, keypath, newDoc); + }); + }); + } + + // move new document into place. + Object.keys(doc).forEach(function (k) { + // Note: this used to be for (var k in doc) however, this does not + // work right in Opera. Deleting from a doc while iterating over it + // would sometimes cause opera to skip some keys. + if (k !== '_id') + delete doc[k]; + }); + Object.keys(newDoc).forEach(function (k) { + doc[k] = newDoc[k]; + }); +}; + +// for a.b.c.2.d.e, keyparts should be ['a', 'b', 'c', '2', 'd', 'e'], +// and then you would operate on the 'e' property of the returned +// object. +// +// if options.noCreate is falsey, creates intermediate levels of +// structure as necessary, like mkdir -p (and raises an exception if +// that would mean giving a non-numeric property to an array.) if +// options.noCreate is true, return undefined instead. +// +// may modify the last element of keyparts to signal to the caller that it needs +// to use a different value to index into the returned object (for example, +// ['a', '01'] -> ['a', 1]). +// +// if forbidArray is true, return null if the keypath goes through an array. +// +// if options.arrayIndices is set, use its first element for the (first) '$' in +// the path. +var findModTarget = function (doc, keyparts, options) { + options = options || {}; + var usedArrayIndex = false; + for (var i = 0; i < keyparts.length; i++) { + var last = (i === keyparts.length - 1); + var keypart = keyparts[i]; + var indexable = isIndexable(doc); + if (!indexable) { + if (options.noCreate) + return undefined; + var e = MinimongoError( + "cannot use the part '" + keypart + "' to traverse " + doc); + e.setPropertyError = true; + throw e; + } + if (doc instanceof Array) { + if (options.forbidArray) + return null; + if (keypart === '$') { + if (usedArrayIndex) + throw MinimongoError("Too many positional (i.e. '$') elements"); + if (!options.arrayIndices || !options.arrayIndices.length) { + throw MinimongoError("The positional operator did not find the " + + "match needed from the query"); + } + keypart = options.arrayIndices[0]; + usedArrayIndex = true; + } else if (isNumericKey(keypart)) { + keypart = parseInt(keypart); + } else { + if (options.noCreate) + return undefined; + throw MinimongoError( + "can't append to array using string field name [" + + keypart + "]"); + } + if (last) + // handle 'a.01' + keyparts[i] = keypart; + if (options.noCreate && keypart >= doc.length) + return undefined; + while (doc.length < keypart) + doc.push(null); + if (!last) { + if (doc.length === keypart) + doc.push({}); + else if (typeof doc[keypart] !== "object") + throw MinimongoError("can't modify field '" + keyparts[i + 1] + + "' of list value " + JSON.stringify(doc[keypart])); + } + } else { + assertIsValidFieldName(keypart); + if (!(keypart in doc)) { + if (options.noCreate) + return undefined; + if (!last) + doc[keypart] = {}; + } + } + + if (last) + return doc; + doc = doc[keypart]; + } + + // notreached +}; + +var NO_CREATE_MODIFIERS = { + $unset: true, + $pop: true, + $rename: true, + $pull: true, + $pullAll: true +}; + +var MODIFIERS = { + $currentDate: function (target, field, arg) { + if (typeof arg === "object" && arg.hasOwnProperty("$type")) { + if (arg.$type !== "date") { + throw MinimongoError( + "Minimongo does currently only support the date type " + + "in $currentDate modifiers", + { field }); + } + } else if (arg !== true) { + throw MinimongoError("Invalid $currentDate modifier", { field }); + } + target[field] = new Date(); + }, + $min: function (target, field, arg) { + if (typeof arg !== "number") { + throw MinimongoError("Modifier $min allowed for numbers only", { field }); + } + if (field in target) { + if (typeof target[field] !== "number") { + throw MinimongoError( + "Cannot apply $min modifier to non-number", { field }); + } + if (target[field] > arg) { + target[field] = arg; + } + } else { + target[field] = arg; + } + }, + $max: function (target, field, arg) { + if (typeof arg !== "number") { + throw MinimongoError("Modifier $max allowed for numbers only", { field }); + } + if (field in target) { + if (typeof target[field] !== "number") { + throw MinimongoError( + "Cannot apply $max modifier to non-number", { field }); + } + if (target[field] < arg) { + target[field] = arg; + } + } else { + target[field] = arg; + } + }, + $inc: function (target, field, arg) { + if (typeof arg !== "number") + throw MinimongoError("Modifier $inc allowed for numbers only", { field }); + if (field in target) { + if (typeof target[field] !== "number") + throw MinimongoError( + "Cannot apply $inc modifier to non-number", { field }); + target[field] += arg; + } else { + target[field] = arg; + } + }, + $set: function (target, field, arg) { + if (target !== Object(target)) { // not an array or an object + var e = MinimongoError( + "Cannot set property on non-object field", { field }); + e.setPropertyError = true; + throw e; + } + if (target === null) { + var e = MinimongoError("Cannot set property on null", { field }); + e.setPropertyError = true; + throw e; + } + assertHasValidFieldNames(arg); + target[field] = arg; + }, + $setOnInsert: function (target, field, arg) { + // converted to `$set` in `_modify` + }, + $unset: function (target, field, arg) { + if (target !== undefined) { + if (target instanceof Array) { + if (field in target) + target[field] = null; + } else + delete target[field]; + } + }, + $push: function (target, field, arg) { + if (target[field] === undefined) + target[field] = []; + if (!(target[field] instanceof Array)) + throw MinimongoError( + "Cannot apply $push modifier to non-array", { field }); + + if (!(arg && arg.$each)) { + // Simple mode: not $each + assertHasValidFieldNames(arg); + target[field].push(arg); + return; + } + + // Fancy mode: $each (and maybe $slice and $sort and $position) + var toPush = arg.$each; + if (!(toPush instanceof Array)) + throw MinimongoError("$each must be an array", { field }); + assertHasValidFieldNames(toPush); + + // Parse $position + var position = undefined; + if ('$position' in arg) { + if (typeof arg.$position !== "number") + throw MinimongoError("$position must be a numeric value", { field }); + // XXX should check to make sure integer + if (arg.$position < 0) + throw MinimongoError( + "$position in $push must be zero or positive", { field }); + position = arg.$position; + } + + // Parse $slice. + var slice = undefined; + if ('$slice' in arg) { + if (typeof arg.$slice !== "number") + throw MinimongoError("$slice must be a numeric value", { field }); + // XXX should check to make sure integer + slice = arg.$slice; + } + + // Parse $sort. + var sortFunction = undefined; + if (arg.$sort) { + if (slice === undefined) + throw MinimongoError("$sort requires $slice to be present", { field }); + // XXX this allows us to use a $sort whose value is an array, but that's + // actually an extension of the Node driver, so it won't work + // server-side. Could be confusing! + // XXX is it correct that we don't do geo-stuff here? + sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); + for (var i = 0; i < toPush.length; i++) { + if (LocalCollection._f._type(toPush[i]) !== 3) { + throw MinimongoError("$push like modifiers using $sort " + + "require all elements to be objects", { field }); + } + } + } + + // Actually push. + if (position === undefined) { + for (var j = 0; j < toPush.length; j++) + target[field].push(toPush[j]); + } else { + var spliceArguments = [position, 0]; + for (var j = 0; j < toPush.length; j++) + spliceArguments.push(toPush[j]); + Array.prototype.splice.apply(target[field], spliceArguments); + } + + // Actually sort. + if (sortFunction) + target[field].sort(sortFunction); + + // Actually slice. + if (slice !== undefined) { + if (slice === 0) + target[field] = []; // differs from Array.slice! + else if (slice < 0) + target[field] = target[field].slice(slice); + else + target[field] = target[field].slice(0, slice); + } + }, + $pushAll: function (target, field, arg) { + if (!(typeof arg === "object" && arg instanceof Array)) + throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); + assertHasValidFieldNames(arg); + var x = target[field]; + if (x === undefined) + target[field] = arg; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $pushAll modifier to non-array", { field }); + else { + for (var i = 0; i < arg.length; i++) + x.push(arg[i]); + } + }, + $addToSet: function (target, field, arg) { + var isEach = false; + if (typeof arg === "object") { + //check if first key is '$each' + const keys = Object.keys(arg); + if (keys[0] === "$each"){ + isEach = true; + } + } + var values = isEach ? arg["$each"] : [arg]; + assertHasValidFieldNames(values); + var x = target[field]; + if (x === undefined) + target[field] = values; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $addToSet modifier to non-array", { field }); + else { + values.forEach(function (value) { + for (var i = 0; i < x.length; i++) + if (LocalCollection._f._equal(value, x[i])) + return; + x.push(value); + }); + } + }, + $pop: function (target, field, arg) { + if (target === undefined) + return; + var x = target[field]; + if (x === undefined) + return; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $pop modifier to non-array", { field }); + else { + if (typeof arg === 'number' && arg < 0) + x.splice(0, 1); + else + x.pop(); + } + }, + $pull: function (target, field, arg) { + if (target === undefined) + return; + var x = target[field]; + if (x === undefined) + return; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $pull/pullAll modifier to non-array", { field }); + else { + var out = []; + if (arg != null && typeof arg === "object" && !(arg instanceof Array)) { + // XXX would be much nicer to compile this once, rather than + // for each document we modify.. but usually we're not + // modifying that many documents, so we'll let it slide for + // now + + // XXX Minimongo.Matcher isn't up for the job, because we need + // to permit stuff like {$pull: {a: {$gt: 4}}}.. something + // like {$gt: 4} is not normally a complete selector. + // same issue as $elemMatch possibly? + var matcher = new Minimongo.Matcher(arg); + for (var i = 0; i < x.length; i++) + if (!matcher.documentMatches(x[i]).result) + out.push(x[i]); + } else { + for (var i = 0; i < x.length; i++) + if (!LocalCollection._f._equal(x[i], arg)) + out.push(x[i]); + } + target[field] = out; + } + }, + $pullAll: function (target, field, arg) { + if (!(typeof arg === "object" && arg instanceof Array)) + throw MinimongoError( + "Modifier $pushAll/pullAll allowed for arrays only", { field }); + if (target === undefined) + return; + var x = target[field]; + if (x === undefined) + return; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $pull/pullAll modifier to non-array", { field }); + else { + var out = []; + for (var i = 0; i < x.length; i++) { + var exclude = false; + for (var j = 0; j < arg.length; j++) { + if (LocalCollection._f._equal(x[i], arg[j])) { + exclude = true; + break; + } + } + if (!exclude) + out.push(x[i]); + } + target[field] = out; + } + }, + $rename: function (target, field, arg, keypath, doc) { + if (keypath === arg) + // no idea why mongo has this restriction.. + throw MinimongoError("$rename source must differ from target", { field }); + if (target === null) + throw MinimongoError("$rename source field invalid", { field }); + if (typeof arg !== "string") + throw MinimongoError("$rename target must be a string", { field }); + if (arg.indexOf('\0') > -1) { + // Null bytes are not allowed in Mongo field names + // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names + throw MinimongoError( + "The 'to' field for $rename cannot contain an embedded null byte", + { field }); + } + if (target === undefined) + return; + var v = target[field]; + delete target[field]; + + var keyparts = arg.split('.'); + var target2 = findModTarget(doc, keyparts, {forbidArray: true}); + if (target2 === null) + throw MinimongoError("$rename target field invalid", { field }); + var field2 = keyparts.pop(); + target2[field2] = v; + }, + $bit: function (target, field, arg) { + // XXX mongo only supports $bit on integers, and we only support + // native javascript numbers (doubles) so far, so we can't support $bit + throw MinimongoError("$bit is not supported", { field }); + } +}; +// ordered: bool. +// old_results and new_results: collections of documents. +// if ordered, they are arrays. +// if unordered, they are IdMaps +LocalCollection._diffQueryChanges = function (ordered, oldResults, newResults, observer, options) { + return DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options); +}; + +LocalCollection._diffQueryUnorderedChanges = function (oldResults, newResults, observer, options) { + return DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options); +}; + + +LocalCollection._diffQueryOrderedChanges = + function (oldResults, newResults, observer, options) { + return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options); +}; + +LocalCollection._diffObjects = function (left, right, callbacks) { + return DiffSequence.diffObjects(left, right, callbacks); +}; +LocalCollection._IdMap = function () { + var self = this; + IdMap.call(self, MongoID.idStringify, MongoID.idParse); +}; + +Meteor._inherits(LocalCollection._IdMap, IdMap); + +// XXX maybe move these into another ObserveHelpers package or something + +// _CachingChangeObserver is an object which receives observeChanges callbacks +// and keeps a cache of the current cursor state up to date in self.docs. Users +// of this class should read the docs field but not modify it. You should pass +// the "applyChange" field as the callbacks to the underlying observeChanges +// call. Optionally, you can specify your own observeChanges callbacks which are +// invoked immediately before the docs field is updated; this object is made +// available as `this` to those callbacks. +LocalCollection._CachingChangeObserver = function (options) { + var self = this; + options = options || {}; + + var orderedFromCallbacks = options.callbacks && + LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); + if (options.hasOwnProperty('ordered')) { + self.ordered = options.ordered; + if (options.callbacks && options.ordered !== orderedFromCallbacks) + throw Error("ordered option doesn't match callbacks"); + } else if (options.callbacks) { + self.ordered = orderedFromCallbacks; + } else { + throw Error("must provide ordered or callbacks"); + } + var callbacks = options.callbacks || {}; + + if (self.ordered) { + self.docs = new OrderedDict(MongoID.idStringify); + self.applyChange = { + addedBefore: function (id, fields, before) { + var doc = EJSON.clone(fields); + doc._id = id; + callbacks.addedBefore && callbacks.addedBefore.call( + self, id, fields, before); + // This line triggers if we provide added with movedBefore. + callbacks.added && callbacks.added.call(self, id, fields); + // XXX could `before` be a falsy ID? Technically + // idStringify seems to allow for them -- though + // OrderedDict won't call stringify on a falsy arg. + self.docs.putBefore(id, doc, before || null); + }, + movedBefore: function (id, before) { + var doc = self.docs.get(id); + callbacks.movedBefore && callbacks.movedBefore.call(self, id, before); + self.docs.moveBefore(id, before || null); + } + }; + } else { + self.docs = new LocalCollection._IdMap; + self.applyChange = { + added: function (id, fields) { + var doc = EJSON.clone(fields); + callbacks.added && callbacks.added.call(self, id, fields); + doc._id = id; + self.docs.set(id, doc); + } + }; + } + + // The methods in _IdMap and OrderedDict used by these callbacks are + // identical. + self.applyChange.changed = function (id, fields) { + var doc = self.docs.get(id); + if (!doc) + throw new Error("Unknown id for changed: " + id); + callbacks.changed && callbacks.changed.call( + self, id, EJSON.clone(fields)); + DiffSequence.applyChanges(doc, fields); + }; + self.applyChange.removed = function (id) { + callbacks.removed && callbacks.removed.call(self, id); + self.docs.remove(id); + }; +}; + +LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) { + var transform = cursor.getTransform() || function (doc) {return doc;}; + var suppressed = !!observeCallbacks._suppress_initial; + + var observeChangesCallbacks; + if (LocalCollection._observeCallbacksAreOrdered(observeCallbacks)) { + // The "_no_indices" option sets all index arguments to -1 and skips the + // linear scans required to generate them. This lets observers that don't + // need absolute indices benefit from the other features of this API -- + // relative order, transforms, and applyChanges -- without the speed hit. + var indices = !observeCallbacks._no_indices; + observeChangesCallbacks = { + addedBefore: function (id, fields, before) { + var self = this; + if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) + return; + var doc = transform(Object.assign(fields, {_id: id})); + if (observeCallbacks.addedAt) { + var index = indices + ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; + observeCallbacks.addedAt(doc, index, before); + } else { + observeCallbacks.added(doc); + } + }, + changed: function (id, fields) { + var self = this; + if (!(observeCallbacks.changedAt || observeCallbacks.changed)) + return; + var doc = EJSON.clone(self.docs.get(id)); + if (!doc) + throw new Error("Unknown id for changed: " + id); + var oldDoc = transform(EJSON.clone(doc)); + DiffSequence.applyChanges(doc, fields); + doc = transform(doc); + if (observeCallbacks.changedAt) { + var index = indices ? self.docs.indexOf(id) : -1; + observeCallbacks.changedAt(doc, oldDoc, index); + } else { + observeCallbacks.changed(doc, oldDoc); + } + }, + movedBefore: function (id, before) { + var self = this; + if (!observeCallbacks.movedTo) + return; + var from = indices ? self.docs.indexOf(id) : -1; + + var to = indices + ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; + // When not moving backwards, adjust for the fact that removing the + // document slides everything back one slot. + if (to > from) + --to; + observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))), + from, to, before || null); + }, + removed: function (id) { + var self = this; + if (!(observeCallbacks.removedAt || observeCallbacks.removed)) + return; + // technically maybe there should be an EJSON.clone here, but it's about + // to be removed from self.docs! + var doc = transform(self.docs.get(id)); + if (observeCallbacks.removedAt) { + var index = indices ? self.docs.indexOf(id) : -1; + observeCallbacks.removedAt(doc, index); + } else { + observeCallbacks.removed(doc); + } + } + }; + } else { + observeChangesCallbacks = { + added: function (id, fields) { + if (!suppressed && observeCallbacks.added) { + var doc = Object.assign(fields, {_id: id}); + observeCallbacks.added(transform(doc)); + } + }, + changed: function (id, fields) { + var self = this; + if (observeCallbacks.changed) { + var oldDoc = self.docs.get(id); + var doc = EJSON.clone(oldDoc); + DiffSequence.applyChanges(doc, fields); + observeCallbacks.changed(transform(doc), + transform(EJSON.clone(oldDoc))); + } + }, + removed: function (id) { + var self = this; + if (observeCallbacks.removed) { + observeCallbacks.removed(transform(self.docs.get(id))); + } + } + }; + } + + var changeObserver = new LocalCollection._CachingChangeObserver( + {callbacks: observeChangesCallbacks}); + var handle = cursor.observeChanges(changeObserver.applyChange); + suppressed = false; + + return handle; +}; +// Is this selector just shorthand for lookup by _id? +LocalCollection._selectorIsId = function (selector) { + return (typeof selector === "string") || + (typeof selector === "number") || + selector instanceof MongoID.ObjectID; +}; + +// Is the selector just lookup by _id (shorthand or not)? +LocalCollection._selectorIsIdPerhapsAsObject = function (selector) { + return LocalCollection._selectorIsId(selector) || + (selector && typeof selector === "object" && + selector._id && LocalCollection._selectorIsId(selector._id) && + Object.keys(selector).length === 1); +}; + +// If this is a selector which explicitly constrains the match by ID to a finite +// number of documents, returns a list of their IDs. Otherwise returns +// null. Note that the selector may have other restrictions so it may not even +// match those document! We care about $in and $and since those are generated +// access-controlled update and remove. +LocalCollection._idsMatchedBySelector = function (selector) { + // Is the selector just an ID? + if (LocalCollection._selectorIsId(selector)) + return [selector]; + if (!selector) + return null; + + // Do we have an _id clause? + if (selector.hasOwnProperty('_id')) { + // Is the _id clause just an ID? + if (LocalCollection._selectorIsId(selector._id)) + return [selector._id]; + // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? + if (selector._id && selector._id.$in + && Array.isArray(selector._id.$in) + && selector._id.$in.length + && selector._id.$in.every(LocalCollection._selectorIsId)) { + return selector._id.$in; + } + return null; + } + + // If this is a top-level $and, and any of the clauses constrain their + // documents, then the whole selector is constrained by any one clause's + // constraint. (Well, by their intersection, but that seems unlikely.) + if (selector.$and && Array.isArray(selector.$and)) { + for (var i = 0; i < selector.$and.length; ++i) { + var subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); + if (subIds) + return subIds; + } + } + + return null; +}; + + diff --git a/packages/minimongo/selector_modifier.js b/packages/minimongo/minimongo_server.js similarity index 75% rename from packages/minimongo/selector_modifier.js rename to packages/minimongo/minimongo_server.js index 32c9f79a6f..acedd58e30 100644 --- a/packages/minimongo/selector_modifier.js +++ b/packages/minimongo/minimongo_server.js @@ -1,3 +1,74 @@ +// Knows how to combine a mongo selector and a fields projection to a new fields +// projection taking into account active fields from the passed selector. +// @returns Object - projection object (same as fields option of mongo cursor) +Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { + var self = this; + var selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); + + // Special case for $where operator in the selector - projection should depend + // on all fields of the document. getSelectorPaths returns a list of paths + // selector depends on. If one of the paths is '' (empty string) representing + // the root or the whole document, complete projection should be returned. + if (selectorPaths.includes('')) + return {}; + + return combineImportantPathsIntoProjection(selectorPaths, projection); +}; + +Minimongo._pathsElidingNumericKeys = function (paths) { + var self = this; + return paths.map(function (path) { + return path.split('.').filter(function (part) { return !isNumericKey(part); }).join('.'); + }); +}; + +combineImportantPathsIntoProjection = function (paths, projection) { + var prjDetails = projectionDetails(projection); + var tree = prjDetails.tree; + var mergedProjection = {}; + + // merge the paths to include + tree = pathsToTree(paths, + function (path) { return true; }, + function (node, path, fullPath) { return true; }, + tree); + mergedProjection = treeToPaths(tree); + if (prjDetails.including) { + // both selector and projection are pointing on fields to include + // so we can just return the merged tree + return mergedProjection; + } else { + // selector is pointing at fields to include + // projection is pointing at fields to exclude + // make sure we don't exclude important paths + var mergedExclProjection = {}; + Object.keys(mergedProjection).forEach(function (path) { + var incl = mergedProjection[path]; + if (!incl) + mergedExclProjection[path] = false; + }); + + return mergedExclProjection; + } +}; + +// Returns a set of key paths similar to +// { 'foo.bar': 1, 'a.b.c': 1 } +var treeToPaths = function (tree, prefix) { + prefix = prefix || ''; + var result = {}; + + Object.keys(tree).forEach(function (key) { + var val = tree[key]; + if (val === Object(val)) + Object.assign(result, treeToPaths(val, prefix + key + '.')); + else + result[prefix + key] = val; + }); + + return result; +}; + // Returns true if the modifier applied to some document may change the result // of matching the document by selector // The modifier is always in a form of Object: @@ -223,3 +294,8 @@ var startsWith = function(str, starts) { return str.length >= starts.length && str.substring(0, starts.length) === starts; }; +Minimongo.Sorter.prototype.combineIntoProjection = function (projection) { + var self = this; + var specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); + return combineImportantPathsIntoProjection(specPaths, projection); +}; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 8bc09c95d0..3c1f84196a 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1,3735 +1,58 @@ +Tinytest.add("minimongo - wrapTransform", function (test) { + var wrap = LocalCollection.wrapTransform; -// Hack to make LocalCollection generate ObjectIDs by default. -LocalCollection._useOID = true; + // Transforming no function gives falsey. + test.isFalse(wrap(undefined)); + test.isFalse(wrap(null)); -// assert that f is a strcmp-style comparison function that puts -// 'values' in the provided order - -var assert_ordering = function (test, f, values) { - for (var i = 0; i < values.length; i++) { - var x = f(values[i], values[i]); - if (x !== 0) { - // XXX super janky - test.fail({type: "minimongo-ordering", - message: "value doesn't order as equal to itself", - value: JSON.stringify(values[i]), - should_be_zero_but_got: JSON.stringify(x)}); - } - if (i + 1 < values.length) { - var less = values[i]; - var more = values[i + 1]; - var x = f(less, more); - if (!(x < 0)) { - // XXX super janky - test.fail({type: "minimongo-ordering", - message: "ordering test failed", - first: JSON.stringify(less), - second: JSON.stringify(more), - should_be_negative_but_got: JSON.stringify(x)}); - } - x = f(more, less); - if (!(x > 0)) { - // XXX super janky - test.fail({type: "minimongo-ordering", - message: "ordering test failed", - first: JSON.stringify(less), - second: JSON.stringify(more), - should_be_positive_but_got: JSON.stringify(x)}); - } - } - } -}; - -var log_callbacks = function (operations) { - return { - addedAt: function (obj, idx, before) { - delete obj._id; - operations.push(EJSON.clone(['added', obj, idx, before])); - }, - changedAt: function (obj, old_obj, at) { - delete obj._id; - delete old_obj._id; - operations.push(EJSON.clone(['changed', obj, at, old_obj])); - }, - movedTo: function (obj, old_at, new_at, before) { - delete obj._id; - operations.push(EJSON.clone(['moved', obj, old_at, new_at, before])); - }, - removedAt: function (old_obj, at) { - var id = old_obj._id; - delete old_obj._id; - operations.push(EJSON.clone(['removed', id, at, old_obj])); - } + // It's OK if you don't change the ID. + var validTransform = function (doc) { + delete doc.x; + doc.y = 42; + doc.z = function () { return 43; }; + return doc; }; -}; + var transformed = wrap(validTransform)({_id: "asdf", x: 54}); + test.equal(Object.keys(transformed), ['_id', 'y', 'z']); + test.equal(transformed.y, 42); + test.equal(transformed.z(), 43); -// XXX test shared structure in all MM entrypoints -Tinytest.add("minimongo - basics", function (test) { - var c = new LocalCollection(), - fluffyKitten_id, - count; - - fluffyKitten_id = c.insert({type: "kitten", name: "fluffy"}); - c.insert({type: "kitten", name: "snookums"}); - c.insert({type: "cryptographer", name: "alice"}); - c.insert({type: "cryptographer", name: "bob"}); - c.insert({type: "cryptographer", name: "cara"}); - test.equal(c.find().count(), 5); - test.equal(c.find({type: "kitten"}).count(), 2); - test.equal(c.find({type: "cryptographer"}).count(), 3); - test.length(c.find({type: "kitten"}).fetch(), 2); - test.length(c.find({type: "cryptographer"}).fetch(), 3); - test.equal(fluffyKitten_id, c.findOne({type: "kitten", name: "fluffy"})._id); - - c.remove({name: "cara"}); - test.equal(c.find().count(), 4); - test.equal(c.find({type: "kitten"}).count(), 2); - test.equal(c.find({type: "cryptographer"}).count(), 2); - test.length(c.find({type: "kitten"}).fetch(), 2); - test.length(c.find({type: "cryptographer"}).fetch(), 2); - - count = c.update({name: "snookums"}, {$set: {type: "cryptographer"}}); - test.equal(count, 1); - test.equal(c.find().count(), 4); - test.equal(c.find({type: "kitten"}).count(), 1); - test.equal(c.find({type: "cryptographer"}).count(), 3); - test.length(c.find({type: "kitten"}).fetch(), 1); - test.length(c.find({type: "cryptographer"}).fetch(), 3); - - c.remove(null); - c.remove(false); - c.remove(undefined); - test.equal(c.find().count(), 4); - - c.remove({_id: null}); - c.remove({_id: false}); - c.remove({_id: undefined}); - count = c.remove(); - test.equal(count, 0); - test.equal(c.find().count(), 4); - - count = c.remove({}); - test.equal(count, 4); - test.equal(c.find().count(), 0); - - c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); - c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); - c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); - - test.equal(c.find({tags: "flower"}).count(), 1); - test.equal(c.find({tags: "fruit"}).count(), 2); - test.equal(c.find({tags: "red"}).count(), 3); - test.length(c.find({tags: "flower"}).fetch(), 1); - test.length(c.find({tags: "fruit"}).fetch(), 2); - test.length(c.find({tags: "red"}).fetch(), 3); - - test.equal(c.findOne(1).name, "strawberry"); - test.equal(c.findOne(2).name, "apple"); - test.equal(c.findOne(3).name, "rose"); - test.equal(c.findOne(4), undefined); - test.equal(c.findOne("abc"), undefined); - test.equal(c.findOne(undefined), undefined); - - test.equal(c.find(1).count(), 1); - test.equal(c.find(4).count(), 0); - test.equal(c.find("abc").count(), 0); - test.equal(c.find(undefined).count(), 0); - test.equal(c.find().count(), 3); - test.equal(c.find(1, {skip: 1}).count(), 0); - test.equal(c.find({_id: 1}, {skip: 1}).count(), 0); - test.equal(c.find({}, {skip: 1}).count(), 2); - test.equal(c.find({}, {skip: 2}).count(), 1); - test.equal(c.find({}, {limit: 2}).count(), 2); - test.equal(c.find({}, {limit: 1}).count(), 1); - test.equal(c.find({}, {skip: 1, limit: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {skip: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {limit: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {skip: 1, limit: 1}).count(), 1); - test.equal(c.find(1, {sort: ['_id','desc'], skip: 1}).count(), 0); - test.equal(c.find({_id: 1}, {sort: ['_id','desc'], skip: 1}).count(), 0); - test.equal(c.find({}, {sort: ['_id','desc'], skip: 1}).count(), 2); - test.equal(c.find({}, {sort: ['_id','desc'], skip: 2}).count(), 1); - test.equal(c.find({}, {sort: ['_id','desc'], limit: 2}).count(), 2); - test.equal(c.find({}, {sort: ['_id','desc'], limit: 1}).count(), 1); - test.equal(c.find({}, {sort: ['_id','desc'], skip: 1, limit: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], skip: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], limit: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], skip: 1, limit: 1}).count(), 1); - - // Regression test for #455. - c.insert({foo: {bar: 'baz'}}); - test.equal(c.find({foo: {bam: 'baz'}}).count(), 0); - test.equal(c.find({foo: {bar: 'baz'}}).count(), 1); - -}); - -Tinytest.add("minimongo - error - no options", function (test) { - try { - throw MinimongoError("Not fun to have errors"); - } catch (e) { - test.equal(e.message, "Not fun to have errors"); - } -}); - -Tinytest.add("minimongo - error - with field", function (test) { - try { - throw MinimongoError("Cats are no fun", { field: "mice" }); - } catch (e) { - test.equal(e.message, "Cats are no fun for field 'mice'"); - } -}); - -Tinytest.add("minimongo - cursors", function (test) { - var c = new LocalCollection(); - var res; - - for (var i = 0; i < 20; i++) - c.insert({i: i}); - - var q = c.find(); - test.equal(q.count(), 20); - - // fetch - res = q.fetch(); - test.length(res, 20); - for (var i = 0; i < 20; i++) { - test.equal(res[i].i, i); - } - // call it again, it still works - test.length(q.fetch(), 20); - - // forEach - var count = 0; - var context = {}; - q.forEach(function (obj, i, cursor) { - test.equal(obj.i, count++); - test.equal(obj.i, i); - test.isTrue(context === this); - test.isTrue(cursor === q); - }, context); - test.equal(count, 20); - // call it again, it still works - test.length(q.fetch(), 20); - - // map - res = q.map(function (obj, i, cursor) { - test.equal(obj.i, i); - test.isTrue(context === this); - test.isTrue(cursor === q); - return obj.i * 2; - }, context); - test.length(res, 20); - for (var i = 0; i < 20; i++) - test.equal(res[i], i * 2); - // call it again, it still works - test.length(q.fetch(), 20); - - // findOne (and no rewind first) - test.equal(c.findOne({i: 0}).i, 0); - test.equal(c.findOne({i: 1}).i, 1); - var id = c.findOne({i: 2})._id; - test.equal(c.findOne(id).i, 2); -}); - -Tinytest.add("minimongo - transform", function (test) { - var c = new LocalCollection; - c.insert({}); - // transform functions must return objects - var invalidTransform = function (doc) { return doc._id; }; - test.throws(function () { - c.findOne({}, {transform: invalidTransform}); - }); - - // transformed documents get _id field transplanted if not present - var transformWithoutId = function (doc) { - var docWithoutId = Object.assign({}, doc); - delete docWithoutId._id; - return docWithoutId; - }; - test.equal(c.findOne({}, {transform: transformWithoutId})._id, - c.findOne()._id); -}); - -Tinytest.add("minimongo - misc", function (test) { - // deepcopy - var a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]}, - f: null, g: new Date()}; - var b = EJSON.clone(a); - test.equal(a, b); - test.isTrue(LocalCollection._f._equal(a, b)); - a.a.push(4); - test.length(b.a, 3); - a.c = false; - test.isTrue(b.c); - b.d.z = 15; - a.d.z = 14; - test.equal(b.d.z, 15); - a.d.y.push(88); - test.length(b.d.y, 1); - test.equal(a.g, b.g); - b.g.setDate(b.g.getDate() + 1); - test.notEqual(a.g, b.g); - - a = {x: function () {}}; - b = EJSON.clone(a); - a.x.a = 14; - test.equal(b.x.a, 14); // just to document current behavior -}); - -Tinytest.add("minimongo - lookup", function (test) { - var lookupA = MinimongoTest.makeLookupFunction('a'); - test.equal(lookupA({}), [{value: undefined}]); - test.equal(lookupA({a: 1}), [{value: 1}]); - test.equal(lookupA({a: [1]}), [{value: [1]}]); - - var lookupAX = MinimongoTest.makeLookupFunction('a.x'); - test.equal(lookupAX({a: {x: 1}}), [{value: 1}]); - test.equal(lookupAX({a: {x: [1]}}), [{value: [1]}]); - test.equal(lookupAX({a: 5}), [{value: undefined}]); - test.equal(lookupAX({a: [{x: 1}, {x: [2]}, {y: 3}]}), - [{value: 1, arrayIndices: [0]}, - {value: [2], arrayIndices: [1]}, - {value: undefined, arrayIndices: [2]}]); - - var lookupA0X = MinimongoTest.makeLookupFunction('a.0.x'); - test.equal(lookupA0X({a: [{x: 1}]}), [ - // From interpreting '0' as "0th array element". - {value: 1, arrayIndices: [0, 'x']}, - // From interpreting '0' as "after branching in the array, look in the - // object {x:1} for a field named 0". - {value: undefined, arrayIndices: [0]}]); - test.equal(lookupA0X({a: [{x: [1]}]}), [ - {value: [1], arrayIndices: [0, 'x']}, - {value: undefined, arrayIndices: [0]}]); - test.equal(lookupA0X({a: 5}), [{value: undefined}]); - test.equal(lookupA0X({a: [{x: 1}, {x: [2]}, {y: 3}]}), [ - // From interpreting '0' as "0th array element". - {value: 1, arrayIndices: [0, 'x']}, - // From interpreting '0' as "after branching in the array, look in the - // object {x:1} for a field named 0". - {value: undefined, arrayIndices: [0]}, - {value: undefined, arrayIndices: [1]}, - {value: undefined, arrayIndices: [2]} - ]); - - test.equal( - MinimongoTest.makeLookupFunction('w.x.0.z')({ - w: [{x: [{z: 5}]}]}), [ - // From interpreting '0' as "0th array element". - {value: 5, arrayIndices: [0, 0, 'x']}, - // From interpreting '0' as "after branching in the array, look in the - // object {z:5} for a field named "0". - {value: undefined, arrayIndices: [0, 0]} - ]); -}); - -Tinytest.add("minimongo - selector_compiler", function (test) { - var matches = function (shouldMatch, selector, doc) { - var doesMatch = new Minimongo.Matcher(selector).documentMatches(doc).result; - if (doesMatch != shouldMatch) { - // XXX super janky - test.fail({message: "minimongo match failure: document " + - (shouldMatch ? "should match, but doesn't" : - "shouldn't match, but does"), - selector: JSON.stringify(selector), - document: JSON.stringify(doc) - }); - } - }; - - var match = matches.bind(null, true); - var nomatch = matches.bind(null, false); - - // XXX blog post about what I learned while writing these tests (weird - // mongo edge cases) - - // empty selectors - match({}, {}); - match({}, {a: 12}); - - // scalars - match(1, {_id: 1, a: 'foo'}); - nomatch(1, {_id: 2, a: 'foo'}); - match('a', {_id: 'a', a: 'foo'}); - nomatch('a', {_id: 'b', a: 'foo'}); - - // safety - nomatch(undefined, {}); - nomatch(undefined, {_id: 'foo'}); - nomatch(false, {_id: 'foo'}); - nomatch(null, {_id: 'foo'}); - nomatch({_id: undefined}, {_id: 'foo'}); - nomatch({_id: false}, {_id: 'foo'}); - nomatch({_id: null}, {_id: 'foo'}); - - // matching one or more keys - nomatch({a: 12}, {}); - match({a: 12}, {a: 12}); - match({a: 12}, {a: 12, b: 13}); - match({a: 12, b: 13}, {a: 12, b: 13}); - match({a: 12, b: 13}, {a: 12, b: 13, c: 14}); - nomatch({a: 12, b: 13, c: 14}, {a: 12, b: 13}); - nomatch({a: 12, b: 13}, {b: 13, c: 14}); - - match({a: 12}, {a: [12]}); - match({a: 12}, {a: [11, 12, 13]}); - nomatch({a: 12}, {a: [11, 13]}); - match({a: 12, b: 13}, {a: [11, 12, 13], b: [13, 14, 15]}); - nomatch({a: 12, b: 13}, {a: [11, 12, 13], b: [14, 15]}); - - // dates - var date1 = new Date; - var date2 = new Date(date1.getTime() + 1000); - match({a: date1}, {a: date1}); - nomatch({a: date1}, {a: date2}); - - - // arrays - match({a: [1,2]}, {a: [1, 2]}); - match({a: [1,2]}, {a: [[1, 2]]}); - match({a: [1,2]}, {a: [[3, 4], [1, 2]]}); - nomatch({a: [1,2]}, {a: [3, 4]}); - nomatch({a: [1,2]}, {a: [[[1, 2]]]}); - - // literal documents - match({a: {b: 12}}, {a: {b: 12}}); - nomatch({a: {b: 12, c: 13}}, {a: {b: 12}}); - nomatch({a: {b: 12}}, {a: {b: 12, c: 13}}); - match({a: {b: 12, c: 13}}, {a: {b: 12, c: 13}}); - nomatch({a: {b: 12, c: 13}}, {a: {c: 13, b: 12}}); // tested on mongodb - nomatch({a: {}}, {a: {b: 12}}); - nomatch({a: {b:12}}, {a: {}}); - match( - {a: {b: 12, c: [13, true, false, 2.2, "a", null, {d: 14}]}}, - {a: {b: 12, c: [13, true, false, 2.2, "a", null, {d: 14}]}}); - match({a: {b: 12}}, {a: {b: 12}, k: 99}); - - match({a: {b: 12}}, {a: [{b: 12}]}); - nomatch({a: {b: 12}}, {a: [[{b: 12}]]}); - match({a: {b: 12}}, {a: [{b: 11}, {b: 12}, {b: 13}]}); - nomatch({a: {b: 12}}, {a: [{b: 11}, {b: 12, c: 20}, {b: 13}]}); - nomatch({a: {b: 12, c: 20}}, {a: [{b: 11}, {b: 12}, {c: 20}]}); - match({a: {b: 12, c: 20}}, {a: [{b: 11}, {b: 12, c: 20}, {b: 13}]}); - - // null - match({a: null}, {a: null}); - match({a: null}, {b: 12}); - nomatch({a: null}, {a: 12}); - match({a: null}, {a: [1, 2, null, 3]}); // tested on mongodb - nomatch({a: null}, {a: [1, 2, {}, 3]}); // tested on mongodb - - // order comparisons: $lt, $gt, $lte, $gte - match({a: {$lt: 10}}, {a: 9}); - nomatch({a: {$lt: 10}}, {a: 10}); - nomatch({a: {$lt: 10}}, {a: 11}); - - match({a: {$gt: 10}}, {a: 11}); - nomatch({a: {$gt: 10}}, {a: 10}); - nomatch({a: {$gt: 10}}, {a: 9}); - - match({a: {$lte: 10}}, {a: 9}); - match({a: {$lte: 10}}, {a: 10}); - nomatch({a: {$lte: 10}}, {a: 11}); - - match({a: {$gte: 10}}, {a: 11}); - match({a: {$gte: 10}}, {a: 10}); - nomatch({a: {$gte: 10}}, {a: 9}); - - match({a: {$lt: 10}}, {a: [11, 9, 12]}); - nomatch({a: {$lt: 10}}, {a: [11, 12]}); - - // (there's a full suite of ordering test elsewhere) - nomatch({a: {$lt: "null"}}, {a: null}); - match({a: {$lt: {x: [2, 3, 4]}}}, {a: {x: [1, 3, 4]}}); - match({a: {$gt: {x: [2, 3, 4]}}}, {a: {x: [3, 3, 4]}}); - nomatch({a: {$gt: {x: [2, 3, 4]}}}, {a: {x: [1, 3, 4]}}); - nomatch({a: {$gt: {x: [2, 3, 4]}}}, {a: {x: [2, 3, 4]}}); - nomatch({a: {$lt: {x: [2, 3, 4]}}}, {a: {x: [2, 3, 4]}}); - match({a: {$gte: {x: [2, 3, 4]}}}, {a: {x: [2, 3, 4]}}); - match({a: {$lte: {x: [2, 3, 4]}}}, {a: {x: [2, 3, 4]}}); - - nomatch({a: {$gt: [2, 3]}}, {a: [1, 2]}); // tested against mongodb - - // composition of two qualifiers - nomatch({a: {$lt: 11, $gt: 9}}, {a: 8}); - nomatch({a: {$lt: 11, $gt: 9}}, {a: 9}); - match({a: {$lt: 11, $gt: 9}}, {a: 10}); - nomatch({a: {$lt: 11, $gt: 9}}, {a: 11}); - nomatch({a: {$lt: 11, $gt: 9}}, {a: 12}); - - match({a: {$lt: 11, $gt: 9}}, {a: [8, 9, 10, 11, 12]}); - match({a: {$lt: 11, $gt: 9}}, {a: [8, 9, 11, 12]}); // tested against mongodb - - // $all - match({a: {$all: [1, 2]}}, {a: [1, 2]}); - nomatch({a: {$all: [1, 2, 3]}}, {a: [1, 2]}); - match({a: {$all: [1, 2]}}, {a: [3, 2, 1]}); - match({a: {$all: [1, "x"]}}, {a: [3, "x", 1]}); - nomatch({a: {$all: ['2']}}, {a: 2}); - nomatch({a: {$all: [2]}}, {a: '2'}); - match({a: {$all: [[1, 2], [1, 3]]}}, {a: [[1, 3], [1, 2], [1, 4]]}); - nomatch({a: {$all: [[1, 2], [1, 3]]}}, {a: [[1, 4], [1, 2], [1, 4]]}); - match({a: {$all: [2, 2]}}, {a: [2]}); // tested against mongodb - nomatch({a: {$all: [2, 3]}}, {a: [2, 2]}); - - nomatch({a: {$all: [1, 2]}}, {a: [[1, 2]]}); // tested against mongodb - nomatch({a: {$all: [1, 2]}}, {}); // tested against mongodb, field doesn't exist - nomatch({a: {$all: [1, 2]}}, {a: {foo: 'bar'}}); // tested against mongodb, field is not an object - nomatch({a: {$all: []}}, {a: []}); - nomatch({a: {$all: []}}, {a: [5]}); - match({a: {$all: [/i/, /e/i]}}, {a: ["foo", "bEr", "biz"]}); - nomatch({a: {$all: [/i/, /e/i]}}, {a: ["foo", "bar", "biz"]}); - match({a: {$all: [{b: 3}]}}, {a: [{b: 3}]}); - // Members of $all other than regexps are *equality matches*, not document - // matches. - nomatch({a: {$all: [{b: 3}]}}, {a: [{b: 3, k: 4}]}); - test.throws(function () { - match({a: {$all: [{$gt: 4}]}}, {}); - }); - - // $exists - match({a: {$exists: true}}, {a: 12}); - nomatch({a: {$exists: true}}, {b: 12}); - nomatch({a: {$exists: false}}, {a: 12}); - match({a: {$exists: false}}, {b: 12}); - - match({a: {$exists: true}}, {a: []}); - nomatch({a: {$exists: true}}, {b: []}); - nomatch({a: {$exists: false}}, {a: []}); - match({a: {$exists: false}}, {b: []}); - - match({a: {$exists: true}}, {a: [1]}); - nomatch({a: {$exists: true}}, {b: [1]}); - nomatch({a: {$exists: false}}, {a: [1]}); - match({a: {$exists: false}}, {b: [1]}); - - match({a: {$exists: 1}}, {a: 5}); - match({a: {$exists: 0}}, {b: 5}); - - nomatch({'a.x':{$exists: false}}, {a: [{}, {x: 5}]}); - match({'a.x':{$exists: true}}, {a: [{}, {x: 5}]}); - match({'a.x':{$exists: true}}, {a: [{}, {x: 5}]}); - match({'a.x':{$exists: true}}, {a: {x: []}}); - match({'a.x':{$exists: true}}, {a: {x: null}}); - - // $mod - match({a: {$mod: [10, 1]}}, {a: 11}); - nomatch({a: {$mod: [10, 1]}}, {a: 12}); - match({a: {$mod: [10, 1]}}, {a: [10, 11, 12]}); - nomatch({a: {$mod: [10, 1]}}, {a: [10, 12]}); - [ - 5, - [10], - [10, 1, 2], - "foo", - {bar: 1}, - [] - ].forEach(function (badMod) { - test.throws(function () { - match({a: {$mod: badMod}}, {a: 11}); - }); - }); - - // $eq - nomatch({a: {$eq: 1}}, {a: 2}); - match({a: {$eq: 2}}, {a: 2}); - nomatch({a: {$eq: [1]}}, {a: [2]}); - - match({a: {$eq: [1, 2]}}, {a: [1, 2]}); - match({a: {$eq: 1}}, {a: [1, 2]}); - match({a: {$eq: 2}}, {a: [1, 2]}); - nomatch({a: {$eq: 3}}, {a: [1, 2]}); - match({'a.b': {$eq: 1}}, {a: [{b: 1}, {b: 2}]}); - match({'a.b': {$eq: 2}}, {a: [{b: 1}, {b: 2}]}); - nomatch({'a.b': {$eq: 3}}, {a: [{b: 1}, {b: 2}]}); - - match({a: {$eq: {x: 1}}}, {a: {x: 1}}); - nomatch({a: {$eq: {x: 1}}}, {a: {x: 2}}); - nomatch({a: {$eq: {x: 1}}}, {a: {x: 1, y: 2}}); - - // $ne - match({a: {$ne: 1}}, {a: 2}); - nomatch({a: {$ne: 2}}, {a: 2}); - match({a: {$ne: [1]}}, {a: [2]}); - - nomatch({a: {$ne: [1, 2]}}, {a: [1, 2]}); // all tested against mongodb - nomatch({a: {$ne: 1}}, {a: [1, 2]}); - nomatch({a: {$ne: 2}}, {a: [1, 2]}); - match({a: {$ne: 3}}, {a: [1, 2]}); - nomatch({'a.b': {$ne: 1}}, {a: [{b: 1}, {b: 2}]}); - nomatch({'a.b': {$ne: 2}}, {a: [{b: 1}, {b: 2}]}); - match({'a.b': {$ne: 3}}, {a: [{b: 1}, {b: 2}]}); - - nomatch({a: {$ne: {x: 1}}}, {a: {x: 1}}); - match({a: {$ne: {x: 1}}}, {a: {x: 2}}); - match({a: {$ne: {x: 1}}}, {a: {x: 1, y: 2}}); - - // This query means: All 'a.b' must be non-5, and some 'a.b' must be >6. - match({'a.b': {$ne: 5, $gt: 6}}, {a: [{b: 2}, {b: 10}]}); - nomatch({'a.b': {$ne: 5, $gt: 6}}, {a: [{b: 2}, {b: 4}]}); - nomatch({'a.b': {$ne: 5, $gt: 6}}, {a: [{b: 2}, {b: 5}]}); - nomatch({'a.b': {$ne: 5, $gt: 6}}, {a: [{b: 10}, {b: 5}]}); - // Should work the same if the branch is at the bottom. - match({a: {$ne: 5, $gt: 6}}, {a: [2, 10]}); - nomatch({a: {$ne: 5, $gt: 6}}, {a: [2, 4]}); - nomatch({a: {$ne: 5, $gt: 6}}, {a: [2, 5]}); - nomatch({a: {$ne: 5, $gt: 6}}, {a: [10, 5]}); - - // $in - match({a: {$in: [1, 2, 3]}}, {a: 2}); - nomatch({a: {$in: [1, 2, 3]}}, {a: 4}); - match({a: {$in: [[1], [2], [3]]}}, {a: [2]}); - nomatch({a: {$in: [[1], [2], [3]]}}, {a: [4]}); - match({a: {$in: [{b: 1}, {b: 2}, {b: 3}]}}, {a: {b: 2}}); - nomatch({a: {$in: [{b: 1}, {b: 2}, {b: 3}]}}, {a: {b: 4}}); - - match({a: {$in: [1, 2, 3]}}, {a: [2]}); // tested against mongodb - match({a: {$in: [{x: 1}, {x: 2}, {x: 3}]}}, {a: [{x: 2}]}); - match({a: {$in: [1, 2, 3]}}, {a: [4, 2]}); - nomatch({a: {$in: [1, 2, 3]}}, {a: [4]}); - - match({a: {$in: ['x', /foo/i]}}, {a: 'x'}); - match({a: {$in: ['x', /foo/i]}}, {a: 'fOo'}); - match({a: {$in: ['x', /foo/i]}}, {a: ['f', 'fOo']}); - nomatch({a: {$in: ['x', /foo/i]}}, {a: ['f', 'fOx']}); - - match({a: {$in: [1, null]}}, {}); - match({'a.b': {$in: [1, null]}}, {}); - match({'a.b': {$in: [1, null]}}, {a: {}}); - match({'a.b': {$in: [1, null]}}, {a: {b: null}}); - nomatch({'a.b': {$in: [1, null]}}, {a: {b: 5}}); - nomatch({'a.b': {$in: [1]}}, {a: {b: null}}); - nomatch({'a.b': {$in: [1]}}, {a: {}}); - nomatch({'a.b': {$in: [1, null]}}, {a: [{b: 5}]}); - match({'a.b': {$in: [1, null]}}, {a: [{b: 5}, {}]}); - nomatch({'a.b': {$in: [1, null]}}, {a: [{b: 5}, []]}); - nomatch({'a.b': {$in: [1, null]}}, {a: [{b: 5}, 5]}); - - // $nin - nomatch({a: {$nin: [1, 2, 3]}}, {a: 2}); - match({a: {$nin: [1, 2, 3]}}, {a: 4}); - nomatch({a: {$nin: [[1], [2], [3]]}}, {a: [2]}); - match({a: {$nin: [[1], [2], [3]]}}, {a: [4]}); - nomatch({a: {$nin: [{b: 1}, {b: 2}, {b: 3}]}}, {a: {b: 2}}); - match({a: {$nin: [{b: 1}, {b: 2}, {b: 3}]}}, {a: {b: 4}}); - - nomatch({a: {$nin: [1, 2, 3]}}, {a: [2]}); // tested against mongodb - nomatch({a: {$nin: [{x: 1}, {x: 2}, {x: 3}]}}, {a: [{x: 2}]}); - nomatch({a: {$nin: [1, 2, 3]}}, {a: [4, 2]}); - nomatch({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}, {b:2}]}); - match({a: {$nin: [1, 2, 3]}}, {a: [4]}); - match({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}]}); - - nomatch({a: {$nin: ['x', /foo/i]}}, {a: 'x'}); - nomatch({a: {$nin: ['x', /foo/i]}}, {a: 'fOo'}); - nomatch({a: {$nin: ['x', /foo/i]}}, {a: ['f', 'fOo']}); - match({a: {$nin: ['x', /foo/i]}}, {a: ['f', 'fOx']}); - - nomatch({a: {$nin: [1, null]}}, {}); - nomatch({'a.b': {$nin: [1, null]}}, {}); - nomatch({'a.b': {$nin: [1, null]}}, {a: {}}); - nomatch({'a.b': {$nin: [1, null]}}, {a: {b: null}}); - match({'a.b': {$nin: [1, null]}}, {a: {b: 5}}); - match({'a.b': {$nin: [1]}}, {a: {b: null}}); - match({'a.b': {$nin: [1]}}, {a: {}}); - match({'a.b': {$nin: [1, null]}}, {a: [{b: 5}]}); - nomatch({'a.b': {$nin: [1, null]}}, {a: [{b: 5}, {}]}); - match({'a.b': {$nin: [1, null]}}, {a: [{b: 5}, []]}); - match({'a.b': {$nin: [1, null]}}, {a: [{b: 5}, 5]}); - - // $size - match({a: {$size: 0}}, {a: []}); - match({a: {$size: 1}}, {a: [2]}); - match({a: {$size: 2}}, {a: [2, 2]}); - nomatch({a: {$size: 0}}, {a: [2]}); - nomatch({a: {$size: 1}}, {a: []}); - nomatch({a: {$size: 1}}, {a: [2, 2]}); - nomatch({a: {$size: 0}}, {a: "2"}); - nomatch({a: {$size: 1}}, {a: "2"}); - nomatch({a: {$size: 2}}, {a: "2"}); - - nomatch({a: {$size: 2}}, {a: [[2,2]]}); // tested against mongodb - - - // $bitsAllClear - number - match({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0}); - match({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b10000}); - nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b1}); - nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b10}); - nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b100}); - nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b1000}); - - // $bitsAllClear - buffer - match({a: {$bitsAllClear: new Uint8Array([3])}}, {a: new Uint8Array([4])}); - match({a: {$bitsAllClear: new Uint8Array([0, 1])}}, {a: new Uint8Array([255])}); // 256 should not be set for 255. - match({a: {$bitsAllClear: new Uint8Array([3])}}, {a: 4 }); - - match({a: {$bitsAllClear: new Uint8Array([3])}}, {a: 0 }); - - // $bitsAllSet - number - match({a: {$bitsAllSet: [0,1,2,3]}}, {a: 0b1111}); - nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 0b111}); - nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 256}); - nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 50000}); - match({a: {$bitsAllSet: [0,1,2]}}, {a: 15}); - match({a: {$bitsAllSet: [0, 12]}}, {a: 0b1000000000001}); - nomatch({a: {$bitsAllSet: [0, 12]}}, {a: 0b1000000000000}); - nomatch({a: {$bitsAllSet: [0, 12]}}, {a: 0b1}); - - // $bitsAllSet - buffer - match({a: {$bitsAllSet: new Uint8Array([3])}}, {a: new Uint8Array([3])}); - match({a: {$bitsAllSet: new Uint8Array([7])}}, {a: new Uint8Array([15])}); - match({a: {$bitsAllSet: new Uint8Array([3])}}, {a: 3 }); - - // $bitsAnySet - number - match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b1}); - match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b10}); - match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b100}); - match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b1000}); - match({a: {$bitsAnySet: [4]}}, {a: 0b10000}); - nomatch({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b10000}); - nomatch({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0}); - - // $bitsAnySet - buffer - match({a: {$bitsAnySet: new Uint8Array([3])}}, {a: new Uint8Array([7])}); - match({a: {$bitsAnySet: new Uint8Array([15])}}, {a: new Uint8Array([7])}); - match({a: {$bitsAnySet: new Uint8Array([3])}}, {a: 1 }); - - // $bitsAnyClear - number - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b10}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b100}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1000}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b10000}); - nomatch({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1111}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b111}); - nomatch({a: {$bitsAnyClear: [0,1,2]}}, {a: 0b111}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b11}); - nomatch({a: {$bitsAnyClear: [0,1]}}, {a: 0b11}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1}); - nomatch({a: {$bitsAnyClear: [0]}}, {a: 0b1}); - nomatch({a: {$bitsAnyClear: [4]}}, {a: 0b10000}); - - // $bitsAnyClear - buffer - match({a: {$bitsAnyClear: new Uint8Array([8])}}, {a: new Uint8Array([7])}); - match({a: {$bitsAnyClear: new Uint8Array([1])}}, {a: new Uint8Array([0])}); - match({a: {$bitsAnyClear: new Uint8Array([1])}}, {a: 4 }); - - // taken from: https://github.com/mongodb/mongo/blob/master/jstests/core/bittest.js - var c = new LocalCollection; - function matchCount(query, count) { - const matches = c.find(query).count() - if (matches !== count) { - test.fail({message: "minimongo match count failure: matched " + matches + " times, but should match " + count + " times", - query: JSON.stringify(query), - count: JSON.stringify(count) - }); - } - } - - // Tests on numbers. - - c.insert({a: 0}) - c.insert({a: 1}) - c.insert({a: 54}) - c.insert({a: 88}) - c.insert({a: 255}) - - // Tests with bitmask. - matchCount({a: {$bitsAllSet: 0}}, 5) - matchCount({a: {$bitsAllSet: 1}}, 2) - matchCount({a: {$bitsAllSet: 16}}, 3) - matchCount({a: {$bitsAllSet: 54}}, 2) - matchCount({a: {$bitsAllSet: 55}}, 1) - matchCount({a: {$bitsAllSet: 88}}, 2) - matchCount({a: {$bitsAllSet: 255}}, 1) - matchCount({a: {$bitsAllClear: 0}}, 5) - matchCount({a: {$bitsAllClear: 1}}, 3) - matchCount({a: {$bitsAllClear: 16}}, 2) - matchCount({a: {$bitsAllClear: 129}}, 3) - matchCount({a: {$bitsAllClear: 255}}, 1) - matchCount({a: {$bitsAnySet: 0}}, 0) - matchCount({a: {$bitsAnySet: 9}}, 3) - matchCount({a: {$bitsAnySet: 255}}, 4) - matchCount({a: {$bitsAnyClear: 0}}, 0) - matchCount({a: {$bitsAnyClear: 18}}, 3) - matchCount({a: {$bitsAnyClear: 24}}, 3) - matchCount({a: {$bitsAnyClear: 255}}, 4) - - // Tests with array of bit positions. - matchCount({a: {$bitsAllSet: []}}, 5) - matchCount({a: {$bitsAllSet: [0]}}, 2) - matchCount({a: {$bitsAllSet: [4]}}, 3) - matchCount({a: {$bitsAllSet: [1, 2, 4, 5]}}, 2) - matchCount({a: {$bitsAllSet: [0, 1, 2, 4, 5]}}, 1) - matchCount({a: {$bitsAllSet: [3, 4, 6]}}, 2) - matchCount({a: {$bitsAllSet: [0, 1, 2, 3, 4, 5, 6, 7]}}, 1) - matchCount({a: {$bitsAllClear: []}}, 5) - matchCount({a: {$bitsAllClear: [0]}}, 3) - matchCount({a: {$bitsAllClear: [4]}}, 2) - matchCount({a: {$bitsAllClear: [1, 7]}}, 3) - matchCount({a: {$bitsAllClear: [0, 1, 2, 3, 4, 5, 6, 7]}}, 1) - matchCount({a: {$bitsAnySet: []}}, 0) - matchCount({a: {$bitsAnySet: [1, 3]}}, 3) - matchCount({a: {$bitsAnySet: [0, 1, 2, 3, 4, 5, 6, 7]}}, 4) - matchCount({a: {$bitsAnyClear: []}}, 0) - matchCount({a: {$bitsAnyClear: [1, 4]}}, 3) - matchCount({a: {$bitsAnyClear: [3, 4]}}, 3) - matchCount({a: {$bitsAnyClear: [0, 1, 2, 3, 4, 5, 6, 7]}}, 4) - - // Tests with multiple predicates. - matchCount({a: {$bitsAllSet: 54, $bitsAllClear: 201}}, 1) - - // Tests on negative numbers - - c.remove({}) - c.insert({a: -0}) - c.insert({a: -1}) - c.insert({a: -54}) - - // Tests with bitmask. - matchCount({a: {$bitsAllSet: 0}}, 3) - matchCount({a: {$bitsAllSet: 2}}, 2) - matchCount({a: {$bitsAllSet: 127}}, 1) - matchCount({a: {$bitsAllSet: 74}}, 2) - matchCount({a: {$bitsAllClear: 0}}, 3) - matchCount({a: {$bitsAllClear: 53}}, 2) - matchCount({a: {$bitsAllClear: 127}}, 1) - matchCount({a: {$bitsAnySet: 0}}, 0) - matchCount({a: {$bitsAnySet: 2}}, 2) - matchCount({a: {$bitsAnySet: 127}}, 2) - matchCount({a: {$bitsAnyClear: 0}}, 0) - matchCount({a: {$bitsAnyClear: 53}}, 2) - matchCount({a: {$bitsAnyClear: 127}}, 2) - - // Tests with array of bit positions. - var allPositions = [] - for (var i = 0; i < 64; i++) { - allPositions.push(i) - } - - matchCount({a: {$bitsAllSet: []}}, 3) - matchCount({a: {$bitsAllSet: [1]}}, 2) - matchCount({a: {$bitsAllSet: allPositions}}, 1) - matchCount({a: {$bitsAllSet: [1, 7, 6, 3, 100]}}, 2) - matchCount({a: {$bitsAllClear: []}}, 3) - matchCount({a: {$bitsAllClear: [5, 4, 2, 0]}}, 2) - matchCount({a: {$bitsAllClear: allPositions}}, 1) - matchCount({a: {$bitsAnySet: []}}, 0) - matchCount({a: {$bitsAnySet: [1]}}, 2) - matchCount({a: {$bitsAnySet: allPositions}}, 2) - matchCount({a: {$bitsAnyClear: []}}, 0) - matchCount({a: {$bitsAnyClear: [0, 2, 4, 5, 100]}}, 2) - matchCount({a: {$bitsAnyClear: allPositions}}, 2) - - // Tests with multiple predicates. - matchCount({a: {$bitsAllSet: 74, $bitsAllClear: 53}}, 1) - - // Tests on BinData. - - c.remove({}) - c.insert({a: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}) - c.insert({a: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}) - c.insert({a: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}) - c.insert({a: EJSON.parse('{"$binary": "////////////////////////////"}')}) - - // Tests with binary string bitmask. - matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 4) - matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 3) - matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}}, 2) - matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 1) - matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 4) - matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "AAyfAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 3) - matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "JAyfqwetkqwklEWRbWERKKJREtbq"}')}}, 2) - matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 1) - matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 0) - matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "AAyfAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 1) - matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "JAyfqwetkqwklEWRbWERKKJREtbq"}')}}, 2) - matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 3) - matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 0) - matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 1) - matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}}, 2) - matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 3) - - // Tests with multiple predicates. - matchCount({ - a: { - $bitsAllSet: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}'), - $bitsAllClear: EJSON.parse('{"$binary": "//yf////////////////////////"}') - } - }, 1) - - c.remove({}) - - nomatch({a: {$bitsAllSet: 1}}, {a: false}) - nomatch({a: {$bitsAllSet: 1}}, {a: NaN}) - nomatch({a: {$bitsAllSet: 1}}, {a: Infinity}) - nomatch({a: {$bitsAllSet: 1}}, {a: null}) - nomatch({a: {$bitsAllSet: 1}}, {a: 'asdf'}) - nomatch({a: {$bitsAllSet: 1}}, {a: ['a', 'b']}) - nomatch({a: {$bitsAllSet: 1}}, {a: {foo: 'bar'}}) - nomatch({a: {$bitsAllSet: 1}}, {a: 1.2}) - nomatch({a: {$bitsAllSet: 1}}, {a: "1"}); - - [ - false, - NaN, - Infinity, - null, - 'asdf', - ['a', 'b'], - {foo: 'bar'}, - 1.2, - "1", - [0, -1] - ].forEach(function (badValue) { - test.throws(function () { - match({a: {$bitsAllSet: badValue}}, {a: 42}); - }); - }); - - // $type - match({a: {$type: 1}}, {a: 1.1}); - match({a: {$type: 1}}, {a: 1}); - nomatch({a: {$type: 1}}, {a: "1"}); - match({a: {$type: 2}}, {a: "1"}); - nomatch({a: {$type: 2}}, {a: 1}); - match({a: {$type: 3}}, {a: {}}); - match({a: {$type: 3}}, {a: {b: 2}}); - nomatch({a: {$type: 3}}, {a: []}); - nomatch({a: {$type: 3}}, {a: [1]}); - nomatch({a: {$type: 3}}, {a: null}); - match({a: {$type: 5}}, {a: EJSON.newBinary(0)}); - match({a: {$type: 5}}, {a: EJSON.newBinary(4)}); - nomatch({a: {$type: 5}}, {a: []}); - nomatch({a: {$type: 5}}, {a: [42]}); - match({a: {$type: 7}}, {a: new MongoID.ObjectID()}); - nomatch({a: {$type: 7}}, {a: "1234567890abcd1234567890"}); - match({a: {$type: 8}}, {a: true}); - match({a: {$type: 8}}, {a: false}); - nomatch({a: {$type: 8}}, {a: "true"}); - nomatch({a: {$type: 8}}, {a: 0}); - nomatch({a: {$type: 8}}, {a: null}); - nomatch({a: {$type: 8}}, {a: ''}); - nomatch({a: {$type: 8}}, {}); - match({a: {$type: 9}}, {a: (new Date)}); - nomatch({a: {$type: 9}}, {a: +(new Date)}); - match({a: {$type: 10}}, {a: null}); - nomatch({a: {$type: 10}}, {a: false}); - nomatch({a: {$type: 10}}, {a: ''}); - nomatch({a: {$type: 10}}, {a: 0}); - nomatch({a: {$type: 10}}, {}); - match({a: {$type: 11}}, {a: /x/}); - nomatch({a: {$type: 11}}, {a: 'x'}); - nomatch({a: {$type: 11}}, {}); - - // The normal rule for {$type:4} (4 means array) is that it NOT good enough to - // just have an array that's the leaf that matches the path. (An array inside - // that array is good, though.) - nomatch({a: {$type: 4}}, {a: []}); - nomatch({a: {$type: 4}}, {a: [1]}); // tested against mongodb - match({a: {$type: 1}}, {a: [1]}); - nomatch({a: {$type: 2}}, {a: [1]}); - match({a: {$type: 1}}, {a: ["1", 1]}); - match({a: {$type: 2}}, {a: ["1", 1]}); - nomatch({a: {$type: 3}}, {a: ["1", 1]}); - nomatch({a: {$type: 4}}, {a: ["1", 1]}); - nomatch({a: {$type: 1}}, {a: ["1", []]}); - match({a: {$type: 2}}, {a: ["1", []]}); - match({a: {$type: 4}}, {a: ["1", []]}); // tested against mongodb - // An exception to the normal rule is that an array found via numeric index is - // examined itself, and its elements are not. - match({'a.0': {$type: 4}}, {a: [[0]]}); - nomatch({'a.0': {$type: 1}}, {a: [[0]]}); - - // regular expressions - match({a: /a/}, {a: 'cat'}); - nomatch({a: /a/}, {a: 'cut'}); - nomatch({a: /a/}, {a: 'CAT'}); - match({a: /a/i}, {a: 'CAT'}); - match({a: /a/}, {a: ['foo', 'bar']}); // search within array... - nomatch({a: /,/}, {a: ['foo', 'bar']}); // but not by stringifying - match({a: {$regex: 'a'}}, {a: ['foo', 'bar']}); - nomatch({a: {$regex: ','}}, {a: ['foo', 'bar']}); - match({a: {$regex: /a/}}, {a: 'cat'}); - nomatch({a: {$regex: /a/}}, {a: 'cut'}); - nomatch({a: {$regex: /a/}}, {a: 'CAT'}); - match({a: {$regex: /a/i}}, {a: 'CAT'}); - match({a: {$regex: /a/, $options: 'i'}}, {a: 'CAT'}); // tested - match({a: {$regex: /a/i, $options: 'i'}}, {a: 'CAT'}); // tested - nomatch({a: {$regex: /a/i, $options: ''}}, {a: 'CAT'}); // tested - match({a: {$regex: 'a'}}, {a: 'cat'}); - nomatch({a: {$regex: 'a'}}, {a: 'cut'}); - nomatch({a: {$regex: 'a'}}, {a: 'CAT'}); - match({a: {$regex: 'a', $options: 'i'}}, {a: 'CAT'}); - match({a: {$regex: '', $options: 'i'}}, {a: 'foo'}); - nomatch({a: {$regex: '', $options: 'i'}}, {}); - nomatch({a: {$regex: '', $options: 'i'}}, {a: 5}); - nomatch({a: /undefined/}, {}); - nomatch({a: {$regex: 'undefined'}}, {}); - nomatch({a: /xxx/}, {}); - nomatch({a: {$regex: 'xxx'}}, {}); - - // GitHub issue #2817: - // Regexps with a global flag ('g') keep a state when tested against the same - // string. Selector shouldn't return different result for similar documents - // because of this state. - var reusedRegexp = /sh/ig; - match({a: reusedRegexp}, {a: 'Shorts'}); - match({a: reusedRegexp}, {a: 'Shorts'}); - match({a: reusedRegexp}, {a: 'Shorts'}); - - match({a: {$regex: reusedRegexp}}, {a: 'Shorts'}); - match({a: {$regex: reusedRegexp}}, {a: 'Shorts'}); - match({a: {$regex: reusedRegexp}}, {a: 'Shorts'}); - - test.throws(function () { - match({a: {$options: 'i'}}, {a: 12}); - }); - - match({a: /a/}, {a: ['dog', 'cat']}); - nomatch({a: /a/}, {a: ['dog', 'puppy']}); - - // we don't support regexps in minimongo very well (eg, there's no EJSON - // encoding so it won't go over the wire), but run these tests anyway - match({a: /a/}, {a: /a/}); - match({a: /a/}, {a: ['x', /a/]}); - nomatch({a: /a/}, {a: /a/i}); - nomatch({a: /a/m}, {a: /a/}); - nomatch({a: /a/}, {a: /b/}); - nomatch({a: /5/}, {a: 5}); - nomatch({a: /t/}, {a: true}); - match({a: /m/i}, {a: ['x', 'xM']}); - - test.throws(function () { - match({a: {$regex: /a/, $options: 'x'}}, {a: 'cat'}); - }); - test.throws(function () { - match({a: {$regex: /a/, $options: 's'}}, {a: 'cat'}); - }); - - // $not - match({x: {$not: {$gt: 7}}}, {x: 6}); - nomatch({x: {$not: {$gt: 7}}}, {x: 8}); - match({x: {$not: {$lt: 10, $gt: 7}}}, {x: 11}); - nomatch({x: {$not: {$lt: 10, $gt: 7}}}, {x: 9}); - match({x: {$not: {$lt: 10, $gt: 7}}}, {x: 6}); - - match({x: {$not: {$gt: 7}}}, {x: [2, 3, 4]}); - match({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}]}); - nomatch({x: {$not: {$gt: 7}}}, {x: [2, 3, 4, 10]}); - nomatch({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}, {y:10}]}); - - match({x: {$not: /a/}}, {x: "dog"}); - nomatch({x: {$not: /a/}}, {x: "cat"}); - match({x: {$not: /a/}}, {x: ["dog", "puppy"]}); - nomatch({x: {$not: /a/}}, {x: ["kitten", "cat"]}); - - // dotted keypaths: bare values - match({"a.b": 1}, {a: {b: 1}}); - nomatch({"a.b": 1}, {a: {b: 2}}); - match({"a.b": [1,2,3]}, {a: {b: [1,2,3]}}); - nomatch({"a.b": [1,2,3]}, {a: {b: [4]}}); - match({"a.b": /a/}, {a: {b: "cat"}}); - nomatch({"a.b": /a/}, {a: {b: "dog"}}); - match({"a.b.c": null}, {}); - match({"a.b.c": null}, {a: 1}); - match({"a.b": null}, {a: 1}); - match({"a.b.c": null}, {a: {b: 4}}); - - // dotted keypaths, nulls, numeric indices, arrays - nomatch({"a.b": null}, {a: [1]}); - match({"a.b": []}, {a: {b: []}}); - var big = {a: [{b: 1}, 2, {}, {b: [3, 4]}]}; - match({"a.b": 1}, big); - match({"a.b": [3, 4]}, big); - match({"a.b": 3}, big); - match({"a.b": 4}, big); - match({"a.b": null}, big); // matches on slot 2 - match({'a.1': 8}, {a: [7, 8, 9]}); - nomatch({'a.1': 7}, {a: [7, 8, 9]}); - nomatch({'a.1': null}, {a: [7, 8, 9]}); - match({'a.1': [8, 9]}, {a: [7, [8, 9]]}); - nomatch({'a.1': 6}, {a: [[6, 7], [8, 9]]}); - nomatch({'a.1': 7}, {a: [[6, 7], [8, 9]]}); - nomatch({'a.1': 8}, {a: [[6, 7], [8, 9]]}); - nomatch({'a.1': 9}, {a: [[6, 7], [8, 9]]}); - match({"a.1": 2}, {a: [0, {1: 2}, 3]}); - match({"a.1": {1: 2}}, {a: [0, {1: 2}, 3]}); - match({"x.1.y": 8}, {x: [7, {y: 8}, 9]}); - // comes from trying '1' as key in the plain object - match({"x.1.y": null}, {x: [7, {y: 8}, 9]}); - match({"a.1.b": 9}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); - match({"a.1.b": 'foo'}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); - match({"a.1.b": null}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); - match({"a.1.b": 2}, {a: [1, [{b: 2}], 3]}); - nomatch({"a.1.b": null}, {a: [1, [{b: 2}], 3]}); - // this is new behavior in mongo 2.5 - nomatch({"a.0.b": null}, {a: [5]}); - match({"a.1": 4}, {a: [{1: 4}, 5]}); - match({"a.1": 5}, {a: [{1: 4}, 5]}); - nomatch({"a.1": null}, {a: [{1: 4}, 5]}); - match({"a.1.foo": 4}, {a: [{1: {foo: 4}}, {foo: 5}]}); - match({"a.1.foo": 5}, {a: [{1: {foo: 4}}, {foo: 5}]}); - match({"a.1.foo": null}, {a: [{1: {foo: 4}}, {foo: 5}]}); - - // trying to access a dotted field that is undefined at some point - // down the chain - nomatch({"a.b": 1}, {x: 2}); - nomatch({"a.b.c": 1}, {a: {x: 2}}); - nomatch({"a.b.c": 1}, {a: {b: {x: 2}}}); - nomatch({"a.b.c": 1}, {a: {b: 1}}); - nomatch({"a.b.c": 1}, {a: {b: 0}}); - - // dotted keypaths: literal objects - match({"a.b": {c: 1}}, {a: {b: {c: 1}}}); - nomatch({"a.b": {c: 1}}, {a: {b: {c: 2}}}); - nomatch({"a.b": {c: 1}}, {a: {b: 2}}); - match({"a.b": {c: 1, d: 2}}, {a: {b: {c: 1, d: 2}}}); - nomatch({"a.b": {c: 1, d: 2}}, {a: {b: {c: 1, d: 1}}}); - nomatch({"a.b": {c: 1, d: 2}}, {a: {b: {d: 2}}}); - - // dotted keypaths: $ operators - match({"a.b": {$in: [1, 2, 3]}}, {a: {b: [2]}}); // tested against mongodb - match({"a.b": {$in: [{x: 1}, {x: 2}, {x: 3}]}}, {a: {b: [{x: 2}]}}); - match({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4, 2]}}); - nomatch({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4]}}); - - // $or - test.throws(function () { - match({$or: []}, {}); - }); - test.throws(function () { - match({$or: [5]}, {}); - }); - test.throws(function () { - match({$or: []}, {a: 1}); - }); - match({$or: [{a: 1}]}, {a: 1}); - nomatch({$or: [{b: 2}]}, {a: 1}); - match({$or: [{a: 1}, {b: 2}]}, {a: 1}); - nomatch({$or: [{c: 3}, {d: 4}]}, {a: 1}); - match({$or: [{a: 1}, {b: 2}]}, {a: [1, 2, 3]}); - nomatch({$or: [{a: 1}, {b: 2}]}, {c: [1, 2, 3]}); - nomatch({$or: [{a: 1}, {b: 2}]}, {a: [2, 3, 4]}); - match({$or: [{a: 1}, {a: 2}]}, {a: 1}); - match({$or: [{a: 1}, {a: 2}], b: 2}, {a: 1, b: 2}); - nomatch({$or: [{a: 2}, {a: 3}], b: 2}, {a: 1, b: 2}); - nomatch({$or: [{a: 1}, {a: 2}], b: 3}, {a: 1, b: 2}); - - // Combining $or with equality - match({x: 1, $or: [{a: 1}, {b: 1}]}, {x: 1, b: 1}); - match({$or: [{a: 1}, {b: 1}], x: 1}, {x: 1, b: 1}); - nomatch({x: 1, $or: [{a: 1}, {b: 1}]}, {b: 1}); - nomatch({x: 1, $or: [{a: 1}, {b: 1}]}, {x: 1}); - - // $or and $lt, $lte, $gt, $gte - match({$or: [{a: {$lte: 1}}, {a: 2}]}, {a: 1}); - nomatch({$or: [{a: {$lt: 1}}, {a: 2}]}, {a: 1}); - match({$or: [{a: {$gte: 1}}, {a: 2}]}, {a: 1}); - nomatch({$or: [{a: {$gt: 1}}, {a: 2}]}, {a: 1}); - match({$or: [{b: {$gt: 1}}, {b: {$lt: 3}}]}, {b: 2}); - nomatch({$or: [{b: {$lt: 1}}, {b: {$gt: 3}}]}, {b: 2}); - - // $or and $in - match({$or: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); - nomatch({$or: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); - match({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {a: 1}); - match({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {b: 2}); - nomatch({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {c: 3}); - match({$or: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {b: 2}); - nomatch({$or: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {b: 2}); - - // $or and $nin - nomatch({$or: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); - match({$or: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); - nomatch({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {a: 1}); - match({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {b: 2}); - match({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {c: 3}); - match({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {b: 2}); - nomatch({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 2}); - match({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {b: 2}); - - // $or and dot-notation - match({$or: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); - match({$or: [{"a.b": 1}, {"a.c": 1}]}, {a: {b: 1}}); - nomatch({$or: [{"a.b": 2}, {"a.c": 1}]}, {a: {b: 1}}); - - // $or and nested objects - match({$or: [{a: {b: 1, c: 2}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); - nomatch({$or: [{a: {b: 1, c: 3}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); - - // $or and regexes - match({$or: [{a: /a/}]}, {a: "cat"}); - nomatch({$or: [{a: /o/}]}, {a: "cat"}); - match({$or: [{a: /a/}, {a: /o/}]}, {a: "cat"}); - nomatch({$or: [{a: /i/}, {a: /o/}]}, {a: "cat"}); - match({$or: [{a: /i/}, {b: /o/}]}, {a: "cat", b: "dog"}); - - // $or and $ne - match({$or: [{a: {$ne: 1}}]}, {}); - nomatch({$or: [{a: {$ne: 1}}]}, {a: 1}); - match({$or: [{a: {$ne: 1}}]}, {a: 2}); - match({$or: [{a: {$ne: 1}}]}, {b: 1}); - match({$or: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 1}); - match({$or: [{a: {$ne: 1}}, {b: {$ne: 1}}]}, {a: 1}); - nomatch({$or: [{a: {$ne: 1}}, {b: {$ne: 2}}]}, {a: 1, b: 2}); - - // $or and $not - match({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {}); - nomatch({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 1}); - match({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 2}); - match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$not: {$mod: [10, 2]}}}]}, {a: 1}); - nomatch({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 1}); - match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 2}); - match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 3}); - // this is possibly an open-ended task, so we stop here ... - - // $nor - test.throws(function () { - match({$nor: []}, {}); - }); - test.throws(function () { - match({$nor: [5]}, {}); - }); - test.throws(function () { - match({$nor: []}, {a: 1}); - }); - nomatch({$nor: [{a: 1}]}, {a: 1}); - match({$nor: [{b: 2}]}, {a: 1}); - nomatch({$nor: [{a: 1}, {b: 2}]}, {a: 1}); - match({$nor: [{c: 3}, {d: 4}]}, {a: 1}); - nomatch({$nor: [{a: 1}, {b: 2}]}, {a: [1, 2, 3]}); - match({$nor: [{a: 1}, {b: 2}]}, {c: [1, 2, 3]}); - match({$nor: [{a: 1}, {b: 2}]}, {a: [2, 3, 4]}); - nomatch({$nor: [{a: 1}, {a: 2}]}, {a: 1}); - - // $nor and $lt, $lte, $gt, $gte - nomatch({$nor: [{a: {$lte: 1}}, {a: 2}]}, {a: 1}); - match({$nor: [{a: {$lt: 1}}, {a: 2}]}, {a: 1}); - nomatch({$nor: [{a: {$gte: 1}}, {a: 2}]}, {a: 1}); - match({$nor: [{a: {$gt: 1}}, {a: 2}]}, {a: 1}); - nomatch({$nor: [{b: {$gt: 1}}, {b: {$lt: 3}}]}, {b: 2}); - match({$nor: [{b: {$lt: 1}}, {b: {$gt: 3}}]}, {b: 2}); - - // $nor and $in - nomatch({$nor: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); - match({$nor: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); - nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {a: 1}); - nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {b: 2}); - match({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {c: 3}); - nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {b: 2}); - match({$nor: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {b: 2}); - - // $nor and $nin - match({$nor: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); - nomatch({$nor: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); - match({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {a: 1}); - nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {b: 2}); - nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {c: 3}); - nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {b: 2}); - match({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 2}); - nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {b: 2}); - - // $nor and dot-notation - nomatch({$nor: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); - nomatch({$nor: [{"a.b": 1}, {"a.c": 1}]}, {a: {b: 1}}); - match({$nor: [{"a.b": 2}, {"a.c": 1}]}, {a: {b: 1}}); - - // $nor and nested objects - nomatch({$nor: [{a: {b: 1, c: 2}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); - match({$nor: [{a: {b: 1, c: 3}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); - - // $nor and regexes - nomatch({$nor: [{a: /a/}]}, {a: "cat"}); - match({$nor: [{a: /o/}]}, {a: "cat"}); - nomatch({$nor: [{a: /a/}, {a: /o/}]}, {a: "cat"}); - match({$nor: [{a: /i/}, {a: /o/}]}, {a: "cat"}); - nomatch({$nor: [{a: /i/}, {b: /o/}]}, {a: "cat", b: "dog"}); - - // $nor and $ne - nomatch({$nor: [{a: {$ne: 1}}]}, {}); - match({$nor: [{a: {$ne: 1}}]}, {a: 1}); - nomatch({$nor: [{a: {$ne: 1}}]}, {a: 2}); - nomatch({$nor: [{a: {$ne: 1}}]}, {b: 1}); - nomatch({$nor: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 1}); - nomatch({$nor: [{a: {$ne: 1}}, {b: {$ne: 1}}]}, {a: 1}); - match({$nor: [{a: {$ne: 1}}, {b: {$ne: 2}}]}, {a: 1, b: 2}); - - // $nor and $not - nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {}); - match({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 1}); - nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 2}); - nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$not: {$mod: [10, 2]}}}]}, {a: 1}); - match({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 1}); - nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 2}); - nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 3}); - - // $and - - test.throws(function () { - match({$and: []}, {}); - }); - test.throws(function () { - match({$and: [5]}, {}); - }); - test.throws(function () { - match({$and: []}, {a: 1}); - }); - match({$and: [{a: 1}]}, {a: 1}); - nomatch({$and: [{a: 1}, {a: 2}]}, {a: 1}); - nomatch({$and: [{a: 1}, {b: 1}]}, {a: 1}); - match({$and: [{a: 1}, {b: 2}]}, {a: 1, b: 2}); - nomatch({$and: [{a: 1}, {b: 1}]}, {a: 1, b: 2}); - match({$and: [{a: 1}, {b: 2}], c: 3}, {a: 1, b: 2, c: 3}); - nomatch({$and: [{a: 1}, {b: 2}], c: 4}, {a: 1, b: 2, c: 3}); - - // $and and regexes - match({$and: [{a: /a/}]}, {a: "cat"}); - match({$and: [{a: /a/i}]}, {a: "CAT"}); - nomatch({$and: [{a: /o/}]}, {a: "cat"}); - nomatch({$and: [{a: /a/}, {a: /o/}]}, {a: "cat"}); - match({$and: [{a: /a/}, {b: /o/}]}, {a: "cat", b: "dog"}); - nomatch({$and: [{a: /a/}, {b: /a/}]}, {a: "cat", b: "dog"}); - - // $and, dot-notation, and nested objects - match({$and: [{"a.b": 1}]}, {a: {b: 1}}); - match({$and: [{a: {b: 1}}]}, {a: {b: 1}}); - nomatch({$and: [{"a.b": 2}]}, {a: {b: 1}}); - nomatch({$and: [{"a.c": 1}]}, {a: {b: 1}}); - nomatch({$and: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); - nomatch({$and: [{"a.b": 1}, {a: {b: 2}}]}, {a: {b: 1}}); - match({$and: [{"a.b": 1}, {"c.d": 2}]}, {a: {b: 1}, c: {d: 2}}); - nomatch({$and: [{"a.b": 1}, {"c.d": 1}]}, {a: {b: 1}, c: {d: 2}}); - match({$and: [{"a.b": 1}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); - nomatch({$and: [{"a.b": 1}, {c: {d: 1}}]}, {a: {b: 1}, c: {d: 2}}); - nomatch({$and: [{"a.b": 2}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); - match({$and: [{a: {b: 1}}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); - nomatch({$and: [{a: {b: 2}}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); - - // $and and $in - nomatch({$and: [{a: {$in: []}}]}, {}); - match({$and: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); - nomatch({$and: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); - nomatch({$and: [{a: {$in: [1, 2, 3]}}, {a: {$in: [4, 5, 6]}}]}, {a: 1}); - nomatch({$and: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {a: 1, b: 4}); - match({$and: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {a: 1, b: 4}); - - - // $and and $nin - match({$and: [{a: {$nin: []}}]}, {}); - nomatch({$and: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); - match({$and: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); - nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {a: {$nin: [4, 5, 6]}}]}, {a: 1}); - nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 4}); - nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {a: 1, b: 4}); - - // $and and $lt, $lte, $gt, $gte - match({$and: [{a: {$lt: 2}}]}, {a: 1}); - nomatch({$and: [{a: {$lt: 1}}]}, {a: 1}); - match({$and: [{a: {$lte: 1}}]}, {a: 1}); - match({$and: [{a: {$gt: 0}}]}, {a: 1}); - nomatch({$and: [{a: {$gt: 1}}]}, {a: 1}); - match({$and: [{a: {$gte: 1}}]}, {a: 1}); - match({$and: [{a: {$gt: 0}}, {a: {$lt: 2}}]}, {a: 1}); - nomatch({$and: [{a: {$gt: 1}}, {a: {$lt: 2}}]}, {a: 1}); - nomatch({$and: [{a: {$gt: 0}}, {a: {$lt: 1}}]}, {a: 1}); - match({$and: [{a: {$gte: 1}}, {a: {$lte: 1}}]}, {a: 1}); - nomatch({$and: [{a: {$gte: 2}}, {a: {$lte: 0}}]}, {a: 1}); - - // $and and $ne - match({$and: [{a: {$ne: 1}}]}, {}); - nomatch({$and: [{a: {$ne: 1}}]}, {a: 1}); - match({$and: [{a: {$ne: 1}}]}, {a: 2}); - nomatch({$and: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 2}); - match({$and: [{a: {$ne: 1}}, {a: {$ne: 3}}]}, {a: 2}); - - // $and and $not - match({$and: [{a: {$not: {$gt: 2}}}]}, {a: 1}); - nomatch({$and: [{a: {$not: {$lt: 2}}}]}, {a: 1}); - match({$and: [{a: {$not: {$lt: 0}}}, {a: {$not: {$gt: 2}}}]}, {a: 1}); - nomatch({$and: [{a: {$not: {$lt: 2}}}, {a: {$not: {$gt: 0}}}]}, {a: 1}); - - // $where - match({$where: "this.a === 1"}, {a: 1}); - match({$where: "obj.a === 1"}, {a: 1}); - nomatch({$where: "this.a !== 1"}, {a: 1}); - nomatch({$where: "obj.a !== 1"}, {a: 1}); - nomatch({$where: "this.a === 1", a: 2}, {a: 1}); - match({$where: "this.a === 1", b: 2}, {a: 1, b: 2}); - match({$where: "this.a === 1 && this.b === 2"}, {a: 1, b: 2}); - match({$where: "this.a instanceof Array"}, {a: []}); - nomatch({$where: "this.a instanceof Array"}, {a: 1}); - - // reaching into array - match({"dogs.0.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({"dogs.1.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - nomatch({"dogs.1.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({"room.1b": "bla"}, {room: {"1b": "bla"}}); - - match({"dogs.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({"dogs.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({"animals.dogs.name": "Fido"}, - {animals: [{dogs: [{name: "Rover"}]}, - {}, - {dogs: [{name: "Fido"}, {name: "Rex"}]}]}); - match({"animals.dogs.name": "Fido"}, - {animals: [{dogs: {name: "Rex"}}, - {dogs: {name: "Fido"}}]}); - match({"animals.dogs.name": "Fido"}, - {animals: [{dogs: [{name: "Rover"}]}, - {}, - {dogs: [{name: ["Fido"]}, {name: "Rex"}]}]}); - nomatch({"dogs.name": "Fido"}, {dogs: []}); - - // $elemMatch - match({dogs: {$elemMatch: {name: /e/}}}, - {dogs: [{name: "Fido"}, {name: "Rex"}]}); - nomatch({dogs: {$elemMatch: {name: /a/}}}, - {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({dogs: {$elemMatch: {age: {$gt: 4}}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - match({dogs: {$elemMatch: {name: "Fido", age: {$gt: 4}}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - nomatch({dogs: {$elemMatch: {name: "Fido", age: {$gt: 5}}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - match({dogs: {$elemMatch: {name: /i/, age: {$gt: 4}}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - nomatch({dogs: {$elemMatch: {name: /e/, age: 5}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - match({x: {$elemMatch: {y: 9}}}, {x: [{y: 9}]}); - nomatch({x: {$elemMatch: {y: 9}}}, {x: [[{y: 9}]]}); - match({x: {$elemMatch: {$gt: 5, $lt: 9}}}, {x: [8]}); - nomatch({x: {$elemMatch: {$gt: 5, $lt: 9}}}, {x: [[8]]}); - match({'a.x': {$elemMatch: {y: 9}}}, - {a: [{x: []}, {x: [{y: 9}]}]}); - nomatch({a: {$elemMatch: {x: 5}}}, {a: {x: 5}}); - match({a: {$elemMatch: {0: {$gt: 5, $lt: 9}}}}, {a: [[6]]}); - match({a: {$elemMatch: {'0.b': {$gt: 5, $lt: 9}}}}, {a: [[{b:6}]]}); - match({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1, b: 1}]}); - match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}], x: 1}}}, - {a: [{x: 1, b: 1}]}); - match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1, b: 1}]}); - match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1, b: 1}]}); - match({a: {$elemMatch: {$and: [{b: 1}, {x: 1}]}}}, - {a: [{x: 1, b: 1}]}); - nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{b: 1}]}); - nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1}]}); - nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1}, {b: 1}]}); - - test.throws(function () { - match({a: {$elemMatch: {$gte: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1, b: 1}]}); - }); - - test.throws(function () { - match({x: {$elemMatch: {$and: [{$gt: 5, $lt: 9}]}}}, {x: [8]}); - }); - - // $comment - match({a: 5, $comment: "asdf"}, {a: 5}); - nomatch({a: 6, $comment: "asdf"}, {a: 5}); - - // XXX still needs tests: - // - non-scalar arguments to $gt, $lt, etc -}); - -Tinytest.add("minimongo - projection_compiler", function (test) { - var testProjection = function (projection, tests) { - var projection_f = LocalCollection._compileProjection(projection); - var equalNonStrict = function (a, b, desc) { - test.isTrue(EJSON.equals(a, b), desc); - }; - - tests.forEach(function (testCase) { - equalNonStrict(projection_f(testCase[0]), testCase[1], testCase[2]); - }); - }; - - var testCompileProjectionThrows = function (projection, expectedError) { - test.throws(function () { - LocalCollection._compileProjection(projection); - }, expectedError); - }; - - testProjection({ 'foo': 1, 'bar': 1 }, [ - [{ foo: 42, bar: "something", baz: "else" }, - { foo: 42, bar: "something" }, - "simplest - whitelist"], - - [{ foo: { nested: 17 }, baz: {} }, - { foo: { nested: 17 } }, - "nested whitelisted field"], - - [{ _id: "uid", bazbaz: 42 }, - { _id: "uid" }, - "simplest whitelist - preserve _id"] - ]); - - testProjection({ 'foo': 0, 'bar': 0 }, [ - [{ foo: 42, bar: "something", baz: "else" }, - { baz: "else" }, - "simplest - blacklist"], - - [{ foo: { nested: 17 }, baz: { foo: "something" } }, - { baz: { foo: "something" } }, - "nested blacklisted field"], - - [{ _id: "uid", bazbaz: 42 }, - { _id: "uid", bazbaz: 42 }, - "simplest blacklist - preserve _id"] - ]); - - testProjection({ _id: 0, foo: 1 }, [ - [{ foo: 42, bar: 33, _id: "uid" }, - { foo: 42 }, - "whitelist - _id blacklisted"] - ]); - - testProjection({ _id: 0, foo: 0 }, [ - [{ foo: 42, bar: 33, _id: "uid" }, - { bar: 33 }, - "blacklist - _id blacklisted"] - ]); - - testProjection({ 'foo.bar.baz': 1 }, [ - [{ foo: { meh: "fur", bar: { baz: 42 }, tr: 1 }, bar: 33, baz: 'trolololo' }, - { foo: { bar: { baz: 42 } } }, - "whitelist nested"], - - // Behavior of this test is looked up in actual mongo - [{ foo: { meh: "fur", bar: "nope", tr: 1 }, bar: 33, baz: 'trolololo' }, - { foo: {} }, - "whitelist nested - path not found in doc, different type"], - - // Behavior of this test is looked up in actual mongo - [{ foo: { meh: "fur", bar: [], tr: 1 }, bar: 33, baz: 'trolololo' }, - { foo: { bar: [] } }, - "whitelist nested - path not found in doc"] - ]); - - testProjection({ 'hope.humanity': 0, 'hope.people': 0 }, [ - [{ hope: { humanity: "lost", people: 'broken', candies: 'long live!' } }, - { hope: { candies: 'long live!' } }, - "blacklist nested"], - - [{ hope: "new" }, - { hope: "new" }, - "blacklist nested - path not found in doc"] - ]); - - testProjection({ _id: 1 }, [ - [{ _id: 42, x: 1, y: { z: "2" } }, - { _id: 42 }, - "_id whitelisted"], - [{ _id: 33 }, - { _id: 33 }, - "_id whitelisted, _id only"], - [{ x: 1 }, - {}, - "_id whitelisted, no _id"] - ]); - - testProjection({ _id: 0 }, [ - [{ _id: 42, x: 1, y: { z: "2" } }, - { x: 1, y: { z: "2" } }, - "_id blacklisted"], - [{ _id: 33 }, - {}, - "_id blacklisted, _id only"], - [{ x: 1 }, - { x: 1 }, - "_id blacklisted, no _id"] - ]); - - testProjection({}, [ - [{ a: 1, b: 2, c: "3" }, - { a: 1, b: 2, c: "3" }, - "empty projection"] - ]); - - testCompileProjectionThrows( - { 'inc': 1, 'excl': 0 }, - "You cannot currently mix including and excluding fields"); - testCompileProjectionThrows( - { _id: 1, a: 0 }, - "You cannot currently mix including and excluding fields"); - - testCompileProjectionThrows( - { 'a': 1, 'a.b': 1 }, - "using both of them may trigger unexpected behavior"); - testCompileProjectionThrows( - { 'a.b.c': 1, 'a.b': 1, 'a': 1 }, - "using both of them may trigger unexpected behavior"); - - testCompileProjectionThrows("some string", "fields option must be an object"); -}); - -Tinytest.add("minimongo - fetch with fields", function (test) { - var c = new LocalCollection(); - Array.from({length: 30}, function (x, i) { - c.insert({ - something: Random.id(), - anything: { - foo: "bar", - cool: "hot" - }, - nothing: i, - i: i - }); - }); - - // Test just a regular fetch with some projection - var fetchResults = c.find({}, { fields: { - 'something': 1, - 'anything.foo': 1 - } }).fetch(); - - test.isTrue(fetchResults.every(function (x) { - return x && - x.something && - x.anything && - x.anything.foo && - x.anything.foo === "bar" && - !x.hasOwnProperty('nothing') && - !x.anything.hasOwnProperty('cool'); - })); - - // Test with a selector, even field used in the selector is excluded in the - // projection - fetchResults = c.find({ - nothing: { $gte: 5 } - }, { - fields: { nothing: 0 } - }).fetch(); - - test.isTrue(fetchResults.every(function (x) { - return x && - x.something && - x.anything && - x.anything.foo === "bar" && - x.anything.cool === "hot" && - !x.hasOwnProperty('nothing') && - x.i && - x.i >= 5; - })); - - test.isTrue(fetchResults.length === 25); - - // Test that we can sort, based on field excluded from the projection, use - // skip and limit as well! - // following find will get indexes [10..20) sorted by nothing - fetchResults = c.find({}, { - sort: { - nothing: 1 - }, - limit: 10, - skip: 10, - fields: { - i: 1, - something: 1 - } - }).fetch(); - - test.isTrue(fetchResults.every(function (x) { - return x && - x.something && - x.i >= 10 && x.i < 20; - })); - - fetchResults.forEach(function (x, i, arr) { - if (!i) return; - test.isTrue(x.i === arr[i-1].i + 1); - }); - - // Temporary unsupported operators - // queries are taken from MongoDB docs examples - test.throws(function () { - c.find({}, { fields: { 'grades.$': 1 } }); - }); - test.throws(function () { - c.find({}, { fields: { grades: { $elemMatch: { mean: 70 } } } }); - }); - test.throws(function () { - c.find({}, { fields: { grades: { $slice: [20, 10] } } }); - }); -}); - -Tinytest.add("minimongo - fetch with projection, subarrays", function (test) { - // Apparently projection of type 'foo.bar.x' for - // { foo: [ { bar: { x: 42 } }, { bar: { x: 3 } } ] } - // should return exactly this object. More precisely, arrays are considered as - // sets and are queried separately and then merged back to result set - var c = new LocalCollection(); - - // Insert a test object with two set fields - c.insert({ - setA: [{ - fieldA: 42, - fieldB: 33 - }, { - fieldA: "the good", - fieldB: "the bad", - fieldC: "the ugly" - }], - setB: [{ - anotherA: { }, - anotherB: "meh" - }, { - anotherA: 1234, - anotherB: 431 - }] - }); - - var equalNonStrict = function (a, b, desc) { - test.isTrue(EJSON.equals(a, b), desc); - }; - - var testForProjection = function (projection, expected) { - var fetched = c.find({}, { fields: projection }).fetch()[0]; - equalNonStrict(fetched, expected, "failed sub-set projection: " + - JSON.stringify(projection)); - }; - - testForProjection({ 'setA.fieldA': 1, 'setB.anotherB': 1, _id: 0 }, - { - setA: [{ fieldA: 42 }, { fieldA: "the good" }], - setB: [{ anotherB: "meh" }, { anotherB: 431 }] - }); - - testForProjection({ 'setA.fieldA': 0, 'setB.anotherA': 0, _id: 0 }, - { - setA: [{fieldB:33}, {fieldB:"the bad",fieldC:"the ugly"}], - setB: [{ anotherB: "meh" }, { anotherB: 431 }] - }); - - c.remove({}); - c.insert({a:[[{b:1,c:2},{b:2,c:4}],{b:3,c:5},[{b:4, c:9}]]}); - - testForProjection({ 'a.b': 1, _id: 0 }, - {a: [ [ { b: 1 }, { b: 2 } ], { b: 3 }, [ { b: 4 } ] ] }); - testForProjection({ 'a.b': 0, _id: 0 }, - {a: [ [ { c: 2 }, { c: 4 } ], { c: 5 }, [ { c: 9 } ] ] }); -}); - -Tinytest.add("minimongo - fetch with projection, deep copy", function (test) { - // Compiled fields projection defines the contract: returned document doesn't - // retain anything from the passed argument. - var doc = { - a: { x: 42 }, - b: { - y: { z: 33 } - }, - c: "asdf" - }; - - var fields = { - 'a': 1, - 'b.y': 1 - }; - - var projectionFn = LocalCollection._compileProjection(fields); - var filteredDoc = projectionFn(doc); - doc.a.x++; - doc.b.y.z--; - test.equal(filteredDoc.a.x, 42, "projection returning deep copy - including"); - test.equal(filteredDoc.b.y.z, 33, "projection returning deep copy - including"); - - fields = { c: 0 }; - projectionFn = LocalCollection._compileProjection(fields); - filteredDoc = projectionFn(doc); - - doc.a.x = 5; - test.equal(filteredDoc.a.x, 43, "projection returning deep copy - excluding"); -}); - -Tinytest.add("minimongo - observe ordered with projection", function (test) { - // These tests are copy-paste from "minimongo -observe ordered", - // slightly modified to test projection - var operations = []; - var cbs = log_callbacks(operations); - var handle; - - var c = new LocalCollection(); - handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(cbs); - test.isTrue(handle.collection === c); - - c.insert({_id: 'foo', a:1, b:2}); - test.equal(operations.shift(), ['added', {a:1}, 0, null]); - c.update({a:1}, {$set: {a: 2, b: 1}}); - test.equal(operations.shift(), ['changed', {a:2}, 0, {a:1}]); - c.insert({_id: 'bar', a:10, c: 33}); - test.equal(operations.shift(), ['added', {a:10}, 1, null]); - c.update({}, {$inc: {a: 1}}, {multi: true}); - c.update({}, {$inc: {c: 1}}, {multi: true}); - test.equal(operations.shift(), ['changed', {a:3}, 0, {a:2}]); - test.equal(operations.shift(), ['changed', {a:11}, 1, {a:10}]); - c.update({a:11}, {a:1, b:44}); - test.equal(operations.shift(), ['changed', {a:1}, 1, {a:11}]); - test.equal(operations.shift(), ['moved', {a:1}, 1, 0, 'foo']); - c.remove({a:2}); - test.equal(operations.shift(), undefined); - c.remove({a:3}); - test.equal(operations.shift(), ['removed', 'foo', 1, {a:3}]); - - // test stop - handle.stop(); - var idA2 = Random.id(); - c.insert({_id: idA2, a:2}); - test.equal(operations.shift(), undefined); - - var cursor = c.find({}, {fields: {a: 1, _id: 0}}); - test.throws(function () { - cursor.observeChanges({added: function () {}}); - }); - test.throws(function () { - cursor.observe({added: function () {}}); - }); - - // test initial inserts (and backwards sort) - handle = c.find({}, {sort: {a: -1}, fields: { a: 1 } }).observe(cbs); - test.equal(operations.shift(), ['added', {a:2}, 0, null]); - test.equal(operations.shift(), ['added', {a:1}, 1, null]); - handle.stop(); - - // test _suppress_initial - handle = c.find({}, {sort: {a: -1}, fields: { a: 1 }}).observe(Object.assign(cbs, {_suppress_initial: true})); - test.equal(operations.shift(), undefined); - c.insert({a:100, b: { foo: "bar" }}); - test.equal(operations.shift(), ['added', {a:100}, 0, idA2]); - handle.stop(); - - // test skip and limit. - c.remove({}); - handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2, fields: { 'blacklisted': 0 }}).observe(cbs); - test.equal(operations.shift(), undefined); - c.insert({a:1, blacklisted:1324}); - test.equal(operations.shift(), undefined); - c.insert({_id: 'foo', a:2, blacklisted:["something"]}); - test.equal(operations.shift(), ['added', {a:2}, 0, null]); - c.insert({a:3, blacklisted: { 2: 3 }}); - test.equal(operations.shift(), ['added', {a:3}, 1, null]); - c.insert({a:4, blacklisted: 6}); - test.equal(operations.shift(), undefined); - c.update({a:1}, {a:0, blacklisted:4444}); - test.equal(operations.shift(), undefined); - c.update({a:0}, {a:5, blacklisted:11111}); - test.equal(operations.shift(), ['removed', 'foo', 0, {a:2}]); - test.equal(operations.shift(), ['added', {a:4}, 1, null]); - c.update({a:3}, {a:3.5, blacklisted:333.4444}); - test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]); - handle.stop(); - - // test _no_indices - - c.remove({}); - handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(Object.assign(cbs, {_no_indices: true})); - c.insert({_id: 'foo', a:1, zoo: "crazy"}); - test.equal(operations.shift(), ['added', {a:1}, -1, null]); - c.update({a:1}, {$set: {a: 2, foobar: "player"}}); - test.equal(operations.shift(), ['changed', {a:2}, -1, {a:1}]); - c.insert({a:10, b:123.45}); - test.equal(operations.shift(), ['added', {a:10}, -1, null]); - c.update({}, {$inc: {a: 1, b:2}}, {multi: true}); - test.equal(operations.shift(), ['changed', {a:3}, -1, {a:2}]); - test.equal(operations.shift(), ['changed', {a:11}, -1, {a:10}]); - c.update({a:11, b:125.45}, {a:1, b:444}); - test.equal(operations.shift(), ['changed', {a:1}, -1, {a:11}]); - test.equal(operations.shift(), ['moved', {a:1}, -1, -1, 'foo']); - c.remove({a:2}); - test.equal(operations.shift(), undefined); - c.remove({a:3}); - test.equal(operations.shift(), ['removed', 'foo', -1, {a:3}]); - handle.stop(); -}); - - -Tinytest.add("minimongo - ordering", function (test) { - var shortBinary = EJSON.newBinary(1); - shortBinary[0] = 128; - var longBinary1 = EJSON.newBinary(2); - longBinary1[1] = 42; - var longBinary2 = EJSON.newBinary(2); - longBinary2[1] = 50; - - var date1 = new Date; - var date2 = new Date(date1.getTime() + 1000); - - // value ordering - assert_ordering(test, LocalCollection._f._cmp, [ - null, - 1, 2.2, 3, - "03", "1", "11", "2", "a", "aaa", - {}, {a: 2}, {a: 3}, {a: 3, b: 4}, {b: 4}, {b: 4, a: 3}, - {b: {}}, {b: [1, 2, 3]}, {b: [1, 2, 4]}, - [], [1, 2], [1, 2, 3], [1, 2, 4], [1, 2, "4"], [1, 2, [4]], - shortBinary, longBinary1, longBinary2, - new MongoID.ObjectID("1234567890abcd1234567890"), - new MongoID.ObjectID("abcd1234567890abcd123456"), - false, true, - date1, date2 - ]); - - // document ordering under a sort specification - var verify = function (sorts, docs) { - (Array.isArray(sorts) ? sorts : [sorts]).forEach(function (sort) { - var sorter = new Minimongo.Sorter(sort); - assert_ordering(test, sorter.getComparator(), docs); - }); - }; - - // note: [] doesn't sort with "arrays", it sorts as "undefined". the position - // of arrays in _typeorder only matters for things like $lt. (This behavior - // verified with MongoDB 2.2.1.) We don't define the relative order of {a: []} - // and {c: 1} is undefined (MongoDB does seem to care but it's not clear how - // or why). - verify([{"a" : 1}, ["a"], [["a", "asc"]]], - [{a: []}, {a: 1}, {a: {}}, {a: true}]); - verify([{"a" : 1}, ["a"], [["a", "asc"]]], - [{c: 1}, {a: 1}, {a: {}}, {a: true}]); - verify([{"a" : -1}, [["a", "desc"]]], - [{a: true}, {a: {}}, {a: 1}, {c: 1}]); - verify([{"a" : -1}, [["a", "desc"]]], - [{a: true}, {a: {}}, {a: 1}, {a: []}]); - - verify([{"a" : 1, "b": -1}, ["a", ["b", "desc"]], - [["a", "asc"], ["b", "desc"]]], - [{c: 1}, {a: 1, b: 3}, {a: 1, b: 2}, {a: 2, b: 0}]); - - verify([{"a" : 1, "b": 1}, ["a", "b"], - [["a", "asc"], ["b", "asc"]]], - [{c: 1}, {a: 1, b: 2}, {a: 1, b: 3}, {a: 2, b: 0}]); - - test.throws(function () { - new Minimongo.Sorter("a"); - }); - - test.throws(function () { - new Minimongo.Sorter(123); - }); - - // We don't support $natural:1 (since we don't actually have Mongo's on-disk - // ordering available!) - test.throws(function () { - new Minimongo.Sorter({$natural: 1}); - }); - - // No sort spec implies everything equal. - test.equal(new Minimongo.Sorter({}).getComparator()({a:1}, {a:2}), 0); - - // All sorts of array edge cases! - // Increasing sort sorts by the smallest element it finds; 1 < 2. - verify({a: 1}, [ - {a: [1, 10, 20]}, - {a: [5, 2, 99]} - ]); - // Decreasing sorts by largest it finds; 99 > 20. - verify({a: -1}, [ - {a: [5, 2, 99]}, - {a: [1, 10, 20]} - ]); - // Can also sort by specific array indices. - verify({'a.1': 1}, [ - {a: [5, 2, 99]}, - {a: [1, 10, 20]} - ]); - // We do NOT expand sub-arrays, so the minimum in the second doc is 5, not - // -20. (Numbers always sort before arrays.) - verify({a: 1}, [ - {a: [1, [10, 15], 20]}, - {a: [5, [-5, -20], 18]} - ]); - // The maximum in each of these is the array, since arrays are "greater" than - // numbers. And [10, 15] is greater than [-5, -20]. - verify({a: -1}, [ - {a: [1, [10, 15], 20]}, - {a: [5, [-5, -20], 18]} - ]); - // 'a.0' here ONLY means "first element of a", not "first element of something - // found in a", so it CANNOT find the 10 or -5. - verify({'a.0': 1}, [ - {a: [1, [10, 15], 20]}, - {a: [5, [-5, -20], 18]} - ]); - verify({'a.0': -1}, [ - {a: [5, [-5, -20], 18]}, - {a: [1, [10, 15], 20]} - ]); - // Similarly, this is just comparing [-5,-20] to [10, 15]. - verify({'a.1': 1}, [ - {a: [5, [-5, -20], 18]}, - {a: [1, [10, 15], 20]} - ]); - verify({'a.1': -1}, [ - {a: [1, [10, 15], 20]}, - {a: [5, [-5, -20], 18]} - ]); - // Here we are just comparing [10,15] directly to [19,3] (and NOT also - // iterating over the numbers; this is implemented by setting dontIterate in - // makeLookupFunction). So [10,15]<[19,3] even though 3 is the smallest - // number you can find there. - verify({'a.1': 1}, [ - {a: [1, [10, 15], 20]}, - {a: [5, [19, 3], 18]} - ]); - verify({'a.1': -1}, [ - {a: [5, [19, 3], 18]}, - {a: [1, [10, 15], 20]} - ]); - // Minimal elements are 1 and 5. - verify({a: 1}, [ - {a: [1, [10, 15], 20]}, - {a: [5, [19, 3], 18]} - ]); - // Maximal elements are [19,3] and [10,15] (because arrays sort higher than - // numbers), even though there's a 20 floating around. - verify({a: -1}, [ - {a: [5, [19, 3], 18]}, - {a: [1, [10, 15], 20]} - ]); - // Maximal elements are [10,15] and [3,19]. [10,15] is bigger even though 19 - // is the biggest number in them, because array comparison is lexicographic. - verify({a: -1}, [ - {a: [1, [10, 15], 20]}, - {a: [5, [3, 19], 18]} - ]); - - // (0,4) < (0,5), so they go in this order. It's not correct to consider - // (0,3) as a sort key for the second document because they come from - // different a-branches. - verify({'a.x': 1, 'a.y': 1}, [ - {a: [{x: 0, y: 4}]}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}]} - ]); - - verify({'a.0.s': 1}, [ - {a: [ {s: 1} ]}, - {a: [ {s: 2} ]} - ]); -}); - -Tinytest.add("minimongo - sort", function (test) { - var c = new LocalCollection(); - for (var i = 0; i < 50; i++) - for (var j = 0; j < 2; j++) - c.insert({a: i, b: j, _id: i + "_" + j}); - - test.equal( - c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, limit: 5}).fetch(), [ - {a: 11, b: 1, _id: "11_1"}, - {a: 12, b: 1, _id: "12_1"}, - {a: 13, b: 1, _id: "13_1"}, - {a: 14, b: 1, _id: "14_1"}, - {a: 15, b: 1, _id: "15_1"}]); - - test.equal( - c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, skip: 3, limit: 5}).fetch(), [ - {a: 14, b: 1, _id: "14_1"}, - {a: 15, b: 1, _id: "15_1"}, - {a: 16, b: 1, _id: "16_1"}, - {a: 17, b: 1, _id: "17_1"}, - {a: 18, b: 1, _id: "18_1"}]); - - test.equal( - c.find({a: {$gte: 20}}, {sort: {a: 1, b: -1}, skip: 50, limit: 5}).fetch(), [ - {a: 45, b: 1, _id: "45_1"}, - {a: 45, b: 0, _id: "45_0"}, - {a: 46, b: 1, _id: "46_1"}, - {a: 46, b: 0, _id: "46_0"}, - {a: 47, b: 1, _id: "47_1"}]); -}); - -Tinytest.add("minimongo - subkey sort", function (test) { - var c = new LocalCollection(); - - // normal case - c.insert({a: {b: 2}}); - c.insert({a: {b: 1}}); - c.insert({a: {b: 3}}); - test.equal( - c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), - [{b: 3}, {b: 2}, {b: 1}]); - - // isn't an object - c.insert({a: 1}); - test.equal( - c.find({}, {sort: {'a.b': 1}}).fetch().map(function (doc) { return doc.a; }), - [1, {b: 1}, {b: 2}, {b: 3}]); - - // complex object - c.insert({a: {b: {c: 1}}}); - test.equal( - c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), - [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1]); - - // no such top level prop - c.insert({c: 1}); - test.equal( - c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), - [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1, undefined]); - - // no such mid level prop. just test that it doesn't throw. - test.equal(c.find({}, {sort: {'a.nope.c': -1}}).count(), 6); -}); - -Tinytest.add("minimongo - array sort", function (test) { - var c = new LocalCollection(); - - // "up" and "down" are the indices that the docs should have when sorted - // ascending and descending by "a.x" respectively. They are not reverses of - // each other: when sorting ascending, you use the minimum value you can find - // in the document, and when sorting descending, you use the maximum value you - // can find. So [1, 4] shows up in the 1 slot when sorting ascending and the 4 - // slot when sorting descending. - // - // Similarly, "selected" is the index that the doc should have in the query - // that sorts ascending on "a.x" and selects {'a.x': {$gt: 1}}. In this case, - // the 1 in [1, 4] may not be used as a sort key. - c.insert({up: 1, down: 1, selected: 2, a: {x: [1, 4]}}); - c.insert({up: 2, down: 2, selected: 0, a: [{x: [2]}, {x: 3}]}); - c.insert({up: 0, down: 4, a: {x: 0}}); - c.insert({up: 3, down: 3, selected: 1, a: {x: 2.5}}); - c.insert({up: 4, down: 0, selected: 3, a: {x: 5}}); - - // Test that the the documents in "cursor" contain values with the name - // "field" running from 0 to the max value of that name in the collection. - var testCursorMatchesField = function (cursor, field) { - var fieldValues = []; - c.find().forEach(function (doc) { - if (doc.hasOwnProperty(field)) - fieldValues.push(doc[field]); - }); - test.equal(cursor.fetch().map(function (doc) { return doc[field]; }), - Array.from({length: Math.max.apply(null, fieldValues) + 1}, function (x, i) { return i; })); - }; - - testCursorMatchesField(c.find({}, {sort: {'a.x': 1}}), 'up'); - testCursorMatchesField(c.find({}, {sort: {'a.x': -1}}), 'down'); - testCursorMatchesField(c.find({'a.x': {$gt: 1}}, {sort: {'a.x': 1}}), - 'selected'); -}); - -Tinytest.add("minimongo - sort keys", function (test) { - var keyListToObject = function (keyList) { - var obj = {}; - keyList.forEach(function (key) { - obj[EJSON.stringify(key)] = true; - }); - return obj; - }; - - var testKeys = function (sortSpec, doc, expectedKeyList) { - var expectedKeys = keyListToObject(expectedKeyList); - var sorter = new Minimongo.Sorter(sortSpec); - - var actualKeyList = []; - sorter._generateKeysFromDoc(doc, function (key) { - actualKeyList.push(key); - }); - var actualKeys = keyListToObject(actualKeyList); - test.equal(actualKeys, expectedKeys); - }; - - var testParallelError = function (sortSpec, doc) { - var sorter = new Minimongo.Sorter(sortSpec); - test.throws(function () { - sorter._generateKeysFromDoc(doc, function (){}); - }, /parallel arrays/); - }; - - // Just non-array fields. - testKeys({'a.x': 1, 'a.y': 1}, - {a: {x: 0, y: 5}}, - [[0,5]]); - - // Ensure that we don't get [0,3] and [1,5]. - testKeys({'a.x': 1, 'a.y': 1}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}]}, - [[0,5], [1,3]]); - - // Ensure we can combine "array fields" with "non-array fields". - testKeys({'a.x': 1, 'a.y': 1, b: -1}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, - [[0,5,42], [1,3,42]]); - testKeys({b: -1, 'a.x': 1, 'a.y': 1}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, - [[42,0,5], [42,1,3]]); - testKeys({'a.x': 1, b: -1, 'a.y': 1}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, - [[0,42,5], [1,42,3]]); - testKeys({a: 1, b: 1}, - {a: [1, 2, 3], b: 42}, - [[1,42], [2,42], [3,42]]); - - // Don't support multiple arrays at the same level. - testParallelError({a: 1, b: 1}, - {a: [1, 2, 3], b: [42]}); - - // We are MORE STRICT than Mongo here; Mongo supports this! - // XXX support this too #NestedArraySort - testParallelError({'a.x': 1, 'a.y': 1}, - {a: [{x: 1, y: [2, 3]}, - {x: 2, y: [4, 5]}]}); -}); - -Tinytest.add("minimongo - sort key filter", function (test) { - var testOrder = function (sortSpec, selector, doc1, doc2) { - var matcher = new Minimongo.Matcher(selector); - var sorter = new Minimongo.Sorter(sortSpec, {matcher: matcher}); - var comparator = sorter.getComparator(); - var comparison = comparator(doc1, doc2); - test.isTrue(comparison < 0); - }; - - testOrder({'a.x': 1}, {'a.x': {$gt: 1}}, - {a: {x: 3}}, - {a: {x: [1, 4]}}); - testOrder({'a.x': 1}, {'a.x': {$gt: 0}}, - {a: {x: [1, 4]}}, - {a: {x: 3}}); - - var keyCompatible = function (sortSpec, selector, key, compatible) { - var matcher = new Minimongo.Matcher(selector); - var sorter = new Minimongo.Sorter(sortSpec, {matcher: matcher}); - var actual = sorter._keyCompatibleWithSelector(key); - test.equal(actual, compatible); - }; - - keyCompatible({a: 1}, {a: 5}, [5], true); - keyCompatible({a: 1}, {a: 5}, [8], false); - keyCompatible({a: 1}, {a: {x: 5}}, [{x: 5}], true); - keyCompatible({a: 1}, {a: {x: 5}}, [{x: 5, y: 9}], false); - keyCompatible({'a.x': 1}, {a: {x: 5}}, [5], true); - // To confirm this: - // > db.x.insert({_id: "q", a: [{x:1}, {x:5}], b: 2}) - // > db.x.insert({_id: "w", a: [{x:5}, {x:10}], b: 1}) - // > db.x.find({}).sort({'a.x': 1, b: 1}) - // { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 } - // { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 } - // > db.x.find({a: {x:5}}).sort({'a.x': 1, b: 1}) - // { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 } - // { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 } - // > db.x.find({'a.x': 5}).sort({'a.x': 1, b: 1}) - // { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 } - // { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 } - // ie, only the last one manages to trigger the key compatibility code, - // not the previous one. (The "b" sort is necessary because when the key - // compatibility code *does* kick in, both documents only end up with "5" - // for the first field as their only sort key, and we need to differentiate - // somehow...) - keyCompatible({'a.x': 1}, {a: {x: 5}}, [1], true); - keyCompatible({'a.x': 1}, {'a.x': 5}, [5], true); - keyCompatible({'a.x': 1}, {'a.x': 5}, [1], false); - - // Regex key check. - keyCompatible({a: 1}, {a: /^foo+/}, ['foo'], true); - keyCompatible({a: 1}, {a: /^foo+/}, ['foooo'], true); - keyCompatible({a: 1}, {a: /^foo+/}, ['foooobar'], true); - keyCompatible({a: 1}, {a: /^foo+/}, ['afoooo'], false); - keyCompatible({a: 1}, {a: /^foo+/}, [''], false); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foo'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foooo'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foooobar'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['afoooo'], false); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, [''], false); - - keyCompatible({a: 1}, {a: /^foo+/i}, ['foo'], true); - // Key compatibility check appears to be turned off for regexps with flags. - keyCompatible({a: 1}, {a: /^foo+/i}, ['bar'], true); - keyCompatible({a: 1}, {a: /^foo+/m}, ['bar'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+", $options: "i"}}, ['bar'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+", $options: "m"}}, ['bar'], true); - - // Multiple keys! - keyCompatible({a: 1, b: 1, c: 1}, - {a: {$gt: 5}, c: {$lt: 3}}, [6, "bla", 2], true); - keyCompatible({a: 1, b: 1, c: 1}, - {a: {$gt: 5}, c: {$lt: 3}}, [6, "bla", 4], false); - keyCompatible({a: 1, b: 1, c: 1}, - {a: {$gt: 5}, c: {$lt: 3}}, [3, "bla", 1], false); - // No filtering is done (ie, all keys are compatible) if the first key isn't - // constrained. - keyCompatible({a: 1, b: 1, c: 1}, - {c: {$lt: 3}}, [3, "bla", 4], true); -}); - -Tinytest.add("minimongo - sort function", function (test) { - var c = new LocalCollection(); - - c.insert({a: 1}); - c.insert({a: 10}); - c.insert({a: 5}); - c.insert({a: 7}); - c.insert({a: 2}); - c.insert({a: 4}); - c.insert({a: 3}); - - var sortFunction = function (doc1, doc2) { - return doc2.a - doc1.a; - }; - - test.equal(c.find({}, {sort: sortFunction}).fetch(), c.find({}).fetch().sort(sortFunction)); - test.notEqual(c.find({}).fetch(), c.find({}).fetch().sort(sortFunction)); - test.equal(c.find({}, {sort: {a: -1}}).fetch(), c.find({}).fetch().sort(sortFunction)); -}); - -Tinytest.add("minimongo - binary search", function (test) { - var forwardCmp = function (a, b) { - return a - b; - }; - - var backwardCmp = function (a, b) { - return -1 * forwardCmp(a, b); - }; - - var checkSearch = function (cmp, array, value, expected, message) { - var actual = LocalCollection._binarySearch(cmp, array, value); - if (expected != actual) { - test.fail({type: "minimongo-binary-search", - message: message + " : Expected index " + expected + - " but had " + actual - }); - } - }; - - var checkSearchForward = function (array, value, expected, message) { - checkSearch(forwardCmp, array, value, expected, message); - }; - var checkSearchBackward = function (array, value, expected, message) { - checkSearch(backwardCmp, array, value, expected, message); - }; - - checkSearchForward([1, 2, 5, 7], 4, 2, "Inner insert"); - checkSearchForward([1, 2, 3, 4], 3, 3, "Inner insert, equal value"); - checkSearchForward([1, 2, 5], 4, 2, "Inner insert, odd length"); - checkSearchForward([1, 3, 5, 6], 9, 4, "End insert"); - checkSearchForward([1, 3, 5, 6], 0, 0, "Beginning insert"); - checkSearchForward([1], 0, 0, "Single array, less than."); - checkSearchForward([1], 1, 1, "Single array, equal."); - checkSearchForward([1], 2, 1, "Single array, greater than."); - checkSearchForward([], 1, 0, "Empty array"); - checkSearchForward([1, 1, 1, 2, 2, 2, 2], 1, 3, "Highly degenerate array, lower"); - checkSearchForward([1, 1, 1, 2, 2, 2, 2], 2, 7, "Highly degenerate array, upper"); - checkSearchForward([2, 2, 2, 2, 2, 2, 2], 1, 0, "Highly degenerate array, lower"); - checkSearchForward([2, 2, 2, 2, 2, 2, 2], 2, 7, "Highly degenerate array, equal"); - checkSearchForward([2, 2, 2, 2, 2, 2, 2], 3, 7, "Highly degenerate array, upper"); - - checkSearchBackward([7, 5, 2, 1], 4, 2, "Backward: Inner insert"); - checkSearchBackward([4, 3, 2, 1], 3, 2, "Backward: Inner insert, equal value"); - checkSearchBackward([5, 2, 1], 4, 1, "Backward: Inner insert, odd length"); - checkSearchBackward([6, 5, 3, 1], 9, 0, "Backward: Beginning insert"); - checkSearchBackward([6, 5, 3, 1], 0, 4, "Backward: End insert"); - checkSearchBackward([1], 0, 1, "Backward: Single array, less than."); - checkSearchBackward([1], 1, 1, "Backward: Single array, equal."); - checkSearchBackward([1], 2, 0, "Backward: Single array, greater than."); - checkSearchBackward([], 1, 0, "Backward: Empty array"); - checkSearchBackward([2, 2, 2, 2, 1, 1, 1], 1, 7, "Backward: Degenerate array, lower"); - checkSearchBackward([2, 2, 2, 2, 1, 1, 1], 2, 4, "Backward: Degenerate array, upper"); - checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 1, 7, "Backward: Highly degenerate array, upper"); - checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 2, 7, "Backward: Highly degenerate array, upper"); - checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 3, 0, "Backward: Highly degenerate array, upper"); -}); - -Tinytest.add("minimongo - modify", function (test) { - var modifyWithQuery = function (doc, query, mod, expected) { - var coll = new LocalCollection; - coll.insert(doc); - // The query is relevant for 'a.$.b'. - coll.update(query, mod); - var actual = coll.findOne(); - delete actual._id; // added by insert - - if (typeof expected === "function") { - expected(actual, EJSON.stringify({input: doc, mod: mod})); - } else { - test.equal(actual, expected, EJSON.stringify({input: doc, mod: mod})); - } - }; - var modify = function (doc, mod, expected) { - modifyWithQuery(doc, {}, mod, expected); - }; - var exceptionWithQuery = function (doc, query, mod) { - var coll = new LocalCollection; - coll.insert(doc); - test.throws(function () { - coll.update(query, mod); - }); - }; - var exception = function (doc, mod) { - exceptionWithQuery(doc, {}, mod); - }; - - var upsert = function (query, mod, expected) { - var coll = new LocalCollection; - - var result = coll.upsert(query, mod); - - var actual = coll.findOne(); - - if (expected._id) { - test.equal(result.insertedId, expected._id); - } - else { - delete actual._id; - } - - test.equal(actual, expected); - }; - - var upsertException = function (query, mod) { - var coll = new LocalCollection; - test.throws(function(){ - coll.upsert(query, mod); - }); - }; - - // document replacement - modify({}, {}, {}); - modify({a: 12}, {}, {}); // tested against mongodb - modify({a: 12}, {a: 13}, {a:13}); - modify({a: 12, b: 99}, {a: 13}, {a:13}); - exception({a: 12}, {a: 13, $set: {b: 13}}); - exception({a: 12}, {$set: {b: 13}, a: 13}); - - exception({a: 12}, {$a: 13}); //invalid operator - exception({a: 12}, {b:{$a: 13}}); - exception({a: 12}, {b:{'a.b': 13}}); - exception({a: 12}, {b:{'\0a': 13}}); - - // keys - modify({}, {$set: {'a': 12}}, {a: 12}); - modify({}, {$set: {'a.b': 12}}, {a: {b: 12}}); - modify({}, {$set: {'a.b.c': 12}}, {a: {b: {c: 12}}}); - modify({a: {d: 99}}, {$set: {'a.b.c': 12}}, {a: {d: 99, b: {c: 12}}}); - modify({}, {$set: {'a.b.3.c': 12}}, {a: {b: {3: {c: 12}}}}); - modify({a: {b: []}}, {$set: {'a.b.3.c': 12}}, { - a: {b: [null, null, null, {c: 12}]}}); - exception({a: [null, null, null]}, {$set: {'a.1.b': 12}}); - exception({a: [null, 1, null]}, {$set: {'a.1.b': 12}}); - exception({a: [null, "x", null]}, {$set: {'a.1.b': 12}}); - exception({a: [null, [], null]}, {$set: {'a.1.b': 12}}); - modify({a: [null, null, null]}, {$set: {'a.3.b': 12}}, { - a: [null, null, null, {b: 12}]}); - exception({a: []}, {$set: {'a.b': 12}}); - exception({a: 12}, {$set: {'a.b': 99}}); // tested on mongo - exception({a: 'x'}, {$set: {'a.b': 99}}); - exception({a: true}, {$set: {'a.b': 99}}); - exception({a: null}, {$set: {'a.b': 99}}); - modify({a: {}}, {$set: {'a.3': 12}}, {a: {'3': 12}}); - modify({a: []}, {$set: {'a.3': 12}}, {a: [null, null, null, 12]}); - exception({}, {$set: {'': 12}}); // tested on mongo - exception({}, {$set: {'.': 12}}); // tested on mongo - exception({}, {$set: {'a.': 12}}); // tested on mongo - exception({}, {$set: {'. ': 12}}); // tested on mongo - exception({}, {$inc: {'... ': 12}}); // tested on mongo - exception({}, {$set: {'a..b': 12}}); // tested on mongo - modify({a: [1,2,3]}, {$set: {'a.01': 99}}, {a: [1, 99, 3]}); - modify({a: [1,{a: 98},3]}, {$set: {'a.01.b': 99}}, {a: [1,{a:98, b: 99},3]}); - modify({}, {$set: {'2.a.b': 12}}, {'2': {'a': {'b': 12}}}); // tested - exception({x: []}, {$set: {'x.2..a': 99}}); - modify({x: [null, null]}, {$set: {'x.2.a': 1}}, {x: [null, null, {a: 1}]}); - exception({x: [null, null]}, {$set: {'x.1.a': 1}}); - - // a.$.b - modifyWithQuery({a: [{x: 2}, {x: 4}]}, {'a.x': 4}, {$set: {'a.$.z': 9}}, - {a: [{x: 2}, {x: 4, z: 9}]}); - exception({a: [{x: 2}, {x: 4}]}, {$set: {'a.$.z': 9}}); - exceptionWithQuery({a: [{x: 2}, {x: 4}], b: 5}, {b: 5}, {$set: {'a.$.z': 9}}); - // can't have two $ - exceptionWithQuery({a: [{x: [2]}]}, {'a.x': 2}, {$set: {'a.$.x.$': 9}}); - modifyWithQuery({a: [5, 6, 7]}, {a: 6}, {$set: {'a.$': 9}}, {a: [5, 9, 7]}); - modifyWithQuery({a: [{b: [{c: 9}, {c: 10}]}, {b: {c: 11}}]}, {'a.b.c': 10}, - {$unset: {'a.$.b': 1}}, {a: [{}, {b: {c: 11}}]}); - modifyWithQuery({a: [{b: [{c: 9}, {c: 10}]}, {b: {c: 11}}]}, {'a.b.c': 11}, - {$unset: {'a.$.b': 1}}, - {a: [{b: [{c: 9}, {c: 10}]}, {}]}); - modifyWithQuery({a: [1]}, {'a.0': 1}, {$set: {'a.$': 5}}, {a: [5]}); - modifyWithQuery({a: [9]}, {a: {$mod: [2, 1]}}, {$set: {'a.$': 5}}, {a: [5]}); - // Negatives don't set '$'. - exceptionWithQuery({a: [1]}, {$not: {a: 2}}, {$set: {'a.$': 5}}); - exceptionWithQuery({a: [1]}, {'a.0': {$ne: 2}}, {$set: {'a.$': 5}}); - // One $or clause works. - modifyWithQuery({a: [{x: 2}, {x: 4}]}, - {$or: [{'a.x': 4}]}, {$set: {'a.$.z': 9}}, - {a: [{x: 2}, {x: 4, z: 9}]}); - // More $or clauses throw. - exceptionWithQuery({a: [{x: 2}, {x: 4}]}, - {$or: [{'a.x': 4}, {'a.x': 4}]}, - {$set: {'a.$.z': 9}}); - // $and uses the last one. - modifyWithQuery({a: [{x: 1}, {x: 3}]}, - {$and: [{'a.x': 1}, {'a.x': 3}]}, - {$set: {'a.$.x': 5}}, - {a: [{x: 1}, {x: 5}]}); - modifyWithQuery({a: [{x: 1}, {x: 3}]}, - {$and: [{'a.x': 3}, {'a.x': 1}]}, - {$set: {'a.$.x': 5}}, - {a: [{x: 5}, {x: 3}]}); - // Same goes for the implicit AND of a document selector. - modifyWithQuery({a: [{x: 1}, {y: 3}]}, - {'a.x': 1, 'a.y': 3}, - {$set: {'a.$.z': 5}}, - {a: [{x: 1}, {y: 3, z: 5}]}); - modifyWithQuery({a: [{x: 1}, {y: 1}, {x: 1, y: 1}]}, - {a: {$elemMatch: {x: 1, y: 1}}}, - {$set: {'a.$.x': 2}}, - {a: [{x: 1}, {y: 1}, {x: 2, y: 1}]}); - modifyWithQuery({a: [{b: [{x: 1}, {y: 1}, {x: 1, y: 1}]}]}, - {'a.b': {$elemMatch: {x: 1, y: 1}}}, - {$set: {'a.$.b': 3}}, - {a: [{b: 3}]}); - // with $near, make sure it does not find the closest one (#3599) - modifyWithQuery({a: []}, - {'a.b': {$near: [5, 5]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[]}); - modifyWithQuery({a: [{b: [ [3,3], [4,4] ]}]}, - {'a.b': {$near: [5, 5]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"}]}); - modifyWithQuery({a: [{b: [1,1]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [5, 5]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); - modifyWithQuery({a: [{b: [1,1]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [9, 9], $maxDistance: 1}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); - modifyWithQuery({a: [{b: [1,1]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [9, 9]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); - modifyWithQuery({a: [{b: [9,9]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [9, 9]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); - modifyWithQuery({a: [{b:[4,3]}, - {c: [1,1]}]}, - {'a.c': {$near: [1, 1]}}, - {$set: {'a.$.c': 'k'}}, - {"a":[{"c": "k", "b":[4,3]},{"c":[1,1]}]}); - modifyWithQuery({a: [{c: [9,9]}, - {b: [ [3,3], [4,4] ]}, - {b: [1,1]}]}, - {'a.b': {$near: [1, 1]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"c": [9,9], "b":"k"},{"b": [ [3,3], [4,4]]},{"b":[1,1]}]}); - modifyWithQuery({a: [{c: [9,9], b:[4,3]}, - {b: [ [3,3], [4,4] ]}, - {b: [1,1]}]}, - {'a.b': {$near: [1, 1]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"c": [9,9], "b":"k"},{"b": [ [3,3], [4,4]]},{"b":[1,1]}]}); - - // $inc - modify({a: 1, b: 2}, {$inc: {a: 10}}, {a: 11, b: 2}); - modify({a: 1, b: 2}, {$inc: {c: 10}}, {a: 1, b: 2, c: 10}); - exception({a: 1}, {$inc: {a: '10'}}); - exception({a: 1}, {$inc: {a: true}}); - exception({a: 1}, {$inc: {a: [10]}}); - exception({a: '1'}, {$inc: {a: 10}}); - exception({a: [1]}, {$inc: {a: 10}}); - exception({a: {}}, {$inc: {a: 10}}); - exception({a: false}, {$inc: {a: 10}}); - exception({a: null}, {$inc: {a: 10}}); - modify({a: [1, 2]}, {$inc: {'a.1': 10}}, {a: [1, 12]}); - modify({a: [1, 2]}, {$inc: {'a.2': 10}}, {a: [1, 2, 10]}); - modify({a: [1, 2]}, {$inc: {'a.3': 10}}, {a: [1, 2, null, 10]}); - modify({a: {b: 2}}, {$inc: {'a.b': 10}}, {a: {b: 12}}); - modify({a: {b: 2}}, {$inc: {'a.c': 10}}, {a: {b: 2, c: 10}}); - exception({}, {$inc: {_id: 1}}); - - // $currentDate - modify({}, {$currentDate: {a: true}}, (result, msg) => { test.instanceOf(result.a,Date,msg) }); - modify({}, {$currentDate: {a: {$type: "date"}}}, (result, msg) => { test.instanceOf(result.a,Date,msg) }); - exception({}, {$currentDate: {a: false}}); - exception({}, {$currentDate: {a: {}}}); - exception({}, {$currentDate: {a: {$type: "timestamp"}}}); - - // $min - modify({a: 1, b: 2}, {$min: {b: 1}}, {a: 1, b: 1}); - modify({a: 1, b: 2}, {$min: {b: 3}}, {a: 1, b: 2}); - modify({a: 1, b: 2}, {$min: {c: 10}}, {a: 1, b: 2, c: 10}); - exception({a: 1}, {$min: {a: '10'}}); - exception({a: 1}, {$min: {a: true}}); - exception({a: 1}, {$min: {a: [10]}}); - exception({a: '1'}, {$min: {a: 10}}); - exception({a: [1]}, {$min: {a: 10}}); - exception({a: {}}, {$min: {a: 10}}); - exception({a: false}, {$min: {a: 10}}); - exception({a: null}, {$min: {a: 10}}); - modify({a: [1, 2]}, {$min: {'a.1': 1}}, {a: [1, 1]}); - modify({a: [1, 2]}, {$min: {'a.1': 3}}, {a: [1, 2]}); - modify({a: [1, 2]}, {$min: {'a.2': 10}}, {a: [1, 2, 10]}); - modify({a: [1, 2]}, {$min: {'a.3': 10}}, {a: [1, 2, null, 10]}); - modify({a: {b: 2}}, {$min: {'a.b': 1}}, {a: {b: 1}}); - modify({a: {b: 2}}, {$min: {'a.c': 10}}, {a: {b: 2, c: 10}}); - exception({}, {$min: {_id: 1}}); - - // $max - modify({a: 1, b: 2}, {$max: {b: 1}}, {a: 1, b: 2}); - modify({a: 1, b: 2}, {$max: {b: 3}}, {a: 1, b: 3}); - modify({a: 1, b: 2}, {$max: {c: 10}}, {a: 1, b: 2, c: 10}); - exception({a: 1}, {$max: {a: '10'}}); - exception({a: 1}, {$max: {a: true}}); - exception({a: 1}, {$max: {a: [10]}}); - exception({a: '1'}, {$max: {a: 10}}); - exception({a: [1]}, {$max: {a: 10}}); - exception({a: {}}, {$max: {a: 10}}); - exception({a: false}, {$max: {a: 10}}); - exception({a: null}, {$max: {a: 10}}); - modify({a: [1, 2]}, {$max: {'a.1': 3}}, {a: [1, 3]}); - modify({a: [1, 2]}, {$max: {'a.1': 1}}, {a: [1, 2]}); - modify({a: [1, 2]}, {$max: {'a.2': 10}}, {a: [1, 2, 10]}); - modify({a: [1, 2]}, {$max: {'a.3': 10}}, {a: [1, 2, null, 10]}); - modify({a: {b: 2}}, {$max: {'a.b': 3}}, {a: {b: 3}}); - modify({a: {b: 2}}, {$max: {'a.c': 10}}, {a: {b: 2, c: 10}}); - exception({}, {$max: {_id: 1}}); - - // $set - modify({a: 1, b: 2}, {$set: {a: 10}}, {a: 10, b: 2}); - modify({a: 1, b: 2}, {$set: {c: 10}}, {a: 1, b: 2, c: 10}); - modify({a: 1, b: 2}, {$set: {a: {c: 10}}}, {a: {c: 10}, b: 2}); - modify({a: [1, 2], b: 2}, {$set: {a: [3, 4]}}, {a: [3, 4], b: 2}); - modify({a: [1, 2, 3], b: 2}, {$set: {'a.1': [3, 4]}}, - {a: [1, [3, 4], 3], b:2}); - modify({a: [1], b: 2}, {$set: {'a.1': 9}}, {a: [1, 9], b: 2}); - modify({a: [1], b: 2}, {$set: {'a.2': 9}}, {a: [1, null, 9], b: 2}); - modify({a: {b: 1}}, {$set: {'a.c': 9}}, {a: {b: 1, c: 9}}); - modify({}, {$set: {'x._id': 4}}, {x: {_id: 4}}); - exception({}, {$set: {_id: 4}}); - exception({_id: 4}, {$set: {_id: 4}}); // even not-changing _id is bad - //restricted field names - exception({a:{}}, {$set:{a:{$a:1}}}); - exception({ a: {} }, { $set: { a: { c: - [{ b: { $a: 1 } }] } } }); - exception({a:{}}, {$set:{a:{'\0a':1}}}); - exception({a:{}}, {$set:{a:{'a.b':1}}}); - - // $unset - modify({}, {$unset: {a: 1}}, {}); - modify({a: 1}, {$unset: {a: 1}}, {}); - modify({a: 1, b: 2}, {$unset: {a: 1}}, {b: 2}); - modify({a: 1, b: 2}, {$unset: {a: 0}}, {b: 2}); - modify({a: 1, b: 2}, {$unset: {a: false}}, {b: 2}); - modify({a: 1, b: 2}, {$unset: {a: null}}, {b: 2}); - modify({a: 1, b: 2}, {$unset: {a: [1]}}, {b: 2}); - modify({a: 1, b: 2}, {$unset: {a: {}}}, {b: 2}); - modify({a: {b: 2, c: 3}}, {$unset: {'a.b': 1}}, {a: {c: 3}}); - modify({a: [1, 2, 3]}, {$unset: {'a.1': 1}}, {a: [1, null, 3]}); // tested - modify({a: [1, 2, 3]}, {$unset: {'a.2': 1}}, {a: [1, 2, null]}); // tested - modify({a: [1, 2, 3]}, {$unset: {'a.x': 1}}, {a: [1, 2, 3]}); // tested - modify({a: {b: 1}}, {$unset: {'a.b.c.d': 1}}, {a: {b: 1}}); - modify({a: {b: 1}}, {$unset: {'a.x.c.d': 1}}, {a: {b: 1}}); - modify({a: {b: {c: 1}}}, {$unset: {'a.b.c': 1}}, {a: {b: {}}}); - exception({}, {$unset: {_id: 1}}); - - // $push - modify({}, {$push: {a: 1}}, {a: [1]}); - modify({a: []}, {$push: {a: 1}}, {a: [1]}); - modify({a: [1]}, {$push: {a: 2}}, {a: [1, 2]}); - exception({a: true}, {$push: {a: 1}}); - modify({a: [1]}, {$push: {a: [2]}}, {a: [1, [2]]}); - modify({a: []}, {$push: {'a.1': 99}}, {a: [null, [99]]}); // tested - modify({a: {}}, {$push: {'a.x': 99}}, {a: {x: [99]}}); - modify({}, {$push: {a: {$each: [1, 2, 3]}}}, - {a: [1, 2, 3]}); - modify({a: []}, {$push: {a: {$each: [1, 2, 3]}}}, - {a: [1, 2, 3]}); - modify({a: [true]}, {$push: {a: {$each: [1, 2, 3]}}}, - {a: [true, 1, 2, 3]}); - modify({a: [true]}, {$push: {a: {$each: [1, 2, 3], $slice: -2}}}, - {a: [2, 3]}); - modify({a: [false, true]}, {$push: {a: {$each: [1], $slice: -2}}}, - {a: [true, 1]}); - modify( - {a: [{x: 3}, {x: 1}]}, - {$push: {a: { - $each: [{x: 4}, {x: 2}], - $slice: -2, - $sort: {x: 1} - }}}, - {a: [{x: 3}, {x: 4}]}); - modify({}, {$push: {a: {$each: [1, 2, 3], $slice: 0}}}, {a: []}); - modify({a: [1, 2]}, {$push: {a: {$each: [1, 2, 3], $slice: 0}}}, {a: []}); - // $push with $position modifier - // No negative number for $position - exception({a: []}, {$push: {a: {$each: [0], $position: -1}}}); - modify({a: [1, 2]}, {$push: {a: {$each: [0], $position: 0}}}, - {a: [0, 1, 2]}); - modify({a: [1, 2]}, {$push: {a: {$each: [-1, 0], $position: 0}}}, - {a: [-1, 0, 1, 2]}); - modify({a: [1, 3]}, {$push: {a: {$each: [2], $position: 1}}}, {a: [1, 2, 3]}); - modify({a: [1, 4]}, {$push: {a: {$each: [2, 3], $position: 1}}}, - {a: [1, 2, 3, 4]}); - modify({a: [1, 2]}, {$push: {a: {$each: [3], $position: 3}}}, {a: [1, 2, 3]}); - modify({a: [1, 2]}, {$push: {a: {$each: [3], $position: 99}}}, - {a: [1, 2, 3]}); - modify({a: [1, 2]}, {$push: {a: {$each: [3], $position: 99, $slice: -2}}}, - {a: [2, 3]}); - modify( - {a: [{x: 1}, {x: 2}]}, - {$push: {a: {$each: [{x: 3}], $position: 0, $sort: {x: 1}, $slice: -3}}}, - {a: [{x: 1}, {x: 2}, {x: 3}]} - ); - modify( - {a: [{x: 1}, {x: 2}]}, - {$push: {a: {$each: [{x: 3}], $position: 0, $sort: {x: 1}, $slice: 0}}}, - {a: []} - ); - //restricted field names - exception({}, {$push: {$a: 1}}); - exception({}, {$push: {'\0a': 1}}); - exception({}, {$push: {a: {$a:1}}}); - exception({}, {$push: {a: {$each: [{$a:1}]}}}); - exception({}, {$push: {a: {$each: [{"a.b":1}]}}}); - exception({}, {$push: {a: {$each: [{'\0a':1}]}}}); - modify({}, {$push: {a: {$each: [{'':1}]}}}, {a: [ { '': 1 } ]}); - modify({}, {$push: {a: {$each: [{' ':1}]}}}, {a: [ { ' ': 1 } ]}); - exception({}, {$push: {a: {$each: [{'.':1}]}}}); - - // #issue 5167 - // $push $slice with positive numbers - modify({}, {$push: {a: {$each: [], $slice: 5}}}, {a:[]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [], $slice: 1}}}, {a:[1]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 1}}}, {a:[1]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 2}}}, {a:[1,2]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 4}}}, {a:[1,2,3,4]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 5}}}, {a:[1,2,3,4,5]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 10}}}, {a:[1,2,3,4,5]}); - - - // $pushAll - modify({}, {$pushAll: {a: [1]}}, {a: [1]}); - modify({a: []}, {$pushAll: {a: [1]}}, {a: [1]}); - modify({a: [1]}, {$pushAll: {a: [2]}}, {a: [1, 2]}); - modify({}, {$pushAll: {a: [1, 2]}}, {a: [1, 2]}); - modify({a: []}, {$pushAll: {a: [1, 2]}}, {a: [1, 2]}); - modify({a: [1]}, {$pushAll: {a: [2, 3]}}, {a: [1, 2, 3]}); - modify({}, {$pushAll: {a: []}}, {a: []}); - modify({a: []}, {$pushAll: {a: []}}, {a: []}); - modify({a: [1]}, {$pushAll: {a: []}}, {a: [1]}); - exception({a: true}, {$pushAll: {a: [1]}}); - exception({a: []}, {$pushAll: {a: 1}}); - modify({a: []}, {$pushAll: {'a.1': [99]}}, {a: [null, [99]]}); - modify({a: []}, {$pushAll: {'a.1': []}}, {a: [null, []]}); - modify({a: {}}, {$pushAll: {'a.x': [99]}}, {a: {x: [99]}}); - modify({a: {}}, {$pushAll: {'a.x': []}}, {a: {x: []}}); - exception({a: [1]}, {$pushAll: {a: [{$a:1}]}}); - exception({a: [1]}, {$pushAll: {a: [{'\0a':1}]}}); - exception({a: [1]}, {$pushAll: {a: [{"a.b":1}]}}); - - // $addToSet - modify({}, {$addToSet: {a: 1}}, {a: [1]}); - modify({a: []}, {$addToSet: {a: 1}}, {a: [1]}); - modify({a: [1]}, {$addToSet: {a: 2}}, {a: [1, 2]}); - modify({a: [1, 2]}, {$addToSet: {a: 1}}, {a: [1, 2]}); - modify({a: [1, 2]}, {$addToSet: {a: 2}}, {a: [1, 2]}); - modify({a: [1, 2]}, {$addToSet: {a: 3}}, {a: [1, 2, 3]}); - exception({a: true}, {$addToSet: {a: 1}}); - modify({a: [1]}, {$addToSet: {a: [2]}}, {a: [1, [2]]}); - modify({}, {$addToSet: {a: {x: 1}}}, {a: [{x: 1}]}); - modify({a: [{x: 1}]}, {$addToSet: {a: {x: 1}}}, {a: [{x: 1}]}); - modify({a: [{x: 1}]}, {$addToSet: {a: {x: 2}}}, {a: [{x: 1}, {x: 2}]}); - modify({a: [{x: 1, y: 2}]}, {$addToSet: {a: {x: 1, y: 2}}}, - {a: [{x: 1, y: 2}]}); - modify({a: [{x: 1, y: 2}]}, {$addToSet: {a: {y: 2, x: 1}}}, - {a: [{x: 1, y: 2}, {y: 2, x: 1}]}); - modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4]}}}, {a: [1, 2, 3, 4]}); - modify({}, {$addToSet: {a: {$each: []}}}, {a: []}); - modify({}, {$addToSet: {a: {$each: [1]}}}, {a: [1]}); - modify({a: []}, {$addToSet: {'a.1': 99}}, {a: [null, [99]]}); - modify({a: {}}, {$addToSet: {'a.x': 99}}, {a: {x: [99]}}); - - // invalid field names - exception({}, {$addToSet: {a: {$b:1}}}); - exception({}, {$addToSet: {a: {"a.b":1}}}); - exception({}, {$addToSet: {a: {"a.":1}}}); - exception({}, {$addToSet: {a: {'\u0000a':1}}}); - exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, {$a:1}]}}}); - exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, {'\0a':1}]}}}); - exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, [{$a:1}]]}}}); - exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, [{b:{c:[{a:1},{"d.s":2}]}}]]}}}); - exception({a: [1, 2]}, {$addToSet: {a:{b: [3, 1, [{b:{c:[{a:1},{"d.s":2}]}}]]}}}); - //$each is first element and thus an operator - modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4], b: 12}}},{a: [ 1, 2, 3, 4 ]}); - // this should fail because $each is now a field name (not first in object) and thus invalid field name with $ - exception({a: [1, 2]}, {$addToSet: {a: {b: 12, $each: [3, 1, 4]}}}); - - // $pop - modify({}, {$pop: {a: 1}}, {}); // tested - modify({}, {$pop: {a: -1}}, {}); // tested - modify({a: []}, {$pop: {a: 1}}, {a: []}); - modify({a: []}, {$pop: {a: -1}}, {a: []}); - modify({a: [1, 2, 3]}, {$pop: {a: 1}}, {a: [1, 2]}); - modify({a: [1, 2, 3]}, {$pop: {a: 10}}, {a: [1, 2]}); - modify({a: [1, 2, 3]}, {$pop: {a: .001}}, {a: [1, 2]}); - modify({a: [1, 2, 3]}, {$pop: {a: 0}}, {a: [1, 2]}); - modify({a: [1, 2, 3]}, {$pop: {a: "stuff"}}, {a: [1, 2]}); - modify({a: [1, 2, 3]}, {$pop: {a: -1}}, {a: [2, 3]}); - modify({a: [1, 2, 3]}, {$pop: {a: -10}}, {a: [2, 3]}); - modify({a: [1, 2, 3]}, {$pop: {a: -.001}}, {a: [2, 3]}); - exception({a: true}, {$pop: {a: 1}}); - exception({a: true}, {$pop: {a: -1}}); - modify({a: []}, {$pop: {'a.1': 1}}, {a: []}); // tested - modify({a: [1, [2, 3], 4]}, {$pop: {'a.1': 1}}, {a: [1, [2], 4]}); - modify({a: {}}, {$pop: {'a.x': 1}}, {a: {}}); // tested - modify({a: {x: [2, 3]}}, {$pop: {'a.x': 1}}, {a: {x: [2]}}); - - // $pull - modify({}, {$pull: {a: 1}}, {}); - modify({}, {$pull: {'a.x': 1}}, {}); - modify({a: {}}, {$pull: {'a.x': 1}}, {a: {}}); - exception({a: true}, {$pull: {a: 1}}); - modify({a: [2, 1, 2]}, {$pull: {a: 1}}, {a: [2, 2]}); - modify({a: [2, 1, 2]}, {$pull: {a: 2}}, {a: [1]}); - modify({a: [2, 1, 2]}, {$pull: {a: 3}}, {a: [2, 1, 2]}); - modify({a: [1, null, 2, null]}, {$pull: {a: null}}, {a: [1, 2]}); - modify({a: []}, {$pull: {a: 3}}, {a: []}); - modify({a: [[2], [2, 1], [3]]}, {$pull: {a: [2, 1]}}, - {a: [[2], [3]]}); // tested - modify({a: [{b: 1, c: 2}, {b: 2, c: 2}]}, {$pull: {a: {b: 1}}}, - {a: [{b: 2, c: 2}]}); - modify({a: [{b: 1, c: 2}, {b: 2, c: 2}]}, {$pull: {a: {c: 2}}}, - {a: []}); - // XXX implement this functionality! - // probably same refactoring as $elemMatch? - // modify({a: [1, 2, 3, 4]}, {$pull: {$gt: 2}}, {a: [1,2]}); fails! - - // $pullAll - modify({}, {$pullAll: {a: [1]}}, {}); - modify({a: [1, 2, 3]}, {$pullAll: {a: []}}, {a: [1, 2, 3]}); - modify({a: [1, 2, 3]}, {$pullAll: {a: [2]}}, {a: [1, 3]}); - modify({a: [1, 2, 3]}, {$pullAll: {a: [2, 1]}}, {a: [3]}); - modify({a: [1, 2, 3]}, {$pullAll: {a: [1, 2]}}, {a: [3]}); - modify({}, {$pullAll: {'a.b.c': [2]}}, {}); - exception({a: true}, {$pullAll: {a: [1]}}); - exception({a: [1, 2, 3]}, {$pullAll: {a: 1}}); - modify({x: [{a: 1}, {a: 1, b: 2}]}, {$pullAll: {x: [{a: 1}]}}, - {x: [{a: 1, b: 2}]}); - - // $rename - modify({}, {$rename: {a: 'b'}}, {}); - modify({a: [12]}, {$rename: {a: 'b'}}, {b: [12]}); - modify({a: {b: 12}}, {$rename: {a: 'c'}}, {c: {b: 12}}); - modify({a: {b: 12}}, {$rename: {'a.b': 'a.c'}}, {a: {c: 12}}); - modify({a: {b: 12}}, {$rename: {'a.b': 'x'}}, {a: {}, x: 12}); // tested - modify({a: {b: 12}}, {$rename: {'a.b': 'q.r'}}, {a: {}, q: {r: 12}}); - modify({a: {b: 12}}, {$rename: {'a.b': 'q.2.r'}}, {a: {}, q: {2: {r: 12}}}); - modify({a: {b: 12}, q: {}}, {$rename: {'a.b': 'q.2.r'}}, - {a: {}, q: {2: {r: 12}}}); - exception({a: {b: 12}, q: []}, {$rename: {'a.b': 'q.2'}}); // tested - exception({a: {b: 12}, q: []}, {$rename: {'a.b': 'q.2.r'}}); // tested - // These strange MongoDB behaviors throw. - // modify({a: {b: 12}, q: []}, {$rename: {'q.1': 'x'}}, - // {a: {b: 12}, x: []}); // tested - // modify({a: {b: 12}, q: []}, {$rename: {'q.1.j': 'x'}}, - // {a: {b: 12}, x: []}); // tested - exception({}, {$rename: {'a': 'a'}}); - exception({}, {$rename: {'a.b': 'a.b'}}); - modify({a: 12, b: 13}, {$rename: {a: 'b'}}, {b: 12}); - exception({a: [12]}, {$rename: {a: '$b'}}); - exception({a: [12]}, {$rename: {a: '\0a'}}); - - // $setOnInsert - modify({a: 0}, {$setOnInsert: {a: 12}}, {a: 0}); - upsert({a: 12}, {$setOnInsert: {b: 12}}, {a: 12, b: 12}); - upsert({a: 12}, {$setOnInsert: {_id: 'test'}}, {_id: 'test', a: 12}); - upsert({"a.b": 10}, {$setOnInsert: {a: {b: 10, c: 12}}}, {a: {b: 10, c: 12}}); - upsert({"a.b": 10}, {$setOnInsert: {c: 12}}, {a: {b: 10}, c: 12}); - upsert({"_id": 'test'}, {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); - upsert('test', {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); - upsertException({a: 0}, {$setOnInsert: {$a: 12}}); - upsertException({a: 0}, {$setOnInsert: {'\0a': 12}}); - upsert({a: 0}, {$setOnInsert: {b: {a:1}}}, {a:0, b:{a:1}}); - upsertException({a: 0}, {$setOnInsert: {b: {$a:1}}}); - upsertException({a: 0}, {$setOnInsert: {b: {'a.b':1}}}); - upsertException({a: 0}, {$setOnInsert: {b: {'\0a':1}}}); - - // Test for https://github.com/meteor/meteor/issues/8775. - upsert( - { a: { $exists: true }}, - { $setOnInsert: { a: 123 }}, - { a: 123 } - ); - - // Tests for https://github.com/meteor/meteor/issues/8794. - const testObjectId = new MongoID.ObjectID(); - upsert( - { _id: testObjectId }, - { $setOnInsert: { a: 123 } }, - { _id: testObjectId, a: 123 }, - ); - upsert( - { someOtherId: testObjectId }, - { $setOnInsert: { a: 123 } }, - { someOtherId: testObjectId, a: 123 }, - ); - upsert( - { a: { $eq: testObjectId } }, - { $setOnInsert: { a: 123 } }, - { a: 123 }, - ); - const testDate = new Date('2017-01-01'); - upsert( - { someDate: testDate }, - { $setOnInsert: { a: 123 } }, - { someDate: testDate, a: 123 }, - ); - upsert( - { - a: Object.create(null, { - $exists: { - writable: true, - configurable: true, - value: true - } - }), - }, - { $setOnInsert: { a: 123 } }, - { a: 123 }, - ); - upsert( - { foo: { $exists: true, $type: 2 }}, - { $setOnInsert: { bar: 'baz' } }, - { bar: 'baz' } - ); - upsert( - { foo: {} }, - { $setOnInsert: { bar: 'baz' } }, - { foo: {}, bar: 'baz' } - ); - - exception({}, {$set: {_id: 'bad'}}); - - // $bit - // unimplemented - - // XXX test case sensitivity of modops - // XXX for each (most) modop, test that it performs a deep copy -}); - -// XXX test update() (selecting docs, multi, upsert..) - -Tinytest.add("minimongo - observe ordered", function (test) { - var operations = []; - var cbs = log_callbacks(operations); - var handle; - - var c = new LocalCollection(); - handle = c.find({}, {sort: {a: 1}}).observe(cbs); - test.isTrue(handle.collection === c); - - c.insert({_id: 'foo', a:1}); - test.equal(operations.shift(), ['added', {a:1}, 0, null]); - c.update({a:1}, {$set: {a: 2}}); - test.equal(operations.shift(), ['changed', {a:2}, 0, {a:1}]); - c.insert({a:10}); - test.equal(operations.shift(), ['added', {a:10}, 1, null]); - c.update({}, {$inc: {a: 1}}, {multi: true}); - test.equal(operations.shift(), ['changed', {a:3}, 0, {a:2}]); - test.equal(operations.shift(), ['changed', {a:11}, 1, {a:10}]); - c.update({a:11}, {a:1}); - test.equal(operations.shift(), ['changed', {a:1}, 1, {a:11}]); - test.equal(operations.shift(), ['moved', {a:1}, 1, 0, 'foo']); - c.remove({a:2}); - test.equal(operations.shift(), undefined); - c.remove({a:3}); - test.equal(operations.shift(), ['removed', 'foo', 1, {a:3}]); - - // test stop - handle.stop(); - var idA2 = Random.id(); - c.insert({_id: idA2, a:2}); - test.equal(operations.shift(), undefined); - - // test initial inserts (and backwards sort) - handle = c.find({}, {sort: {a: -1}}).observe(cbs); - test.equal(operations.shift(), ['added', {a:2}, 0, null]); - test.equal(operations.shift(), ['added', {a:1}, 1, null]); - handle.stop(); - - // test _suppress_initial - handle = c.find({}, {sort: {a: -1}}).observe(Object.assign({ - _suppress_initial: true}, cbs)); - test.equal(operations.shift(), undefined); - c.insert({a:100}); - test.equal(operations.shift(), ['added', {a:100}, 0, idA2]); - handle.stop(); - - // test skip and limit. - c.remove({}); - handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2}).observe(cbs); - test.equal(operations.shift(), undefined); - c.insert({a:1}); - test.equal(operations.shift(), undefined); - c.insert({_id: 'foo', a:2}); - test.equal(operations.shift(), ['added', {a:2}, 0, null]); - c.insert({a:3}); - test.equal(operations.shift(), ['added', {a:3}, 1, null]); - c.insert({a:4}); - test.equal(operations.shift(), undefined); - c.update({a:1}, {a:0}); - test.equal(operations.shift(), undefined); - c.update({a:0}, {a:5}); - test.equal(operations.shift(), ['removed', 'foo', 0, {a:2}]); - test.equal(operations.shift(), ['added', {a:4}, 1, null]); - c.update({a:3}, {a:3.5}); - test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]); - handle.stop(); - - // test observe limit with pre-existing docs - c.remove({}); - c.insert({a: 1}); - c.insert({_id: 'two', a: 2}); - c.insert({a: 3}); - handle = c.find({}, {sort: {a: 1}, limit: 2}).observe(cbs); - test.equal(operations.shift(), ['added', {a:1}, 0, null]); - test.equal(operations.shift(), ['added', {a:2}, 1, null]); - test.equal(operations.shift(), undefined); - c.remove({a: 2}); - test.equal(operations.shift(), ['removed', 'two', 1, {a:2}]); - test.equal(operations.shift(), ['added', {a:3}, 1, null]); - test.equal(operations.shift(), undefined); - handle.stop(); - - // test _no_indices - - c.remove({}); - handle = c.find({}, {sort: {a: 1}}).observe(Object.assign(cbs, {_no_indices: true})); - c.insert({_id: 'foo', a:1}); - test.equal(operations.shift(), ['added', {a:1}, -1, null]); - c.update({a:1}, {$set: {a: 2}}); - test.equal(operations.shift(), ['changed', {a:2}, -1, {a:1}]); - c.insert({a:10}); - test.equal(operations.shift(), ['added', {a:10}, -1, null]); - c.update({}, {$inc: {a: 1}}, {multi: true}); - test.equal(operations.shift(), ['changed', {a:3}, -1, {a:2}]); - test.equal(operations.shift(), ['changed', {a:11}, -1, {a:10}]); - c.update({a:11}, {a:1}); - test.equal(operations.shift(), ['changed', {a:1}, -1, {a:11}]); - test.equal(operations.shift(), ['moved', {a:1}, -1, -1, 'foo']); - c.remove({a:2}); - test.equal(operations.shift(), undefined); - c.remove({a:3}); - test.equal(operations.shift(), ['removed', 'foo', -1, {a:3}]); - handle.stop(); -}); - -[true, false].forEach(function (ordered) { - Tinytest.add("minimongo - observe ordered: " + ordered, function (test) { - var c = new LocalCollection(); - - var ev = ""; - var makecb = function (tag) { - var ret = {}; - ["added", "changed", "removed"].forEach(function (fn) { - var fnName = ordered ? fn + "At" : fn; - ret[fnName] = function (doc) { - ev = (ev + fn.substr(0, 1) + tag + doc._id + "_"); - }; - }); - return ret; - }; - var expect = function (x) { - test.equal(ev, x); - ev = ""; - }; - - c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); - c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); - c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); - - // This should work equally well for ordered and unordered observations - // (because the callbacks don't look at indices and there's no 'moved' - // callback). - var handle = c.find({tags: "flower"}).observe(makecb('a')); - expect("aa3_"); - c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}}); - expect("ra3_"); - c.update({name: "rose"}, {$set: {tags: ["flower", "red", "squishy"]}}); - expect("aa3_"); - c.update({name: "rose"}, {$set: {food: false}}); - expect("ca3_"); - c.remove({}); - expect("ra3_"); - c.insert({_id: 4, name: "daisy", tags: ["flower"]}); - expect("aa4_"); - handle.stop(); - // After calling stop, no more callbacks are called. - c.insert({_id: 5, name: "iris", tags: ["flower"]}); - expect(""); - - // Test that observing a lookup by ID works. - handle = c.find(4).observe(makecb('b')); - expect('ab4_'); - c.update(4, {$set: {eek: 5}}); - expect('cb4_'); - handle.stop(); - - // Test observe with reactive: false. - handle = c.find({tags: "flower"}, {reactive: false}).observe(makecb('c')); - expect('ac4_ac5_'); - // This insert shouldn't trigger a callback because it's not reactive. - c.insert({_id: 6, name: "river", tags: ["flower"]}); - expect(''); - handle.stop(); - }); -}); - - -Tinytest.add("minimongo - saveOriginals", function (test) { - // set up some data - var c = new LocalCollection(), - count; - c.insert({_id: 'foo', x: 'untouched'}); - c.insert({_id: 'bar', x: 'updateme'}); - c.insert({_id: 'baz', x: 'updateme'}); - c.insert({_id: 'quux', y: 'removeme'}); - c.insert({_id: 'whoa', y: 'removeme'}); - - // Save originals and make some changes. - c.saveOriginals(); - c.insert({_id: "hooray", z: 'insertme'}); - c.remove({y: 'removeme'}); - count = c.update({x: 'updateme'}, {$set: {z: 5}}, {multi: true}); - c.update('bar', {$set: {k: 7}}); // update same doc twice - - // Verify returned count is correct - test.equal(count, 2); - - // Verify the originals. - var originals = c.retrieveOriginals(); - var affected = ['bar', 'baz', 'quux', 'whoa', 'hooray']; - test.equal(originals.size(), affected.length); - affected.forEach(function (id) { - test.isTrue(originals.has(id)); - }); - test.equal(originals.get('bar'), {_id: 'bar', x: 'updateme'}); - test.equal(originals.get('baz'), {_id: 'baz', x: 'updateme'}); - test.equal(originals.get('quux'), {_id: 'quux', y: 'removeme'}); - test.equal(originals.get('whoa'), {_id: 'whoa', y: 'removeme'}); - test.equal(originals.get('hooray'), undefined); - - // Verify that changes actually occured. - test.equal(c.find().count(), 4); - test.equal(c.findOne('foo'), {_id: 'foo', x: 'untouched'}); - test.equal(c.findOne('bar'), {_id: 'bar', x: 'updateme', z: 5, k: 7}); - test.equal(c.findOne('baz'), {_id: 'baz', x: 'updateme', z: 5}); - test.equal(c.findOne('hooray'), {_id: 'hooray', z: 'insertme'}); - - // The next call doesn't get the same originals again. - c.saveOriginals(); - originals = c.retrieveOriginals(); - test.isTrue(originals); - test.isTrue(originals.empty()); - - // Insert and remove a document during the period. - c.saveOriginals(); - c.insert({_id: 'temp', q: 8}); - c.remove('temp'); - originals = c.retrieveOriginals(); - test.equal(originals.size(), 1); - test.isTrue(originals.has('temp')); - test.equal(originals.get('temp'), undefined); -}); - -Tinytest.add("minimongo - saveOriginals errors", function (test) { - var c = new LocalCollection(); - // Can't call retrieve before save. - test.throws(function () { c.retrieveOriginals(); }); - c.saveOriginals(); - // Can't call save twice. - test.throws(function () { c.saveOriginals(); }); -}); - -Tinytest.add("minimongo - objectid transformation", function (test) { - var testId = function (item) { - test.equal(item, MongoID.idParse(MongoID.idStringify(item))); - }; - var randomOid = new MongoID.ObjectID(); - testId(randomOid); - testId("FOO"); - testId("ffffffffffff"); - testId("0987654321abcdef09876543"); - testId(new MongoID.ObjectID()); - testId("--a string"); - - test.equal("ffffffffffff", MongoID.idParse(MongoID.idStringify("ffffffffffff"))); -}); - - -Tinytest.add("minimongo - objectid", function (test) { - var randomOid = new MongoID.ObjectID(); - var anotherRandomOid = new MongoID.ObjectID(); - test.notEqual(randomOid, anotherRandomOid); - test.throws(function() { new MongoID.ObjectID("qqqqqqqqqqqqqqqqqqqqqqqq");}); - test.throws(function() { new MongoID.ObjectID("ABCDEF"); }); - test.equal(randomOid, new MongoID.ObjectID(randomOid.valueOf())); -}); - -Tinytest.add("minimongo - pause", function (test) { - var operations = []; - var cbs = log_callbacks(operations); - - var c = new LocalCollection(); - var h = c.find({}).observe(cbs); - - // remove and add cancel out. - c.insert({_id: 1, a: 1}); - test.equal(operations.shift(), ['added', {a:1}, 0, null]); - - c.pauseObservers(); - - c.remove({_id: 1}); - test.length(operations, 0); - c.insert({_id: 1, a: 1}); - test.length(operations, 0); - - c.resumeObservers(); - test.length(operations, 0); - - - // two modifications become one - c.pauseObservers(); - - c.update({_id: 1}, {a: 2}); - c.update({_id: 1}, {a: 3}); - - c.resumeObservers(); - test.equal(operations.shift(), ['changed', {a:3}, 0, {a:1}]); - test.length(operations, 0); - - // test special case for remove({}) - c.pauseObservers(); - test.equal(c.remove({}), 1); - test.length(operations, 0); - c.resumeObservers(); - test.equal(operations.shift(), ['removed', 1, 0, {a:3}]); - test.length(operations, 0); - - h.stop(); -}); - -Tinytest.add("minimongo - ids matched by selector", function (test) { - var check = function (selector, ids) { - var idsFromSelector = LocalCollection._idsMatchedBySelector(selector); - // XXX normalize order, in a way that also works for ObjectIDs? - test.equal(idsFromSelector, ids); - }; - check("foo", ["foo"]); - check({_id: "foo"}, ["foo"]); + // Ensure that ObjectIDs work (even if the _ids in question are not ===-equal) var oid1 = new MongoID.ObjectID(); - check(oid1, [oid1]); - check({_id: oid1}, [oid1]); - check({_id: "foo", x: 42}, ["foo"]); - check({}, null); - check({_id: {$in: ["foo", oid1]}}, ["foo", oid1]); - check({_id: {$ne: "foo"}}, null); - // not actually valid, but works for now... - check({$and: ["foo"]}, ["foo"]); - check({$and: [{x: 42}, {_id: oid1}]}, [oid1]); - check({$and: [{x: 42}, {_id: {$in: [oid1]}}]}, [oid1]); -}); + var oid2 = new MongoID.ObjectID(oid1.toHexString()); + test.equal(wrap(function () {return {_id: oid2};})({_id: oid1}), + {_id: oid2}); -Tinytest.add("minimongo - reactive stop", function (test) { - var coll = new LocalCollection(); - coll.insert({_id: 'A'}); - coll.insert({_id: 'B'}); - coll.insert({_id: 'C'}); - - var addBefore = function (str, newChar, before) { - var idx = str.indexOf(before); - if (idx === -1) - return str + newChar; - return str.slice(0, idx) + newChar + str.slice(idx); - }; - - var x, y; - var sortOrder = ReactiveVar(1); - - var c = Tracker.autorun(function () { - var q = coll.find({}, {sort: {_id: sortOrder.get()}}); - x = ""; - q.observe({ addedAt: function (doc, atIndex, before) { - x = addBefore(x, doc._id, before); - }}); - y = ""; - q.observeChanges({ addedBefore: function (id, fields, before) { - y = addBefore(y, id, before); - }}); - }); - - test.equal(x, "ABC"); - test.equal(y, "ABC"); - - sortOrder.set(-1); - test.equal(x, "ABC"); - test.equal(y, "ABC"); - Tracker.flush(); - test.equal(x, "CBA"); - test.equal(y, "CBA"); - - coll.insert({_id: 'D'}); - coll.insert({_id: 'E'}); - test.equal(x, "EDCBA"); - test.equal(y, "EDCBA"); - - c.stop(); - // stopping kills the observes immediately - coll.insert({_id: 'F'}); - test.equal(x, "EDCBA"); - test.equal(y, "EDCBA"); -}); - -Tinytest.add("minimongo - immediate invalidate", function (test) { - var coll = new LocalCollection(); - coll.insert({_id: 'A'}); - - // This has two separate findOnes. findOne() uses skip/limit, which means - // that its response to an update() call involves a recompute. We used to have - // a bug where we would first calculate all the calls that need to be - // recomputed, then recompute them one by one, without checking to see if the - // callbacks from recomputing one query stopped the second query, which - // crashed. - var c = Tracker.autorun(function () { - coll.findOne('A'); - coll.findOne('A'); - }); - - coll.update('A', {$set: {x: 42}}); - - c.stop(); -}); - - -Tinytest.add("minimongo - count on cursor with limit", function(test){ - var coll = new LocalCollection(), count; - - coll.insert({_id: 'A'}); - coll.insert({_id: 'B'}); - coll.insert({_id: 'C'}); - coll.insert({_id: 'D'}); - - var c = Tracker.autorun(function (c) { - var cursor = coll.find({_id: {$exists: true}}, {sort: {_id: 1}, limit: 3}); - count = cursor.count(); - }); - - test.equal(count, 3); - - coll.remove('A'); // still 3 in the collection - Tracker.flush(); - test.equal(count, 3); - - coll.remove('B'); // expect count now 2 - Tracker.flush(); - test.equal(count, 2); - - - coll.insert({_id: 'A'}); // now 3 again - Tracker.flush(); - test.equal(count, 3); - - coll.insert({_id: 'B'}); // now 4 entries, but count should be 3 still - Tracker.flush(); - test.equal(count, 3); - - c.stop(); -}); - -Tinytest.add("minimongo - reactive count with cached cursor", function (test) { - var coll = new LocalCollection; - var cursor = coll.find({}); - var firstAutorunCount, secondAutorunCount; - Tracker.autorun(function(){ - firstAutorunCount = cursor.count(); - }); - Tracker.autorun(function(){ - secondAutorunCount = coll.find({}).count(); - }); - test.equal(firstAutorunCount, 0); - test.equal(secondAutorunCount, 0); - coll.insert({i: 1}); - coll.insert({i: 2}); - coll.insert({i: 3}); - Tracker.flush(); - test.equal(firstAutorunCount, 3); - test.equal(secondAutorunCount, 3); -}); - -Tinytest.add("minimongo - $near operator tests", function (test) { - var coll = new LocalCollection(); - coll.insert({ rest: { loc: [2, 3] } }); - coll.insert({ rest: { loc: [-3, 3] } }); - coll.insert({ rest: { loc: [5, 5] } }); - - test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 30 } }).count(), 3); - test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 1); - var points = coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 6 } }).fetch(); - points.forEach(function (point, i, points) { - test.isTrue(!i || distance([0, 0], point.rest.loc) >= distance([0, 0], points[i - 1].rest.loc)); - }); - - function distance(a, b) { - var x = a[0] - b[0]; - var y = a[1] - b[1]; - return Math.sqrt(x * x + y * y); - } - - // GeoJSON tests - coll = new LocalCollection(); - var data = [{ "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] } }, - { "category" : "WEAPON LAWS", "descript" : "POSS OF PROHIBITED WEAPON", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] } }, - { "category" : "LARCENY/THEFT", "descript" : "GRAND THEFT OF PROPERTY", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.41538270191, 37.774683628213 ] } }, - { "category" : "LARCENY/THEFT", "descript" : "PETTY THEFT FROM LOCKED AUTO", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415396041221, 37.7747879744156 ] } }, - { "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879734156 ] } } + // transform functions must return objects + var invalidObjects = [ + "asdf", new MongoID.ObjectID(), false, null, true, + 27, [123], /adsf/, new Date, function () {}, undefined ]; - - data.forEach(function (x, i) { coll.insert(Object.assign(x, { x: i })); }); - - var close15 = coll.find({ location: { $near: { - $geometry: { type: "Point", - coordinates: [-122.4154282, 37.7746115] }, - $maxDistance: 15 } } }).fetch(); - test.length(close15, 1); - test.equal(close15[0].descript, "GRAND THEFT OF PROPERTY"); - - var close20 = coll.find({ location: { $near: { - $geometry: { type: "Point", - coordinates: [-122.4154282, 37.7746115] }, - $maxDistance: 20 } } }).fetch(); - test.length(close20, 4); - test.equal(close20[0].descript, "GRAND THEFT OF PROPERTY"); - test.equal(close20[1].descript, "PETTY THEFT FROM LOCKED AUTO"); - test.equal(close20[2].descript, "POSSESSION OF BURGLARY TOOLS"); - test.equal(close20[3].descript, "POSS OF PROHIBITED WEAPON"); - - // Any combinations of $near with $or/$and/$nor/$not should throw an error - test.throws(function () { - coll.find({ location: { - $not: { - $near: { - $geometry: { - type: "Point", - coordinates: [-122.4154282, 37.7746115] - }, $maxDistance: 20 } } } }); - }); - test.throws(function () { - coll.find({ - $and: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, - { x: 0 }] + invalidObjects.forEach(function (invalidObject) { + var wrapped = wrap(function () { return invalidObject; }); + test.throws(function () { + wrapped({_id: "asdf"}); }); - }); - test.throws(function () { - coll.find({ - $or: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, - { x: 0 }] - }); - }); - test.throws(function () { - coll.find({ - $nor: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 1 }}}, - { x: 0 }] - }); - }); - test.throws(function () { - coll.find({ - $and: [{ - $and: [{ - location: { - $near: { - $geometry: { - type: "Point", - coordinates: [-122.4154282, 37.7746115] - }, - $maxDistance: 1 - } - } - }] - }] - }); - }); + }, /transform must return object/); - // array tests - coll = new LocalCollection(); - coll.insert({ - _id: "x", - k: 9, - a: [ - {b: [ - [100, 100], - [1, 1]]}, - {b: [150, 150]}]}); - coll.insert({ - _id: "y", - k: 9, - a: {b: [5, 5]}}); - var testNear = function (near, md, expected) { - test.equal( - coll.find({'a.b': {$near: near, $maxDistance: md}}).fetch().map(function (doc) { return doc._id }), - expected); + // transform functions may not change _ids + var wrapped = wrap(function (doc) { doc._id = 'x'; return doc; }); + test.throws(function () { + wrapped({_id: 'y'}); + }, /can't have different _id/); + + // transform functions may remove _ids + test.equal({_id: 'a', x: 2}, + wrap(function (d) {delete d._id; return d;})({_id: 'a', x: 2})); + + // test that wrapped transform functions are nonreactive + var unwrapped = function (doc) { + test.isFalse(Tracker.active); + return doc; }; - testNear([149, 149], 4, ['x']); - testNear([149, 149], 1000, ['x', 'y']); - // It's important that we figure out that 'x' is closer than 'y' to [2,2] even - // though the first within-1000 point in 'x' (ie, [100,100]) is farther than - // 'y'. - testNear([2, 2], 1000, ['x', 'y']); - - // issue #3599 - // Ensure that distance is not used as a tie-breaker for sort. - test.equal( - coll.find({'a.b': {$near: [1, 1]}}, {sort: {k: 1}}).fetch().map(function (doc) { return doc._id; }), - ['x', 'y']); - test.equal( - coll.find({'a.b': {$near: [5, 5]}}, {sort: {k: 1}}).fetch().map(function (doc) { return doc._id; }), - ['x', 'y']); - - var operations = []; - var cbs = log_callbacks(operations); - var handle = coll.find({'a.b': {$near: [7,7]}}).observe(cbs); - - test.length(operations, 2); - test.equal(operations.shift(), ['added', {k:9, a:{b:[5,5]}}, 0, null]); - test.equal(operations.shift(), - ['added', {k: 9, a:[{b:[[100,100],[1,1]]},{b:[150,150]}]}, - 1, null]); - // This needs to be inserted in the MIDDLE of the two existing ones. - coll.insert({a: {b: [3,3]}}); - test.length(operations, 1); - test.equal(operations.shift(), ['added', {a: {b: [3, 3]}}, 1, 'x']); - + var handle = Tracker.autorun(function () { + test.isTrue(Tracker.active); + wrap(unwrapped)({_id: "xxx"}); + }); handle.stop(); }); - -// issue #2077 -Tinytest.add("minimongo - $near and $geometry for legacy coordinates", function(test){ - var coll = new LocalCollection(); - - coll.insert({ - loc: { - x: 1, - y: 1 - } - }); - coll.insert({ - loc: [-1,-1] - }); - coll.insert({ - loc: [40,-10] - }); - coll.insert({ - loc: { - x: -10, - y: 40 - } - }); - - test.equal(coll.find({ 'loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 2); - test.equal(coll.find({ 'loc': { $near: {$geometry: {type: "Point", coordinates: [0, 0]}}} }).count(), 4); - test.equal(coll.find({ 'loc': { $near: {$geometry: {type: "Point", coordinates: [0, 0]}, $maxDistance:200000}}}).count(), 2); - -}); - -// Regression test for #4377. Previously, "replace" updates didn't clone the -// argument. -Tinytest.add("minimongo - update should clone", function (test) { - var x = []; - var coll = new LocalCollection; - var id = coll.insert({}); - coll.update(id, {x: x}); - x.push(1); - test.equal(coll.findOne(id), {_id: id, x: []}); -}); - -// See #2275. -Tinytest.add("minimongo - fetch in observe", function (test) { - var coll = new LocalCollection; - var callbackInvoked = false; - var observe = coll.find().observeChanges({ - added: function (id, fields) { - callbackInvoked = true; - test.equal(fields, {foo: 1}); - var doc = coll.findOne({foo: 1}); - test.isTrue(doc); - test.equal(doc.foo, 1); - } - }); - test.isFalse(callbackInvoked); - var computation = Tracker.autorun(function (computation) { - if (computation.firstRun) { - coll.insert({foo: 1}); - } - }); - test.isTrue(callbackInvoked); - observe.stop(); - computation.stop(); -}); - -// See #2254 -Tinytest.add("minimongo - fine-grained reactivity of observe with fields projection", function (test) { - var X = new LocalCollection; - var id = "asdf"; - X.insert({_id: id, foo: {bar: 123}}); - - var callbackInvoked = false; - var obs = X.find(id, {fields: {'foo.bar': 1}}).observeChanges({ - changed: function (id, fields) { - callbackInvoked = true; - } - }); - - test.isFalse(callbackInvoked); - X.update(id, {$set: {'foo.baz': 456}}); - test.isFalse(callbackInvoked); - - obs.stop(); -}); -Tinytest.add("minimongo - fine-grained reactivity of query with fields projection", function (test) { - var X = new LocalCollection; - var id = "asdf"; - X.insert({_id: id, foo: {bar: 123}}); - - var callbackInvoked = false; - var computation = Tracker.autorun(function () { - callbackInvoked = true; - return X.findOne(id, { fields: { 'foo.bar': 1 } }); - }); - test.isTrue(callbackInvoked); - callbackInvoked = false; - X.update(id, {$set: {'foo.baz': 456}}); - test.isFalse(callbackInvoked); - X.update(id, {$set: {'foo.bar': 124}}); - Tracker.flush(); - test.isTrue(callbackInvoked); - - computation.stop(); -}); - -// Tests that the logic in `LocalCollection.prototype.update` -// correctly deals with count() on a cursor with skip or limit (since -// then the result set is an IdMap, not an array) -Tinytest.add("minimongo - reactive skip/limit count while updating", function(test) { - var X = new LocalCollection; - var count = -1; - - var c = Tracker.autorun(function() { - count = X.find({}, {skip: 1, limit: 1}).count(); - }); - - test.equal(count, 0); - - X.insert({}); - Tracker.flush({_throwFirstError: true}); - test.equal(count, 0); - - X.insert({}); - Tracker.flush({_throwFirstError: true}); - test.equal(count, 1); - - X.update({}, {$set: {foo: 1}}); - Tracker.flush({_throwFirstError: true}); - test.equal(count, 1); - - // Make sure a second update also works - X.update({}, {$set: {foo: 2}}); - Tracker.flush({_throwFirstError: true}); - test.equal(count, 1); - - c.stop(); -}); - -// Makes sure inserts cannot be performed using field names that have -// Mongo restricted characters in them ('.', '$', '\0'): -// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot insert using invalid field names", function (test) { - const collection = new LocalCollection(); - - // Quick test to make sure non-dot field inserts are working - collection.insert({ a: 'b' }); - - // Quick test to make sure field values with dots are allowed - collection.insert({ a: 'b.c' }); - - // Verify top level dot-field inserts are prohibited - ['a.b', '.b', 'a.', 'a.b.c'].forEach((field) => { - test.throws(function () { - collection.insert({ [field]: 'c' }); - }, `Key ${field} must not contain '.'`); - }); - - // Verify nested dot-field inserts are prohibited - test.throws(function () { - collection.insert({ a: { b: { 'c.d': 'e' } } }); - }, "Key c.d must not contain '.'"); - - // Verify field names starting with $ are prohibited - test.throws(function () { - collection.insert({ '$a': 'b' }); - }, "Key $a must not start with '$'"); - - // Verify nested field names starting with $ are prohibited - test.throws(function () { - collection.insert({ a: { b: { '$c': 'd' } } }); - }, "Key $c must not start with '$'"); - - // Verify top level fields with null characters are prohibited - ['\0a', 'a\0', 'a\0b', '\u0000a', 'a\u0000', 'a\u0000b'].forEach((field) => { - test.throws(function () { - collection.insert({ [field]: 'c' }); - }, `Key ${field} must not contain null bytes`); - }); - - // Verify nested field names with null characters are prohibited - test.throws(function () { - collection.insert({ a: { b: { '\0c': 'd' } } }); - }, 'Key \0c must not contain null bytes'); -}); - -// Makes sure $set's cannot be performed using null bytes -// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot $set with null bytes", function (test) { - const collection = new LocalCollection(); - - // Quick test to make sure non-null byte $set's are working - const id = collection.insert({ a: 'b', 'c': 'd' }); - collection.update({ _id: id }, { $set: { e: 'f' } }); - - // Verify $set's with null bytes throw an exception - test.throws(() => { - collection.update({ _id: id }, { $set: { '\0a': 'b' } }); - }, 'Key \0a must not contain null bytes'); -}); - -// Makes sure $rename's cannot be performed using null bytes -// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot $rename with null bytes", function (test) { - const collection = new LocalCollection(); - - // Quick test to make sure non-null byte $rename's are working - let id = collection.insert({ a: 'b', c: 'd' }); - collection.update({ _id: id }, { $rename: { a: 'a1', c: 'c1' } }); - - // Verify $rename's with null bytes throw an exception - collection.remove({}); - id = collection.insert({ a: 'b', c: 'd' }); - test.throws(() => { - collection.update({ _id: id }, { $rename: { a: '\0a', c: 'c\0' } }); - }, "The 'to' field for $rename cannot contain an embedded null byte"); -}); diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js new file mode 100644 index 0000000000..8bc09c95d0 --- /dev/null +++ b/packages/minimongo/minimongo_tests_client.js @@ -0,0 +1,3735 @@ + +// Hack to make LocalCollection generate ObjectIDs by default. +LocalCollection._useOID = true; + +// assert that f is a strcmp-style comparison function that puts +// 'values' in the provided order + +var assert_ordering = function (test, f, values) { + for (var i = 0; i < values.length; i++) { + var x = f(values[i], values[i]); + if (x !== 0) { + // XXX super janky + test.fail({type: "minimongo-ordering", + message: "value doesn't order as equal to itself", + value: JSON.stringify(values[i]), + should_be_zero_but_got: JSON.stringify(x)}); + } + if (i + 1 < values.length) { + var less = values[i]; + var more = values[i + 1]; + var x = f(less, more); + if (!(x < 0)) { + // XXX super janky + test.fail({type: "minimongo-ordering", + message: "ordering test failed", + first: JSON.stringify(less), + second: JSON.stringify(more), + should_be_negative_but_got: JSON.stringify(x)}); + } + x = f(more, less); + if (!(x > 0)) { + // XXX super janky + test.fail({type: "minimongo-ordering", + message: "ordering test failed", + first: JSON.stringify(less), + second: JSON.stringify(more), + should_be_positive_but_got: JSON.stringify(x)}); + } + } + } +}; + +var log_callbacks = function (operations) { + return { + addedAt: function (obj, idx, before) { + delete obj._id; + operations.push(EJSON.clone(['added', obj, idx, before])); + }, + changedAt: function (obj, old_obj, at) { + delete obj._id; + delete old_obj._id; + operations.push(EJSON.clone(['changed', obj, at, old_obj])); + }, + movedTo: function (obj, old_at, new_at, before) { + delete obj._id; + operations.push(EJSON.clone(['moved', obj, old_at, new_at, before])); + }, + removedAt: function (old_obj, at) { + var id = old_obj._id; + delete old_obj._id; + operations.push(EJSON.clone(['removed', id, at, old_obj])); + } + }; +}; + +// XXX test shared structure in all MM entrypoints +Tinytest.add("minimongo - basics", function (test) { + var c = new LocalCollection(), + fluffyKitten_id, + count; + + fluffyKitten_id = c.insert({type: "kitten", name: "fluffy"}); + c.insert({type: "kitten", name: "snookums"}); + c.insert({type: "cryptographer", name: "alice"}); + c.insert({type: "cryptographer", name: "bob"}); + c.insert({type: "cryptographer", name: "cara"}); + test.equal(c.find().count(), 5); + test.equal(c.find({type: "kitten"}).count(), 2); + test.equal(c.find({type: "cryptographer"}).count(), 3); + test.length(c.find({type: "kitten"}).fetch(), 2); + test.length(c.find({type: "cryptographer"}).fetch(), 3); + test.equal(fluffyKitten_id, c.findOne({type: "kitten", name: "fluffy"})._id); + + c.remove({name: "cara"}); + test.equal(c.find().count(), 4); + test.equal(c.find({type: "kitten"}).count(), 2); + test.equal(c.find({type: "cryptographer"}).count(), 2); + test.length(c.find({type: "kitten"}).fetch(), 2); + test.length(c.find({type: "cryptographer"}).fetch(), 2); + + count = c.update({name: "snookums"}, {$set: {type: "cryptographer"}}); + test.equal(count, 1); + test.equal(c.find().count(), 4); + test.equal(c.find({type: "kitten"}).count(), 1); + test.equal(c.find({type: "cryptographer"}).count(), 3); + test.length(c.find({type: "kitten"}).fetch(), 1); + test.length(c.find({type: "cryptographer"}).fetch(), 3); + + c.remove(null); + c.remove(false); + c.remove(undefined); + test.equal(c.find().count(), 4); + + c.remove({_id: null}); + c.remove({_id: false}); + c.remove({_id: undefined}); + count = c.remove(); + test.equal(count, 0); + test.equal(c.find().count(), 4); + + count = c.remove({}); + test.equal(count, 4); + test.equal(c.find().count(), 0); + + c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); + c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); + c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); + + test.equal(c.find({tags: "flower"}).count(), 1); + test.equal(c.find({tags: "fruit"}).count(), 2); + test.equal(c.find({tags: "red"}).count(), 3); + test.length(c.find({tags: "flower"}).fetch(), 1); + test.length(c.find({tags: "fruit"}).fetch(), 2); + test.length(c.find({tags: "red"}).fetch(), 3); + + test.equal(c.findOne(1).name, "strawberry"); + test.equal(c.findOne(2).name, "apple"); + test.equal(c.findOne(3).name, "rose"); + test.equal(c.findOne(4), undefined); + test.equal(c.findOne("abc"), undefined); + test.equal(c.findOne(undefined), undefined); + + test.equal(c.find(1).count(), 1); + test.equal(c.find(4).count(), 0); + test.equal(c.find("abc").count(), 0); + test.equal(c.find(undefined).count(), 0); + test.equal(c.find().count(), 3); + test.equal(c.find(1, {skip: 1}).count(), 0); + test.equal(c.find({_id: 1}, {skip: 1}).count(), 0); + test.equal(c.find({}, {skip: 1}).count(), 2); + test.equal(c.find({}, {skip: 2}).count(), 1); + test.equal(c.find({}, {limit: 2}).count(), 2); + test.equal(c.find({}, {limit: 1}).count(), 1); + test.equal(c.find({}, {skip: 1, limit: 1}).count(), 1); + test.equal(c.find({tags: "fruit"}, {skip: 1}).count(), 1); + test.equal(c.find({tags: "fruit"}, {limit: 1}).count(), 1); + test.equal(c.find({tags: "fruit"}, {skip: 1, limit: 1}).count(), 1); + test.equal(c.find(1, {sort: ['_id','desc'], skip: 1}).count(), 0); + test.equal(c.find({_id: 1}, {sort: ['_id','desc'], skip: 1}).count(), 0); + test.equal(c.find({}, {sort: ['_id','desc'], skip: 1}).count(), 2); + test.equal(c.find({}, {sort: ['_id','desc'], skip: 2}).count(), 1); + test.equal(c.find({}, {sort: ['_id','desc'], limit: 2}).count(), 2); + test.equal(c.find({}, {sort: ['_id','desc'], limit: 1}).count(), 1); + test.equal(c.find({}, {sort: ['_id','desc'], skip: 1, limit: 1}).count(), 1); + test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], skip: 1}).count(), 1); + test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], limit: 1}).count(), 1); + test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], skip: 1, limit: 1}).count(), 1); + + // Regression test for #455. + c.insert({foo: {bar: 'baz'}}); + test.equal(c.find({foo: {bam: 'baz'}}).count(), 0); + test.equal(c.find({foo: {bar: 'baz'}}).count(), 1); + +}); + +Tinytest.add("minimongo - error - no options", function (test) { + try { + throw MinimongoError("Not fun to have errors"); + } catch (e) { + test.equal(e.message, "Not fun to have errors"); + } +}); + +Tinytest.add("minimongo - error - with field", function (test) { + try { + throw MinimongoError("Cats are no fun", { field: "mice" }); + } catch (e) { + test.equal(e.message, "Cats are no fun for field 'mice'"); + } +}); + +Tinytest.add("minimongo - cursors", function (test) { + var c = new LocalCollection(); + var res; + + for (var i = 0; i < 20; i++) + c.insert({i: i}); + + var q = c.find(); + test.equal(q.count(), 20); + + // fetch + res = q.fetch(); + test.length(res, 20); + for (var i = 0; i < 20; i++) { + test.equal(res[i].i, i); + } + // call it again, it still works + test.length(q.fetch(), 20); + + // forEach + var count = 0; + var context = {}; + q.forEach(function (obj, i, cursor) { + test.equal(obj.i, count++); + test.equal(obj.i, i); + test.isTrue(context === this); + test.isTrue(cursor === q); + }, context); + test.equal(count, 20); + // call it again, it still works + test.length(q.fetch(), 20); + + // map + res = q.map(function (obj, i, cursor) { + test.equal(obj.i, i); + test.isTrue(context === this); + test.isTrue(cursor === q); + return obj.i * 2; + }, context); + test.length(res, 20); + for (var i = 0; i < 20; i++) + test.equal(res[i], i * 2); + // call it again, it still works + test.length(q.fetch(), 20); + + // findOne (and no rewind first) + test.equal(c.findOne({i: 0}).i, 0); + test.equal(c.findOne({i: 1}).i, 1); + var id = c.findOne({i: 2})._id; + test.equal(c.findOne(id).i, 2); +}); + +Tinytest.add("minimongo - transform", function (test) { + var c = new LocalCollection; + c.insert({}); + // transform functions must return objects + var invalidTransform = function (doc) { return doc._id; }; + test.throws(function () { + c.findOne({}, {transform: invalidTransform}); + }); + + // transformed documents get _id field transplanted if not present + var transformWithoutId = function (doc) { + var docWithoutId = Object.assign({}, doc); + delete docWithoutId._id; + return docWithoutId; + }; + test.equal(c.findOne({}, {transform: transformWithoutId})._id, + c.findOne()._id); +}); + +Tinytest.add("minimongo - misc", function (test) { + // deepcopy + var a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]}, + f: null, g: new Date()}; + var b = EJSON.clone(a); + test.equal(a, b); + test.isTrue(LocalCollection._f._equal(a, b)); + a.a.push(4); + test.length(b.a, 3); + a.c = false; + test.isTrue(b.c); + b.d.z = 15; + a.d.z = 14; + test.equal(b.d.z, 15); + a.d.y.push(88); + test.length(b.d.y, 1); + test.equal(a.g, b.g); + b.g.setDate(b.g.getDate() + 1); + test.notEqual(a.g, b.g); + + a = {x: function () {}}; + b = EJSON.clone(a); + a.x.a = 14; + test.equal(b.x.a, 14); // just to document current behavior +}); + +Tinytest.add("minimongo - lookup", function (test) { + var lookupA = MinimongoTest.makeLookupFunction('a'); + test.equal(lookupA({}), [{value: undefined}]); + test.equal(lookupA({a: 1}), [{value: 1}]); + test.equal(lookupA({a: [1]}), [{value: [1]}]); + + var lookupAX = MinimongoTest.makeLookupFunction('a.x'); + test.equal(lookupAX({a: {x: 1}}), [{value: 1}]); + test.equal(lookupAX({a: {x: [1]}}), [{value: [1]}]); + test.equal(lookupAX({a: 5}), [{value: undefined}]); + test.equal(lookupAX({a: [{x: 1}, {x: [2]}, {y: 3}]}), + [{value: 1, arrayIndices: [0]}, + {value: [2], arrayIndices: [1]}, + {value: undefined, arrayIndices: [2]}]); + + var lookupA0X = MinimongoTest.makeLookupFunction('a.0.x'); + test.equal(lookupA0X({a: [{x: 1}]}), [ + // From interpreting '0' as "0th array element". + {value: 1, arrayIndices: [0, 'x']}, + // From interpreting '0' as "after branching in the array, look in the + // object {x:1} for a field named 0". + {value: undefined, arrayIndices: [0]}]); + test.equal(lookupA0X({a: [{x: [1]}]}), [ + {value: [1], arrayIndices: [0, 'x']}, + {value: undefined, arrayIndices: [0]}]); + test.equal(lookupA0X({a: 5}), [{value: undefined}]); + test.equal(lookupA0X({a: [{x: 1}, {x: [2]}, {y: 3}]}), [ + // From interpreting '0' as "0th array element". + {value: 1, arrayIndices: [0, 'x']}, + // From interpreting '0' as "after branching in the array, look in the + // object {x:1} for a field named 0". + {value: undefined, arrayIndices: [0]}, + {value: undefined, arrayIndices: [1]}, + {value: undefined, arrayIndices: [2]} + ]); + + test.equal( + MinimongoTest.makeLookupFunction('w.x.0.z')({ + w: [{x: [{z: 5}]}]}), [ + // From interpreting '0' as "0th array element". + {value: 5, arrayIndices: [0, 0, 'x']}, + // From interpreting '0' as "after branching in the array, look in the + // object {z:5} for a field named "0". + {value: undefined, arrayIndices: [0, 0]} + ]); +}); + +Tinytest.add("minimongo - selector_compiler", function (test) { + var matches = function (shouldMatch, selector, doc) { + var doesMatch = new Minimongo.Matcher(selector).documentMatches(doc).result; + if (doesMatch != shouldMatch) { + // XXX super janky + test.fail({message: "minimongo match failure: document " + + (shouldMatch ? "should match, but doesn't" : + "shouldn't match, but does"), + selector: JSON.stringify(selector), + document: JSON.stringify(doc) + }); + } + }; + + var match = matches.bind(null, true); + var nomatch = matches.bind(null, false); + + // XXX blog post about what I learned while writing these tests (weird + // mongo edge cases) + + // empty selectors + match({}, {}); + match({}, {a: 12}); + + // scalars + match(1, {_id: 1, a: 'foo'}); + nomatch(1, {_id: 2, a: 'foo'}); + match('a', {_id: 'a', a: 'foo'}); + nomatch('a', {_id: 'b', a: 'foo'}); + + // safety + nomatch(undefined, {}); + nomatch(undefined, {_id: 'foo'}); + nomatch(false, {_id: 'foo'}); + nomatch(null, {_id: 'foo'}); + nomatch({_id: undefined}, {_id: 'foo'}); + nomatch({_id: false}, {_id: 'foo'}); + nomatch({_id: null}, {_id: 'foo'}); + + // matching one or more keys + nomatch({a: 12}, {}); + match({a: 12}, {a: 12}); + match({a: 12}, {a: 12, b: 13}); + match({a: 12, b: 13}, {a: 12, b: 13}); + match({a: 12, b: 13}, {a: 12, b: 13, c: 14}); + nomatch({a: 12, b: 13, c: 14}, {a: 12, b: 13}); + nomatch({a: 12, b: 13}, {b: 13, c: 14}); + + match({a: 12}, {a: [12]}); + match({a: 12}, {a: [11, 12, 13]}); + nomatch({a: 12}, {a: [11, 13]}); + match({a: 12, b: 13}, {a: [11, 12, 13], b: [13, 14, 15]}); + nomatch({a: 12, b: 13}, {a: [11, 12, 13], b: [14, 15]}); + + // dates + var date1 = new Date; + var date2 = new Date(date1.getTime() + 1000); + match({a: date1}, {a: date1}); + nomatch({a: date1}, {a: date2}); + + + // arrays + match({a: [1,2]}, {a: [1, 2]}); + match({a: [1,2]}, {a: [[1, 2]]}); + match({a: [1,2]}, {a: [[3, 4], [1, 2]]}); + nomatch({a: [1,2]}, {a: [3, 4]}); + nomatch({a: [1,2]}, {a: [[[1, 2]]]}); + + // literal documents + match({a: {b: 12}}, {a: {b: 12}}); + nomatch({a: {b: 12, c: 13}}, {a: {b: 12}}); + nomatch({a: {b: 12}}, {a: {b: 12, c: 13}}); + match({a: {b: 12, c: 13}}, {a: {b: 12, c: 13}}); + nomatch({a: {b: 12, c: 13}}, {a: {c: 13, b: 12}}); // tested on mongodb + nomatch({a: {}}, {a: {b: 12}}); + nomatch({a: {b:12}}, {a: {}}); + match( + {a: {b: 12, c: [13, true, false, 2.2, "a", null, {d: 14}]}}, + {a: {b: 12, c: [13, true, false, 2.2, "a", null, {d: 14}]}}); + match({a: {b: 12}}, {a: {b: 12}, k: 99}); + + match({a: {b: 12}}, {a: [{b: 12}]}); + nomatch({a: {b: 12}}, {a: [[{b: 12}]]}); + match({a: {b: 12}}, {a: [{b: 11}, {b: 12}, {b: 13}]}); + nomatch({a: {b: 12}}, {a: [{b: 11}, {b: 12, c: 20}, {b: 13}]}); + nomatch({a: {b: 12, c: 20}}, {a: [{b: 11}, {b: 12}, {c: 20}]}); + match({a: {b: 12, c: 20}}, {a: [{b: 11}, {b: 12, c: 20}, {b: 13}]}); + + // null + match({a: null}, {a: null}); + match({a: null}, {b: 12}); + nomatch({a: null}, {a: 12}); + match({a: null}, {a: [1, 2, null, 3]}); // tested on mongodb + nomatch({a: null}, {a: [1, 2, {}, 3]}); // tested on mongodb + + // order comparisons: $lt, $gt, $lte, $gte + match({a: {$lt: 10}}, {a: 9}); + nomatch({a: {$lt: 10}}, {a: 10}); + nomatch({a: {$lt: 10}}, {a: 11}); + + match({a: {$gt: 10}}, {a: 11}); + nomatch({a: {$gt: 10}}, {a: 10}); + nomatch({a: {$gt: 10}}, {a: 9}); + + match({a: {$lte: 10}}, {a: 9}); + match({a: {$lte: 10}}, {a: 10}); + nomatch({a: {$lte: 10}}, {a: 11}); + + match({a: {$gte: 10}}, {a: 11}); + match({a: {$gte: 10}}, {a: 10}); + nomatch({a: {$gte: 10}}, {a: 9}); + + match({a: {$lt: 10}}, {a: [11, 9, 12]}); + nomatch({a: {$lt: 10}}, {a: [11, 12]}); + + // (there's a full suite of ordering test elsewhere) + nomatch({a: {$lt: "null"}}, {a: null}); + match({a: {$lt: {x: [2, 3, 4]}}}, {a: {x: [1, 3, 4]}}); + match({a: {$gt: {x: [2, 3, 4]}}}, {a: {x: [3, 3, 4]}}); + nomatch({a: {$gt: {x: [2, 3, 4]}}}, {a: {x: [1, 3, 4]}}); + nomatch({a: {$gt: {x: [2, 3, 4]}}}, {a: {x: [2, 3, 4]}}); + nomatch({a: {$lt: {x: [2, 3, 4]}}}, {a: {x: [2, 3, 4]}}); + match({a: {$gte: {x: [2, 3, 4]}}}, {a: {x: [2, 3, 4]}}); + match({a: {$lte: {x: [2, 3, 4]}}}, {a: {x: [2, 3, 4]}}); + + nomatch({a: {$gt: [2, 3]}}, {a: [1, 2]}); // tested against mongodb + + // composition of two qualifiers + nomatch({a: {$lt: 11, $gt: 9}}, {a: 8}); + nomatch({a: {$lt: 11, $gt: 9}}, {a: 9}); + match({a: {$lt: 11, $gt: 9}}, {a: 10}); + nomatch({a: {$lt: 11, $gt: 9}}, {a: 11}); + nomatch({a: {$lt: 11, $gt: 9}}, {a: 12}); + + match({a: {$lt: 11, $gt: 9}}, {a: [8, 9, 10, 11, 12]}); + match({a: {$lt: 11, $gt: 9}}, {a: [8, 9, 11, 12]}); // tested against mongodb + + // $all + match({a: {$all: [1, 2]}}, {a: [1, 2]}); + nomatch({a: {$all: [1, 2, 3]}}, {a: [1, 2]}); + match({a: {$all: [1, 2]}}, {a: [3, 2, 1]}); + match({a: {$all: [1, "x"]}}, {a: [3, "x", 1]}); + nomatch({a: {$all: ['2']}}, {a: 2}); + nomatch({a: {$all: [2]}}, {a: '2'}); + match({a: {$all: [[1, 2], [1, 3]]}}, {a: [[1, 3], [1, 2], [1, 4]]}); + nomatch({a: {$all: [[1, 2], [1, 3]]}}, {a: [[1, 4], [1, 2], [1, 4]]}); + match({a: {$all: [2, 2]}}, {a: [2]}); // tested against mongodb + nomatch({a: {$all: [2, 3]}}, {a: [2, 2]}); + + nomatch({a: {$all: [1, 2]}}, {a: [[1, 2]]}); // tested against mongodb + nomatch({a: {$all: [1, 2]}}, {}); // tested against mongodb, field doesn't exist + nomatch({a: {$all: [1, 2]}}, {a: {foo: 'bar'}}); // tested against mongodb, field is not an object + nomatch({a: {$all: []}}, {a: []}); + nomatch({a: {$all: []}}, {a: [5]}); + match({a: {$all: [/i/, /e/i]}}, {a: ["foo", "bEr", "biz"]}); + nomatch({a: {$all: [/i/, /e/i]}}, {a: ["foo", "bar", "biz"]}); + match({a: {$all: [{b: 3}]}}, {a: [{b: 3}]}); + // Members of $all other than regexps are *equality matches*, not document + // matches. + nomatch({a: {$all: [{b: 3}]}}, {a: [{b: 3, k: 4}]}); + test.throws(function () { + match({a: {$all: [{$gt: 4}]}}, {}); + }); + + // $exists + match({a: {$exists: true}}, {a: 12}); + nomatch({a: {$exists: true}}, {b: 12}); + nomatch({a: {$exists: false}}, {a: 12}); + match({a: {$exists: false}}, {b: 12}); + + match({a: {$exists: true}}, {a: []}); + nomatch({a: {$exists: true}}, {b: []}); + nomatch({a: {$exists: false}}, {a: []}); + match({a: {$exists: false}}, {b: []}); + + match({a: {$exists: true}}, {a: [1]}); + nomatch({a: {$exists: true}}, {b: [1]}); + nomatch({a: {$exists: false}}, {a: [1]}); + match({a: {$exists: false}}, {b: [1]}); + + match({a: {$exists: 1}}, {a: 5}); + match({a: {$exists: 0}}, {b: 5}); + + nomatch({'a.x':{$exists: false}}, {a: [{}, {x: 5}]}); + match({'a.x':{$exists: true}}, {a: [{}, {x: 5}]}); + match({'a.x':{$exists: true}}, {a: [{}, {x: 5}]}); + match({'a.x':{$exists: true}}, {a: {x: []}}); + match({'a.x':{$exists: true}}, {a: {x: null}}); + + // $mod + match({a: {$mod: [10, 1]}}, {a: 11}); + nomatch({a: {$mod: [10, 1]}}, {a: 12}); + match({a: {$mod: [10, 1]}}, {a: [10, 11, 12]}); + nomatch({a: {$mod: [10, 1]}}, {a: [10, 12]}); + [ + 5, + [10], + [10, 1, 2], + "foo", + {bar: 1}, + [] + ].forEach(function (badMod) { + test.throws(function () { + match({a: {$mod: badMod}}, {a: 11}); + }); + }); + + // $eq + nomatch({a: {$eq: 1}}, {a: 2}); + match({a: {$eq: 2}}, {a: 2}); + nomatch({a: {$eq: [1]}}, {a: [2]}); + + match({a: {$eq: [1, 2]}}, {a: [1, 2]}); + match({a: {$eq: 1}}, {a: [1, 2]}); + match({a: {$eq: 2}}, {a: [1, 2]}); + nomatch({a: {$eq: 3}}, {a: [1, 2]}); + match({'a.b': {$eq: 1}}, {a: [{b: 1}, {b: 2}]}); + match({'a.b': {$eq: 2}}, {a: [{b: 1}, {b: 2}]}); + nomatch({'a.b': {$eq: 3}}, {a: [{b: 1}, {b: 2}]}); + + match({a: {$eq: {x: 1}}}, {a: {x: 1}}); + nomatch({a: {$eq: {x: 1}}}, {a: {x: 2}}); + nomatch({a: {$eq: {x: 1}}}, {a: {x: 1, y: 2}}); + + // $ne + match({a: {$ne: 1}}, {a: 2}); + nomatch({a: {$ne: 2}}, {a: 2}); + match({a: {$ne: [1]}}, {a: [2]}); + + nomatch({a: {$ne: [1, 2]}}, {a: [1, 2]}); // all tested against mongodb + nomatch({a: {$ne: 1}}, {a: [1, 2]}); + nomatch({a: {$ne: 2}}, {a: [1, 2]}); + match({a: {$ne: 3}}, {a: [1, 2]}); + nomatch({'a.b': {$ne: 1}}, {a: [{b: 1}, {b: 2}]}); + nomatch({'a.b': {$ne: 2}}, {a: [{b: 1}, {b: 2}]}); + match({'a.b': {$ne: 3}}, {a: [{b: 1}, {b: 2}]}); + + nomatch({a: {$ne: {x: 1}}}, {a: {x: 1}}); + match({a: {$ne: {x: 1}}}, {a: {x: 2}}); + match({a: {$ne: {x: 1}}}, {a: {x: 1, y: 2}}); + + // This query means: All 'a.b' must be non-5, and some 'a.b' must be >6. + match({'a.b': {$ne: 5, $gt: 6}}, {a: [{b: 2}, {b: 10}]}); + nomatch({'a.b': {$ne: 5, $gt: 6}}, {a: [{b: 2}, {b: 4}]}); + nomatch({'a.b': {$ne: 5, $gt: 6}}, {a: [{b: 2}, {b: 5}]}); + nomatch({'a.b': {$ne: 5, $gt: 6}}, {a: [{b: 10}, {b: 5}]}); + // Should work the same if the branch is at the bottom. + match({a: {$ne: 5, $gt: 6}}, {a: [2, 10]}); + nomatch({a: {$ne: 5, $gt: 6}}, {a: [2, 4]}); + nomatch({a: {$ne: 5, $gt: 6}}, {a: [2, 5]}); + nomatch({a: {$ne: 5, $gt: 6}}, {a: [10, 5]}); + + // $in + match({a: {$in: [1, 2, 3]}}, {a: 2}); + nomatch({a: {$in: [1, 2, 3]}}, {a: 4}); + match({a: {$in: [[1], [2], [3]]}}, {a: [2]}); + nomatch({a: {$in: [[1], [2], [3]]}}, {a: [4]}); + match({a: {$in: [{b: 1}, {b: 2}, {b: 3}]}}, {a: {b: 2}}); + nomatch({a: {$in: [{b: 1}, {b: 2}, {b: 3}]}}, {a: {b: 4}}); + + match({a: {$in: [1, 2, 3]}}, {a: [2]}); // tested against mongodb + match({a: {$in: [{x: 1}, {x: 2}, {x: 3}]}}, {a: [{x: 2}]}); + match({a: {$in: [1, 2, 3]}}, {a: [4, 2]}); + nomatch({a: {$in: [1, 2, 3]}}, {a: [4]}); + + match({a: {$in: ['x', /foo/i]}}, {a: 'x'}); + match({a: {$in: ['x', /foo/i]}}, {a: 'fOo'}); + match({a: {$in: ['x', /foo/i]}}, {a: ['f', 'fOo']}); + nomatch({a: {$in: ['x', /foo/i]}}, {a: ['f', 'fOx']}); + + match({a: {$in: [1, null]}}, {}); + match({'a.b': {$in: [1, null]}}, {}); + match({'a.b': {$in: [1, null]}}, {a: {}}); + match({'a.b': {$in: [1, null]}}, {a: {b: null}}); + nomatch({'a.b': {$in: [1, null]}}, {a: {b: 5}}); + nomatch({'a.b': {$in: [1]}}, {a: {b: null}}); + nomatch({'a.b': {$in: [1]}}, {a: {}}); + nomatch({'a.b': {$in: [1, null]}}, {a: [{b: 5}]}); + match({'a.b': {$in: [1, null]}}, {a: [{b: 5}, {}]}); + nomatch({'a.b': {$in: [1, null]}}, {a: [{b: 5}, []]}); + nomatch({'a.b': {$in: [1, null]}}, {a: [{b: 5}, 5]}); + + // $nin + nomatch({a: {$nin: [1, 2, 3]}}, {a: 2}); + match({a: {$nin: [1, 2, 3]}}, {a: 4}); + nomatch({a: {$nin: [[1], [2], [3]]}}, {a: [2]}); + match({a: {$nin: [[1], [2], [3]]}}, {a: [4]}); + nomatch({a: {$nin: [{b: 1}, {b: 2}, {b: 3}]}}, {a: {b: 2}}); + match({a: {$nin: [{b: 1}, {b: 2}, {b: 3}]}}, {a: {b: 4}}); + + nomatch({a: {$nin: [1, 2, 3]}}, {a: [2]}); // tested against mongodb + nomatch({a: {$nin: [{x: 1}, {x: 2}, {x: 3}]}}, {a: [{x: 2}]}); + nomatch({a: {$nin: [1, 2, 3]}}, {a: [4, 2]}); + nomatch({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}, {b:2}]}); + match({a: {$nin: [1, 2, 3]}}, {a: [4]}); + match({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}]}); + + nomatch({a: {$nin: ['x', /foo/i]}}, {a: 'x'}); + nomatch({a: {$nin: ['x', /foo/i]}}, {a: 'fOo'}); + nomatch({a: {$nin: ['x', /foo/i]}}, {a: ['f', 'fOo']}); + match({a: {$nin: ['x', /foo/i]}}, {a: ['f', 'fOx']}); + + nomatch({a: {$nin: [1, null]}}, {}); + nomatch({'a.b': {$nin: [1, null]}}, {}); + nomatch({'a.b': {$nin: [1, null]}}, {a: {}}); + nomatch({'a.b': {$nin: [1, null]}}, {a: {b: null}}); + match({'a.b': {$nin: [1, null]}}, {a: {b: 5}}); + match({'a.b': {$nin: [1]}}, {a: {b: null}}); + match({'a.b': {$nin: [1]}}, {a: {}}); + match({'a.b': {$nin: [1, null]}}, {a: [{b: 5}]}); + nomatch({'a.b': {$nin: [1, null]}}, {a: [{b: 5}, {}]}); + match({'a.b': {$nin: [1, null]}}, {a: [{b: 5}, []]}); + match({'a.b': {$nin: [1, null]}}, {a: [{b: 5}, 5]}); + + // $size + match({a: {$size: 0}}, {a: []}); + match({a: {$size: 1}}, {a: [2]}); + match({a: {$size: 2}}, {a: [2, 2]}); + nomatch({a: {$size: 0}}, {a: [2]}); + nomatch({a: {$size: 1}}, {a: []}); + nomatch({a: {$size: 1}}, {a: [2, 2]}); + nomatch({a: {$size: 0}}, {a: "2"}); + nomatch({a: {$size: 1}}, {a: "2"}); + nomatch({a: {$size: 2}}, {a: "2"}); + + nomatch({a: {$size: 2}}, {a: [[2,2]]}); // tested against mongodb + + + // $bitsAllClear - number + match({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0}); + match({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b10000}); + nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b1}); + nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b10}); + nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b100}); + nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b1000}); + + // $bitsAllClear - buffer + match({a: {$bitsAllClear: new Uint8Array([3])}}, {a: new Uint8Array([4])}); + match({a: {$bitsAllClear: new Uint8Array([0, 1])}}, {a: new Uint8Array([255])}); // 256 should not be set for 255. + match({a: {$bitsAllClear: new Uint8Array([3])}}, {a: 4 }); + + match({a: {$bitsAllClear: new Uint8Array([3])}}, {a: 0 }); + + // $bitsAllSet - number + match({a: {$bitsAllSet: [0,1,2,3]}}, {a: 0b1111}); + nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 0b111}); + nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 256}); + nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 50000}); + match({a: {$bitsAllSet: [0,1,2]}}, {a: 15}); + match({a: {$bitsAllSet: [0, 12]}}, {a: 0b1000000000001}); + nomatch({a: {$bitsAllSet: [0, 12]}}, {a: 0b1000000000000}); + nomatch({a: {$bitsAllSet: [0, 12]}}, {a: 0b1}); + + // $bitsAllSet - buffer + match({a: {$bitsAllSet: new Uint8Array([3])}}, {a: new Uint8Array([3])}); + match({a: {$bitsAllSet: new Uint8Array([7])}}, {a: new Uint8Array([15])}); + match({a: {$bitsAllSet: new Uint8Array([3])}}, {a: 3 }); + + // $bitsAnySet - number + match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b1}); + match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b10}); + match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b100}); + match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b1000}); + match({a: {$bitsAnySet: [4]}}, {a: 0b10000}); + nomatch({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b10000}); + nomatch({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0}); + + // $bitsAnySet - buffer + match({a: {$bitsAnySet: new Uint8Array([3])}}, {a: new Uint8Array([7])}); + match({a: {$bitsAnySet: new Uint8Array([15])}}, {a: new Uint8Array([7])}); + match({a: {$bitsAnySet: new Uint8Array([3])}}, {a: 1 }); + + // $bitsAnyClear - number + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0}); + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1}); + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b10}); + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b100}); + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1000}); + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b10000}); + nomatch({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1111}); + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b111}); + nomatch({a: {$bitsAnyClear: [0,1,2]}}, {a: 0b111}); + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b11}); + nomatch({a: {$bitsAnyClear: [0,1]}}, {a: 0b11}); + match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1}); + nomatch({a: {$bitsAnyClear: [0]}}, {a: 0b1}); + nomatch({a: {$bitsAnyClear: [4]}}, {a: 0b10000}); + + // $bitsAnyClear - buffer + match({a: {$bitsAnyClear: new Uint8Array([8])}}, {a: new Uint8Array([7])}); + match({a: {$bitsAnyClear: new Uint8Array([1])}}, {a: new Uint8Array([0])}); + match({a: {$bitsAnyClear: new Uint8Array([1])}}, {a: 4 }); + + // taken from: https://github.com/mongodb/mongo/blob/master/jstests/core/bittest.js + var c = new LocalCollection; + function matchCount(query, count) { + const matches = c.find(query).count() + if (matches !== count) { + test.fail({message: "minimongo match count failure: matched " + matches + " times, but should match " + count + " times", + query: JSON.stringify(query), + count: JSON.stringify(count) + }); + } + } + + // Tests on numbers. + + c.insert({a: 0}) + c.insert({a: 1}) + c.insert({a: 54}) + c.insert({a: 88}) + c.insert({a: 255}) + + // Tests with bitmask. + matchCount({a: {$bitsAllSet: 0}}, 5) + matchCount({a: {$bitsAllSet: 1}}, 2) + matchCount({a: {$bitsAllSet: 16}}, 3) + matchCount({a: {$bitsAllSet: 54}}, 2) + matchCount({a: {$bitsAllSet: 55}}, 1) + matchCount({a: {$bitsAllSet: 88}}, 2) + matchCount({a: {$bitsAllSet: 255}}, 1) + matchCount({a: {$bitsAllClear: 0}}, 5) + matchCount({a: {$bitsAllClear: 1}}, 3) + matchCount({a: {$bitsAllClear: 16}}, 2) + matchCount({a: {$bitsAllClear: 129}}, 3) + matchCount({a: {$bitsAllClear: 255}}, 1) + matchCount({a: {$bitsAnySet: 0}}, 0) + matchCount({a: {$bitsAnySet: 9}}, 3) + matchCount({a: {$bitsAnySet: 255}}, 4) + matchCount({a: {$bitsAnyClear: 0}}, 0) + matchCount({a: {$bitsAnyClear: 18}}, 3) + matchCount({a: {$bitsAnyClear: 24}}, 3) + matchCount({a: {$bitsAnyClear: 255}}, 4) + + // Tests with array of bit positions. + matchCount({a: {$bitsAllSet: []}}, 5) + matchCount({a: {$bitsAllSet: [0]}}, 2) + matchCount({a: {$bitsAllSet: [4]}}, 3) + matchCount({a: {$bitsAllSet: [1, 2, 4, 5]}}, 2) + matchCount({a: {$bitsAllSet: [0, 1, 2, 4, 5]}}, 1) + matchCount({a: {$bitsAllSet: [3, 4, 6]}}, 2) + matchCount({a: {$bitsAllSet: [0, 1, 2, 3, 4, 5, 6, 7]}}, 1) + matchCount({a: {$bitsAllClear: []}}, 5) + matchCount({a: {$bitsAllClear: [0]}}, 3) + matchCount({a: {$bitsAllClear: [4]}}, 2) + matchCount({a: {$bitsAllClear: [1, 7]}}, 3) + matchCount({a: {$bitsAllClear: [0, 1, 2, 3, 4, 5, 6, 7]}}, 1) + matchCount({a: {$bitsAnySet: []}}, 0) + matchCount({a: {$bitsAnySet: [1, 3]}}, 3) + matchCount({a: {$bitsAnySet: [0, 1, 2, 3, 4, 5, 6, 7]}}, 4) + matchCount({a: {$bitsAnyClear: []}}, 0) + matchCount({a: {$bitsAnyClear: [1, 4]}}, 3) + matchCount({a: {$bitsAnyClear: [3, 4]}}, 3) + matchCount({a: {$bitsAnyClear: [0, 1, 2, 3, 4, 5, 6, 7]}}, 4) + + // Tests with multiple predicates. + matchCount({a: {$bitsAllSet: 54, $bitsAllClear: 201}}, 1) + + // Tests on negative numbers + + c.remove({}) + c.insert({a: -0}) + c.insert({a: -1}) + c.insert({a: -54}) + + // Tests with bitmask. + matchCount({a: {$bitsAllSet: 0}}, 3) + matchCount({a: {$bitsAllSet: 2}}, 2) + matchCount({a: {$bitsAllSet: 127}}, 1) + matchCount({a: {$bitsAllSet: 74}}, 2) + matchCount({a: {$bitsAllClear: 0}}, 3) + matchCount({a: {$bitsAllClear: 53}}, 2) + matchCount({a: {$bitsAllClear: 127}}, 1) + matchCount({a: {$bitsAnySet: 0}}, 0) + matchCount({a: {$bitsAnySet: 2}}, 2) + matchCount({a: {$bitsAnySet: 127}}, 2) + matchCount({a: {$bitsAnyClear: 0}}, 0) + matchCount({a: {$bitsAnyClear: 53}}, 2) + matchCount({a: {$bitsAnyClear: 127}}, 2) + + // Tests with array of bit positions. + var allPositions = [] + for (var i = 0; i < 64; i++) { + allPositions.push(i) + } + + matchCount({a: {$bitsAllSet: []}}, 3) + matchCount({a: {$bitsAllSet: [1]}}, 2) + matchCount({a: {$bitsAllSet: allPositions}}, 1) + matchCount({a: {$bitsAllSet: [1, 7, 6, 3, 100]}}, 2) + matchCount({a: {$bitsAllClear: []}}, 3) + matchCount({a: {$bitsAllClear: [5, 4, 2, 0]}}, 2) + matchCount({a: {$bitsAllClear: allPositions}}, 1) + matchCount({a: {$bitsAnySet: []}}, 0) + matchCount({a: {$bitsAnySet: [1]}}, 2) + matchCount({a: {$bitsAnySet: allPositions}}, 2) + matchCount({a: {$bitsAnyClear: []}}, 0) + matchCount({a: {$bitsAnyClear: [0, 2, 4, 5, 100]}}, 2) + matchCount({a: {$bitsAnyClear: allPositions}}, 2) + + // Tests with multiple predicates. + matchCount({a: {$bitsAllSet: 74, $bitsAllClear: 53}}, 1) + + // Tests on BinData. + + c.remove({}) + c.insert({a: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}) + c.insert({a: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}) + c.insert({a: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}) + c.insert({a: EJSON.parse('{"$binary": "////////////////////////////"}')}) + + // Tests with binary string bitmask. + matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 4) + matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 3) + matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}}, 2) + matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 1) + matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 4) + matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "AAyfAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 3) + matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "JAyfqwetkqwklEWRbWERKKJREtbq"}')}}, 2) + matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 1) + matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 0) + matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "AAyfAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 1) + matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "JAyfqwetkqwklEWRbWERKKJREtbq"}')}}, 2) + matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 3) + matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 0) + matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 1) + matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}}, 2) + matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 3) + + // Tests with multiple predicates. + matchCount({ + a: { + $bitsAllSet: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}'), + $bitsAllClear: EJSON.parse('{"$binary": "//yf////////////////////////"}') + } + }, 1) + + c.remove({}) + + nomatch({a: {$bitsAllSet: 1}}, {a: false}) + nomatch({a: {$bitsAllSet: 1}}, {a: NaN}) + nomatch({a: {$bitsAllSet: 1}}, {a: Infinity}) + nomatch({a: {$bitsAllSet: 1}}, {a: null}) + nomatch({a: {$bitsAllSet: 1}}, {a: 'asdf'}) + nomatch({a: {$bitsAllSet: 1}}, {a: ['a', 'b']}) + nomatch({a: {$bitsAllSet: 1}}, {a: {foo: 'bar'}}) + nomatch({a: {$bitsAllSet: 1}}, {a: 1.2}) + nomatch({a: {$bitsAllSet: 1}}, {a: "1"}); + + [ + false, + NaN, + Infinity, + null, + 'asdf', + ['a', 'b'], + {foo: 'bar'}, + 1.2, + "1", + [0, -1] + ].forEach(function (badValue) { + test.throws(function () { + match({a: {$bitsAllSet: badValue}}, {a: 42}); + }); + }); + + // $type + match({a: {$type: 1}}, {a: 1.1}); + match({a: {$type: 1}}, {a: 1}); + nomatch({a: {$type: 1}}, {a: "1"}); + match({a: {$type: 2}}, {a: "1"}); + nomatch({a: {$type: 2}}, {a: 1}); + match({a: {$type: 3}}, {a: {}}); + match({a: {$type: 3}}, {a: {b: 2}}); + nomatch({a: {$type: 3}}, {a: []}); + nomatch({a: {$type: 3}}, {a: [1]}); + nomatch({a: {$type: 3}}, {a: null}); + match({a: {$type: 5}}, {a: EJSON.newBinary(0)}); + match({a: {$type: 5}}, {a: EJSON.newBinary(4)}); + nomatch({a: {$type: 5}}, {a: []}); + nomatch({a: {$type: 5}}, {a: [42]}); + match({a: {$type: 7}}, {a: new MongoID.ObjectID()}); + nomatch({a: {$type: 7}}, {a: "1234567890abcd1234567890"}); + match({a: {$type: 8}}, {a: true}); + match({a: {$type: 8}}, {a: false}); + nomatch({a: {$type: 8}}, {a: "true"}); + nomatch({a: {$type: 8}}, {a: 0}); + nomatch({a: {$type: 8}}, {a: null}); + nomatch({a: {$type: 8}}, {a: ''}); + nomatch({a: {$type: 8}}, {}); + match({a: {$type: 9}}, {a: (new Date)}); + nomatch({a: {$type: 9}}, {a: +(new Date)}); + match({a: {$type: 10}}, {a: null}); + nomatch({a: {$type: 10}}, {a: false}); + nomatch({a: {$type: 10}}, {a: ''}); + nomatch({a: {$type: 10}}, {a: 0}); + nomatch({a: {$type: 10}}, {}); + match({a: {$type: 11}}, {a: /x/}); + nomatch({a: {$type: 11}}, {a: 'x'}); + nomatch({a: {$type: 11}}, {}); + + // The normal rule for {$type:4} (4 means array) is that it NOT good enough to + // just have an array that's the leaf that matches the path. (An array inside + // that array is good, though.) + nomatch({a: {$type: 4}}, {a: []}); + nomatch({a: {$type: 4}}, {a: [1]}); // tested against mongodb + match({a: {$type: 1}}, {a: [1]}); + nomatch({a: {$type: 2}}, {a: [1]}); + match({a: {$type: 1}}, {a: ["1", 1]}); + match({a: {$type: 2}}, {a: ["1", 1]}); + nomatch({a: {$type: 3}}, {a: ["1", 1]}); + nomatch({a: {$type: 4}}, {a: ["1", 1]}); + nomatch({a: {$type: 1}}, {a: ["1", []]}); + match({a: {$type: 2}}, {a: ["1", []]}); + match({a: {$type: 4}}, {a: ["1", []]}); // tested against mongodb + // An exception to the normal rule is that an array found via numeric index is + // examined itself, and its elements are not. + match({'a.0': {$type: 4}}, {a: [[0]]}); + nomatch({'a.0': {$type: 1}}, {a: [[0]]}); + + // regular expressions + match({a: /a/}, {a: 'cat'}); + nomatch({a: /a/}, {a: 'cut'}); + nomatch({a: /a/}, {a: 'CAT'}); + match({a: /a/i}, {a: 'CAT'}); + match({a: /a/}, {a: ['foo', 'bar']}); // search within array... + nomatch({a: /,/}, {a: ['foo', 'bar']}); // but not by stringifying + match({a: {$regex: 'a'}}, {a: ['foo', 'bar']}); + nomatch({a: {$regex: ','}}, {a: ['foo', 'bar']}); + match({a: {$regex: /a/}}, {a: 'cat'}); + nomatch({a: {$regex: /a/}}, {a: 'cut'}); + nomatch({a: {$regex: /a/}}, {a: 'CAT'}); + match({a: {$regex: /a/i}}, {a: 'CAT'}); + match({a: {$regex: /a/, $options: 'i'}}, {a: 'CAT'}); // tested + match({a: {$regex: /a/i, $options: 'i'}}, {a: 'CAT'}); // tested + nomatch({a: {$regex: /a/i, $options: ''}}, {a: 'CAT'}); // tested + match({a: {$regex: 'a'}}, {a: 'cat'}); + nomatch({a: {$regex: 'a'}}, {a: 'cut'}); + nomatch({a: {$regex: 'a'}}, {a: 'CAT'}); + match({a: {$regex: 'a', $options: 'i'}}, {a: 'CAT'}); + match({a: {$regex: '', $options: 'i'}}, {a: 'foo'}); + nomatch({a: {$regex: '', $options: 'i'}}, {}); + nomatch({a: {$regex: '', $options: 'i'}}, {a: 5}); + nomatch({a: /undefined/}, {}); + nomatch({a: {$regex: 'undefined'}}, {}); + nomatch({a: /xxx/}, {}); + nomatch({a: {$regex: 'xxx'}}, {}); + + // GitHub issue #2817: + // Regexps with a global flag ('g') keep a state when tested against the same + // string. Selector shouldn't return different result for similar documents + // because of this state. + var reusedRegexp = /sh/ig; + match({a: reusedRegexp}, {a: 'Shorts'}); + match({a: reusedRegexp}, {a: 'Shorts'}); + match({a: reusedRegexp}, {a: 'Shorts'}); + + match({a: {$regex: reusedRegexp}}, {a: 'Shorts'}); + match({a: {$regex: reusedRegexp}}, {a: 'Shorts'}); + match({a: {$regex: reusedRegexp}}, {a: 'Shorts'}); + + test.throws(function () { + match({a: {$options: 'i'}}, {a: 12}); + }); + + match({a: /a/}, {a: ['dog', 'cat']}); + nomatch({a: /a/}, {a: ['dog', 'puppy']}); + + // we don't support regexps in minimongo very well (eg, there's no EJSON + // encoding so it won't go over the wire), but run these tests anyway + match({a: /a/}, {a: /a/}); + match({a: /a/}, {a: ['x', /a/]}); + nomatch({a: /a/}, {a: /a/i}); + nomatch({a: /a/m}, {a: /a/}); + nomatch({a: /a/}, {a: /b/}); + nomatch({a: /5/}, {a: 5}); + nomatch({a: /t/}, {a: true}); + match({a: /m/i}, {a: ['x', 'xM']}); + + test.throws(function () { + match({a: {$regex: /a/, $options: 'x'}}, {a: 'cat'}); + }); + test.throws(function () { + match({a: {$regex: /a/, $options: 's'}}, {a: 'cat'}); + }); + + // $not + match({x: {$not: {$gt: 7}}}, {x: 6}); + nomatch({x: {$not: {$gt: 7}}}, {x: 8}); + match({x: {$not: {$lt: 10, $gt: 7}}}, {x: 11}); + nomatch({x: {$not: {$lt: 10, $gt: 7}}}, {x: 9}); + match({x: {$not: {$lt: 10, $gt: 7}}}, {x: 6}); + + match({x: {$not: {$gt: 7}}}, {x: [2, 3, 4]}); + match({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}]}); + nomatch({x: {$not: {$gt: 7}}}, {x: [2, 3, 4, 10]}); + nomatch({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}, {y:10}]}); + + match({x: {$not: /a/}}, {x: "dog"}); + nomatch({x: {$not: /a/}}, {x: "cat"}); + match({x: {$not: /a/}}, {x: ["dog", "puppy"]}); + nomatch({x: {$not: /a/}}, {x: ["kitten", "cat"]}); + + // dotted keypaths: bare values + match({"a.b": 1}, {a: {b: 1}}); + nomatch({"a.b": 1}, {a: {b: 2}}); + match({"a.b": [1,2,3]}, {a: {b: [1,2,3]}}); + nomatch({"a.b": [1,2,3]}, {a: {b: [4]}}); + match({"a.b": /a/}, {a: {b: "cat"}}); + nomatch({"a.b": /a/}, {a: {b: "dog"}}); + match({"a.b.c": null}, {}); + match({"a.b.c": null}, {a: 1}); + match({"a.b": null}, {a: 1}); + match({"a.b.c": null}, {a: {b: 4}}); + + // dotted keypaths, nulls, numeric indices, arrays + nomatch({"a.b": null}, {a: [1]}); + match({"a.b": []}, {a: {b: []}}); + var big = {a: [{b: 1}, 2, {}, {b: [3, 4]}]}; + match({"a.b": 1}, big); + match({"a.b": [3, 4]}, big); + match({"a.b": 3}, big); + match({"a.b": 4}, big); + match({"a.b": null}, big); // matches on slot 2 + match({'a.1': 8}, {a: [7, 8, 9]}); + nomatch({'a.1': 7}, {a: [7, 8, 9]}); + nomatch({'a.1': null}, {a: [7, 8, 9]}); + match({'a.1': [8, 9]}, {a: [7, [8, 9]]}); + nomatch({'a.1': 6}, {a: [[6, 7], [8, 9]]}); + nomatch({'a.1': 7}, {a: [[6, 7], [8, 9]]}); + nomatch({'a.1': 8}, {a: [[6, 7], [8, 9]]}); + nomatch({'a.1': 9}, {a: [[6, 7], [8, 9]]}); + match({"a.1": 2}, {a: [0, {1: 2}, 3]}); + match({"a.1": {1: 2}}, {a: [0, {1: 2}, 3]}); + match({"x.1.y": 8}, {x: [7, {y: 8}, 9]}); + // comes from trying '1' as key in the plain object + match({"x.1.y": null}, {x: [7, {y: 8}, 9]}); + match({"a.1.b": 9}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); + match({"a.1.b": 'foo'}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); + match({"a.1.b": null}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); + match({"a.1.b": 2}, {a: [1, [{b: 2}], 3]}); + nomatch({"a.1.b": null}, {a: [1, [{b: 2}], 3]}); + // this is new behavior in mongo 2.5 + nomatch({"a.0.b": null}, {a: [5]}); + match({"a.1": 4}, {a: [{1: 4}, 5]}); + match({"a.1": 5}, {a: [{1: 4}, 5]}); + nomatch({"a.1": null}, {a: [{1: 4}, 5]}); + match({"a.1.foo": 4}, {a: [{1: {foo: 4}}, {foo: 5}]}); + match({"a.1.foo": 5}, {a: [{1: {foo: 4}}, {foo: 5}]}); + match({"a.1.foo": null}, {a: [{1: {foo: 4}}, {foo: 5}]}); + + // trying to access a dotted field that is undefined at some point + // down the chain + nomatch({"a.b": 1}, {x: 2}); + nomatch({"a.b.c": 1}, {a: {x: 2}}); + nomatch({"a.b.c": 1}, {a: {b: {x: 2}}}); + nomatch({"a.b.c": 1}, {a: {b: 1}}); + nomatch({"a.b.c": 1}, {a: {b: 0}}); + + // dotted keypaths: literal objects + match({"a.b": {c: 1}}, {a: {b: {c: 1}}}); + nomatch({"a.b": {c: 1}}, {a: {b: {c: 2}}}); + nomatch({"a.b": {c: 1}}, {a: {b: 2}}); + match({"a.b": {c: 1, d: 2}}, {a: {b: {c: 1, d: 2}}}); + nomatch({"a.b": {c: 1, d: 2}}, {a: {b: {c: 1, d: 1}}}); + nomatch({"a.b": {c: 1, d: 2}}, {a: {b: {d: 2}}}); + + // dotted keypaths: $ operators + match({"a.b": {$in: [1, 2, 3]}}, {a: {b: [2]}}); // tested against mongodb + match({"a.b": {$in: [{x: 1}, {x: 2}, {x: 3}]}}, {a: {b: [{x: 2}]}}); + match({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4, 2]}}); + nomatch({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4]}}); + + // $or + test.throws(function () { + match({$or: []}, {}); + }); + test.throws(function () { + match({$or: [5]}, {}); + }); + test.throws(function () { + match({$or: []}, {a: 1}); + }); + match({$or: [{a: 1}]}, {a: 1}); + nomatch({$or: [{b: 2}]}, {a: 1}); + match({$or: [{a: 1}, {b: 2}]}, {a: 1}); + nomatch({$or: [{c: 3}, {d: 4}]}, {a: 1}); + match({$or: [{a: 1}, {b: 2}]}, {a: [1, 2, 3]}); + nomatch({$or: [{a: 1}, {b: 2}]}, {c: [1, 2, 3]}); + nomatch({$or: [{a: 1}, {b: 2}]}, {a: [2, 3, 4]}); + match({$or: [{a: 1}, {a: 2}]}, {a: 1}); + match({$or: [{a: 1}, {a: 2}], b: 2}, {a: 1, b: 2}); + nomatch({$or: [{a: 2}, {a: 3}], b: 2}, {a: 1, b: 2}); + nomatch({$or: [{a: 1}, {a: 2}], b: 3}, {a: 1, b: 2}); + + // Combining $or with equality + match({x: 1, $or: [{a: 1}, {b: 1}]}, {x: 1, b: 1}); + match({$or: [{a: 1}, {b: 1}], x: 1}, {x: 1, b: 1}); + nomatch({x: 1, $or: [{a: 1}, {b: 1}]}, {b: 1}); + nomatch({x: 1, $or: [{a: 1}, {b: 1}]}, {x: 1}); + + // $or and $lt, $lte, $gt, $gte + match({$or: [{a: {$lte: 1}}, {a: 2}]}, {a: 1}); + nomatch({$or: [{a: {$lt: 1}}, {a: 2}]}, {a: 1}); + match({$or: [{a: {$gte: 1}}, {a: 2}]}, {a: 1}); + nomatch({$or: [{a: {$gt: 1}}, {a: 2}]}, {a: 1}); + match({$or: [{b: {$gt: 1}}, {b: {$lt: 3}}]}, {b: 2}); + nomatch({$or: [{b: {$lt: 1}}, {b: {$gt: 3}}]}, {b: 2}); + + // $or and $in + match({$or: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); + nomatch({$or: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); + match({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {a: 1}); + match({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {b: 2}); + nomatch({$or: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {c: 3}); + match({$or: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {b: 2}); + nomatch({$or: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {b: 2}); + + // $or and $nin + nomatch({$or: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); + match({$or: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); + nomatch({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {a: 1}); + match({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {b: 2}); + match({$or: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {c: 3}); + match({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {b: 2}); + nomatch({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 2}); + match({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {b: 2}); + + // $or and dot-notation + match({$or: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); + match({$or: [{"a.b": 1}, {"a.c": 1}]}, {a: {b: 1}}); + nomatch({$or: [{"a.b": 2}, {"a.c": 1}]}, {a: {b: 1}}); + + // $or and nested objects + match({$or: [{a: {b: 1, c: 2}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); + nomatch({$or: [{a: {b: 1, c: 3}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); + + // $or and regexes + match({$or: [{a: /a/}]}, {a: "cat"}); + nomatch({$or: [{a: /o/}]}, {a: "cat"}); + match({$or: [{a: /a/}, {a: /o/}]}, {a: "cat"}); + nomatch({$or: [{a: /i/}, {a: /o/}]}, {a: "cat"}); + match({$or: [{a: /i/}, {b: /o/}]}, {a: "cat", b: "dog"}); + + // $or and $ne + match({$or: [{a: {$ne: 1}}]}, {}); + nomatch({$or: [{a: {$ne: 1}}]}, {a: 1}); + match({$or: [{a: {$ne: 1}}]}, {a: 2}); + match({$or: [{a: {$ne: 1}}]}, {b: 1}); + match({$or: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 1}); + match({$or: [{a: {$ne: 1}}, {b: {$ne: 1}}]}, {a: 1}); + nomatch({$or: [{a: {$ne: 1}}, {b: {$ne: 2}}]}, {a: 1, b: 2}); + + // $or and $not + match({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {}); + nomatch({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 1}); + match({$or: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 2}); + match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$not: {$mod: [10, 2]}}}]}, {a: 1}); + nomatch({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 1}); + match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 2}); + match({$or: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 3}); + // this is possibly an open-ended task, so we stop here ... + + // $nor + test.throws(function () { + match({$nor: []}, {}); + }); + test.throws(function () { + match({$nor: [5]}, {}); + }); + test.throws(function () { + match({$nor: []}, {a: 1}); + }); + nomatch({$nor: [{a: 1}]}, {a: 1}); + match({$nor: [{b: 2}]}, {a: 1}); + nomatch({$nor: [{a: 1}, {b: 2}]}, {a: 1}); + match({$nor: [{c: 3}, {d: 4}]}, {a: 1}); + nomatch({$nor: [{a: 1}, {b: 2}]}, {a: [1, 2, 3]}); + match({$nor: [{a: 1}, {b: 2}]}, {c: [1, 2, 3]}); + match({$nor: [{a: 1}, {b: 2}]}, {a: [2, 3, 4]}); + nomatch({$nor: [{a: 1}, {a: 2}]}, {a: 1}); + + // $nor and $lt, $lte, $gt, $gte + nomatch({$nor: [{a: {$lte: 1}}, {a: 2}]}, {a: 1}); + match({$nor: [{a: {$lt: 1}}, {a: 2}]}, {a: 1}); + nomatch({$nor: [{a: {$gte: 1}}, {a: 2}]}, {a: 1}); + match({$nor: [{a: {$gt: 1}}, {a: 2}]}, {a: 1}); + nomatch({$nor: [{b: {$gt: 1}}, {b: {$lt: 3}}]}, {b: 2}); + match({$nor: [{b: {$lt: 1}}, {b: {$gt: 3}}]}, {b: 2}); + + // $nor and $in + nomatch({$nor: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); + match({$nor: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); + nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {a: 1}); + nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {b: 2}); + match({$nor: [{a: {$in: [1, 2, 3]}}, {b: 2}]}, {c: 3}); + nomatch({$nor: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {b: 2}); + match({$nor: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {b: 2}); + + // $nor and $nin + match({$nor: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); + nomatch({$nor: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); + match({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {a: 1}); + nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {b: 2}); + nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: 2}]}, {c: 3}); + nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {b: 2}); + match({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 2}); + nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {b: 2}); + + // $nor and dot-notation + nomatch({$nor: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); + nomatch({$nor: [{"a.b": 1}, {"a.c": 1}]}, {a: {b: 1}}); + match({$nor: [{"a.b": 2}, {"a.c": 1}]}, {a: {b: 1}}); + + // $nor and nested objects + nomatch({$nor: [{a: {b: 1, c: 2}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); + match({$nor: [{a: {b: 1, c: 3}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); + + // $nor and regexes + nomatch({$nor: [{a: /a/}]}, {a: "cat"}); + match({$nor: [{a: /o/}]}, {a: "cat"}); + nomatch({$nor: [{a: /a/}, {a: /o/}]}, {a: "cat"}); + match({$nor: [{a: /i/}, {a: /o/}]}, {a: "cat"}); + nomatch({$nor: [{a: /i/}, {b: /o/}]}, {a: "cat", b: "dog"}); + + // $nor and $ne + nomatch({$nor: [{a: {$ne: 1}}]}, {}); + match({$nor: [{a: {$ne: 1}}]}, {a: 1}); + nomatch({$nor: [{a: {$ne: 1}}]}, {a: 2}); + nomatch({$nor: [{a: {$ne: 1}}]}, {b: 1}); + nomatch({$nor: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 1}); + nomatch({$nor: [{a: {$ne: 1}}, {b: {$ne: 1}}]}, {a: 1}); + match({$nor: [{a: {$ne: 1}}, {b: {$ne: 2}}]}, {a: 1, b: 2}); + + // $nor and $not + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {}); + match({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 1}); + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}]}, {a: 2}); + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$not: {$mod: [10, 2]}}}]}, {a: 1}); + match({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 1}); + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 2}); + nomatch({$nor: [{a: {$not: {$mod: [10, 1]}}}, {a: {$mod: [10, 2]}}]}, {a: 3}); + + // $and + + test.throws(function () { + match({$and: []}, {}); + }); + test.throws(function () { + match({$and: [5]}, {}); + }); + test.throws(function () { + match({$and: []}, {a: 1}); + }); + match({$and: [{a: 1}]}, {a: 1}); + nomatch({$and: [{a: 1}, {a: 2}]}, {a: 1}); + nomatch({$and: [{a: 1}, {b: 1}]}, {a: 1}); + match({$and: [{a: 1}, {b: 2}]}, {a: 1, b: 2}); + nomatch({$and: [{a: 1}, {b: 1}]}, {a: 1, b: 2}); + match({$and: [{a: 1}, {b: 2}], c: 3}, {a: 1, b: 2, c: 3}); + nomatch({$and: [{a: 1}, {b: 2}], c: 4}, {a: 1, b: 2, c: 3}); + + // $and and regexes + match({$and: [{a: /a/}]}, {a: "cat"}); + match({$and: [{a: /a/i}]}, {a: "CAT"}); + nomatch({$and: [{a: /o/}]}, {a: "cat"}); + nomatch({$and: [{a: /a/}, {a: /o/}]}, {a: "cat"}); + match({$and: [{a: /a/}, {b: /o/}]}, {a: "cat", b: "dog"}); + nomatch({$and: [{a: /a/}, {b: /a/}]}, {a: "cat", b: "dog"}); + + // $and, dot-notation, and nested objects + match({$and: [{"a.b": 1}]}, {a: {b: 1}}); + match({$and: [{a: {b: 1}}]}, {a: {b: 1}}); + nomatch({$and: [{"a.b": 2}]}, {a: {b: 1}}); + nomatch({$and: [{"a.c": 1}]}, {a: {b: 1}}); + nomatch({$and: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); + nomatch({$and: [{"a.b": 1}, {a: {b: 2}}]}, {a: {b: 1}}); + match({$and: [{"a.b": 1}, {"c.d": 2}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{"a.b": 1}, {"c.d": 1}]}, {a: {b: 1}, c: {d: 2}}); + match({$and: [{"a.b": 1}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{"a.b": 1}, {c: {d: 1}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{"a.b": 2}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + match({$and: [{a: {b: 1}}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{a: {b: 2}}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + + // $and and $in + nomatch({$and: [{a: {$in: []}}]}, {}); + match({$and: [{a: {$in: [1, 2, 3]}}]}, {a: 1}); + nomatch({$and: [{a: {$in: [4, 5, 6]}}]}, {a: 1}); + nomatch({$and: [{a: {$in: [1, 2, 3]}}, {a: {$in: [4, 5, 6]}}]}, {a: 1}); + nomatch({$and: [{a: {$in: [1, 2, 3]}}, {b: {$in: [1, 2, 3]}}]}, {a: 1, b: 4}); + match({$and: [{a: {$in: [1, 2, 3]}}, {b: {$in: [4, 5, 6]}}]}, {a: 1, b: 4}); + + + // $and and $nin + match({$and: [{a: {$nin: []}}]}, {}); + nomatch({$and: [{a: {$nin: [1, 2, 3]}}]}, {a: 1}); + match({$and: [{a: {$nin: [4, 5, 6]}}]}, {a: 1}); + nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {a: {$nin: [4, 5, 6]}}]}, {a: 1}); + nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [1, 2, 3]}}]}, {a: 1, b: 4}); + nomatch({$and: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {a: 1, b: 4}); + + // $and and $lt, $lte, $gt, $gte + match({$and: [{a: {$lt: 2}}]}, {a: 1}); + nomatch({$and: [{a: {$lt: 1}}]}, {a: 1}); + match({$and: [{a: {$lte: 1}}]}, {a: 1}); + match({$and: [{a: {$gt: 0}}]}, {a: 1}); + nomatch({$and: [{a: {$gt: 1}}]}, {a: 1}); + match({$and: [{a: {$gte: 1}}]}, {a: 1}); + match({$and: [{a: {$gt: 0}}, {a: {$lt: 2}}]}, {a: 1}); + nomatch({$and: [{a: {$gt: 1}}, {a: {$lt: 2}}]}, {a: 1}); + nomatch({$and: [{a: {$gt: 0}}, {a: {$lt: 1}}]}, {a: 1}); + match({$and: [{a: {$gte: 1}}, {a: {$lte: 1}}]}, {a: 1}); + nomatch({$and: [{a: {$gte: 2}}, {a: {$lte: 0}}]}, {a: 1}); + + // $and and $ne + match({$and: [{a: {$ne: 1}}]}, {}); + nomatch({$and: [{a: {$ne: 1}}]}, {a: 1}); + match({$and: [{a: {$ne: 1}}]}, {a: 2}); + nomatch({$and: [{a: {$ne: 1}}, {a: {$ne: 2}}]}, {a: 2}); + match({$and: [{a: {$ne: 1}}, {a: {$ne: 3}}]}, {a: 2}); + + // $and and $not + match({$and: [{a: {$not: {$gt: 2}}}]}, {a: 1}); + nomatch({$and: [{a: {$not: {$lt: 2}}}]}, {a: 1}); + match({$and: [{a: {$not: {$lt: 0}}}, {a: {$not: {$gt: 2}}}]}, {a: 1}); + nomatch({$and: [{a: {$not: {$lt: 2}}}, {a: {$not: {$gt: 0}}}]}, {a: 1}); + + // $where + match({$where: "this.a === 1"}, {a: 1}); + match({$where: "obj.a === 1"}, {a: 1}); + nomatch({$where: "this.a !== 1"}, {a: 1}); + nomatch({$where: "obj.a !== 1"}, {a: 1}); + nomatch({$where: "this.a === 1", a: 2}, {a: 1}); + match({$where: "this.a === 1", b: 2}, {a: 1, b: 2}); + match({$where: "this.a === 1 && this.b === 2"}, {a: 1, b: 2}); + match({$where: "this.a instanceof Array"}, {a: []}); + nomatch({$where: "this.a instanceof Array"}, {a: 1}); + + // reaching into array + match({"dogs.0.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({"dogs.1.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + nomatch({"dogs.1.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({"room.1b": "bla"}, {room: {"1b": "bla"}}); + + match({"dogs.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({"dogs.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({"animals.dogs.name": "Fido"}, + {animals: [{dogs: [{name: "Rover"}]}, + {}, + {dogs: [{name: "Fido"}, {name: "Rex"}]}]}); + match({"animals.dogs.name": "Fido"}, + {animals: [{dogs: {name: "Rex"}}, + {dogs: {name: "Fido"}}]}); + match({"animals.dogs.name": "Fido"}, + {animals: [{dogs: [{name: "Rover"}]}, + {}, + {dogs: [{name: ["Fido"]}, {name: "Rex"}]}]}); + nomatch({"dogs.name": "Fido"}, {dogs: []}); + + // $elemMatch + match({dogs: {$elemMatch: {name: /e/}}}, + {dogs: [{name: "Fido"}, {name: "Rex"}]}); + nomatch({dogs: {$elemMatch: {name: /a/}}}, + {dogs: [{name: "Fido"}, {name: "Rex"}]}); + match({dogs: {$elemMatch: {age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: "Fido", age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + nomatch({dogs: {$elemMatch: {name: "Fido", age: {$gt: 5}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({dogs: {$elemMatch: {name: /i/, age: {$gt: 4}}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + nomatch({dogs: {$elemMatch: {name: /e/, age: 5}}}, + {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + match({x: {$elemMatch: {y: 9}}}, {x: [{y: 9}]}); + nomatch({x: {$elemMatch: {y: 9}}}, {x: [[{y: 9}]]}); + match({x: {$elemMatch: {$gt: 5, $lt: 9}}}, {x: [8]}); + nomatch({x: {$elemMatch: {$gt: 5, $lt: 9}}}, {x: [[8]]}); + match({'a.x': {$elemMatch: {y: 9}}}, + {a: [{x: []}, {x: [{y: 9}]}]}); + nomatch({a: {$elemMatch: {x: 5}}}, {a: {x: 5}}); + match({a: {$elemMatch: {0: {$gt: 5, $lt: 9}}}}, {a: [[6]]}); + match({a: {$elemMatch: {'0.b': {$gt: 5, $lt: 9}}}}, {a: [[{b:6}]]}); + match({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, + {a: [{x: 1, b: 1}]}); + match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}], x: 1}}}, + {a: [{x: 1, b: 1}]}); + match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}]}}}, + {a: [{x: 1, b: 1}]}); + match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}]}}}, + {a: [{x: 1, b: 1}]}); + match({a: {$elemMatch: {$and: [{b: 1}, {x: 1}]}}}, + {a: [{x: 1, b: 1}]}); + nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, + {a: [{b: 1}]}); + nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, + {a: [{x: 1}]}); + nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, + {a: [{x: 1}, {b: 1}]}); + + test.throws(function () { + match({a: {$elemMatch: {$gte: 1, $or: [{a: 1}, {b: 1}]}}}, + {a: [{x: 1, b: 1}]}); + }); + + test.throws(function () { + match({x: {$elemMatch: {$and: [{$gt: 5, $lt: 9}]}}}, {x: [8]}); + }); + + // $comment + match({a: 5, $comment: "asdf"}, {a: 5}); + nomatch({a: 6, $comment: "asdf"}, {a: 5}); + + // XXX still needs tests: + // - non-scalar arguments to $gt, $lt, etc +}); + +Tinytest.add("minimongo - projection_compiler", function (test) { + var testProjection = function (projection, tests) { + var projection_f = LocalCollection._compileProjection(projection); + var equalNonStrict = function (a, b, desc) { + test.isTrue(EJSON.equals(a, b), desc); + }; + + tests.forEach(function (testCase) { + equalNonStrict(projection_f(testCase[0]), testCase[1], testCase[2]); + }); + }; + + var testCompileProjectionThrows = function (projection, expectedError) { + test.throws(function () { + LocalCollection._compileProjection(projection); + }, expectedError); + }; + + testProjection({ 'foo': 1, 'bar': 1 }, [ + [{ foo: 42, bar: "something", baz: "else" }, + { foo: 42, bar: "something" }, + "simplest - whitelist"], + + [{ foo: { nested: 17 }, baz: {} }, + { foo: { nested: 17 } }, + "nested whitelisted field"], + + [{ _id: "uid", bazbaz: 42 }, + { _id: "uid" }, + "simplest whitelist - preserve _id"] + ]); + + testProjection({ 'foo': 0, 'bar': 0 }, [ + [{ foo: 42, bar: "something", baz: "else" }, + { baz: "else" }, + "simplest - blacklist"], + + [{ foo: { nested: 17 }, baz: { foo: "something" } }, + { baz: { foo: "something" } }, + "nested blacklisted field"], + + [{ _id: "uid", bazbaz: 42 }, + { _id: "uid", bazbaz: 42 }, + "simplest blacklist - preserve _id"] + ]); + + testProjection({ _id: 0, foo: 1 }, [ + [{ foo: 42, bar: 33, _id: "uid" }, + { foo: 42 }, + "whitelist - _id blacklisted"] + ]); + + testProjection({ _id: 0, foo: 0 }, [ + [{ foo: 42, bar: 33, _id: "uid" }, + { bar: 33 }, + "blacklist - _id blacklisted"] + ]); + + testProjection({ 'foo.bar.baz': 1 }, [ + [{ foo: { meh: "fur", bar: { baz: 42 }, tr: 1 }, bar: 33, baz: 'trolololo' }, + { foo: { bar: { baz: 42 } } }, + "whitelist nested"], + + // Behavior of this test is looked up in actual mongo + [{ foo: { meh: "fur", bar: "nope", tr: 1 }, bar: 33, baz: 'trolololo' }, + { foo: {} }, + "whitelist nested - path not found in doc, different type"], + + // Behavior of this test is looked up in actual mongo + [{ foo: { meh: "fur", bar: [], tr: 1 }, bar: 33, baz: 'trolololo' }, + { foo: { bar: [] } }, + "whitelist nested - path not found in doc"] + ]); + + testProjection({ 'hope.humanity': 0, 'hope.people': 0 }, [ + [{ hope: { humanity: "lost", people: 'broken', candies: 'long live!' } }, + { hope: { candies: 'long live!' } }, + "blacklist nested"], + + [{ hope: "new" }, + { hope: "new" }, + "blacklist nested - path not found in doc"] + ]); + + testProjection({ _id: 1 }, [ + [{ _id: 42, x: 1, y: { z: "2" } }, + { _id: 42 }, + "_id whitelisted"], + [{ _id: 33 }, + { _id: 33 }, + "_id whitelisted, _id only"], + [{ x: 1 }, + {}, + "_id whitelisted, no _id"] + ]); + + testProjection({ _id: 0 }, [ + [{ _id: 42, x: 1, y: { z: "2" } }, + { x: 1, y: { z: "2" } }, + "_id blacklisted"], + [{ _id: 33 }, + {}, + "_id blacklisted, _id only"], + [{ x: 1 }, + { x: 1 }, + "_id blacklisted, no _id"] + ]); + + testProjection({}, [ + [{ a: 1, b: 2, c: "3" }, + { a: 1, b: 2, c: "3" }, + "empty projection"] + ]); + + testCompileProjectionThrows( + { 'inc': 1, 'excl': 0 }, + "You cannot currently mix including and excluding fields"); + testCompileProjectionThrows( + { _id: 1, a: 0 }, + "You cannot currently mix including and excluding fields"); + + testCompileProjectionThrows( + { 'a': 1, 'a.b': 1 }, + "using both of them may trigger unexpected behavior"); + testCompileProjectionThrows( + { 'a.b.c': 1, 'a.b': 1, 'a': 1 }, + "using both of them may trigger unexpected behavior"); + + testCompileProjectionThrows("some string", "fields option must be an object"); +}); + +Tinytest.add("minimongo - fetch with fields", function (test) { + var c = new LocalCollection(); + Array.from({length: 30}, function (x, i) { + c.insert({ + something: Random.id(), + anything: { + foo: "bar", + cool: "hot" + }, + nothing: i, + i: i + }); + }); + + // Test just a regular fetch with some projection + var fetchResults = c.find({}, { fields: { + 'something': 1, + 'anything.foo': 1 + } }).fetch(); + + test.isTrue(fetchResults.every(function (x) { + return x && + x.something && + x.anything && + x.anything.foo && + x.anything.foo === "bar" && + !x.hasOwnProperty('nothing') && + !x.anything.hasOwnProperty('cool'); + })); + + // Test with a selector, even field used in the selector is excluded in the + // projection + fetchResults = c.find({ + nothing: { $gte: 5 } + }, { + fields: { nothing: 0 } + }).fetch(); + + test.isTrue(fetchResults.every(function (x) { + return x && + x.something && + x.anything && + x.anything.foo === "bar" && + x.anything.cool === "hot" && + !x.hasOwnProperty('nothing') && + x.i && + x.i >= 5; + })); + + test.isTrue(fetchResults.length === 25); + + // Test that we can sort, based on field excluded from the projection, use + // skip and limit as well! + // following find will get indexes [10..20) sorted by nothing + fetchResults = c.find({}, { + sort: { + nothing: 1 + }, + limit: 10, + skip: 10, + fields: { + i: 1, + something: 1 + } + }).fetch(); + + test.isTrue(fetchResults.every(function (x) { + return x && + x.something && + x.i >= 10 && x.i < 20; + })); + + fetchResults.forEach(function (x, i, arr) { + if (!i) return; + test.isTrue(x.i === arr[i-1].i + 1); + }); + + // Temporary unsupported operators + // queries are taken from MongoDB docs examples + test.throws(function () { + c.find({}, { fields: { 'grades.$': 1 } }); + }); + test.throws(function () { + c.find({}, { fields: { grades: { $elemMatch: { mean: 70 } } } }); + }); + test.throws(function () { + c.find({}, { fields: { grades: { $slice: [20, 10] } } }); + }); +}); + +Tinytest.add("minimongo - fetch with projection, subarrays", function (test) { + // Apparently projection of type 'foo.bar.x' for + // { foo: [ { bar: { x: 42 } }, { bar: { x: 3 } } ] } + // should return exactly this object. More precisely, arrays are considered as + // sets and are queried separately and then merged back to result set + var c = new LocalCollection(); + + // Insert a test object with two set fields + c.insert({ + setA: [{ + fieldA: 42, + fieldB: 33 + }, { + fieldA: "the good", + fieldB: "the bad", + fieldC: "the ugly" + }], + setB: [{ + anotherA: { }, + anotherB: "meh" + }, { + anotherA: 1234, + anotherB: 431 + }] + }); + + var equalNonStrict = function (a, b, desc) { + test.isTrue(EJSON.equals(a, b), desc); + }; + + var testForProjection = function (projection, expected) { + var fetched = c.find({}, { fields: projection }).fetch()[0]; + equalNonStrict(fetched, expected, "failed sub-set projection: " + + JSON.stringify(projection)); + }; + + testForProjection({ 'setA.fieldA': 1, 'setB.anotherB': 1, _id: 0 }, + { + setA: [{ fieldA: 42 }, { fieldA: "the good" }], + setB: [{ anotherB: "meh" }, { anotherB: 431 }] + }); + + testForProjection({ 'setA.fieldA': 0, 'setB.anotherA': 0, _id: 0 }, + { + setA: [{fieldB:33}, {fieldB:"the bad",fieldC:"the ugly"}], + setB: [{ anotherB: "meh" }, { anotherB: 431 }] + }); + + c.remove({}); + c.insert({a:[[{b:1,c:2},{b:2,c:4}],{b:3,c:5},[{b:4, c:9}]]}); + + testForProjection({ 'a.b': 1, _id: 0 }, + {a: [ [ { b: 1 }, { b: 2 } ], { b: 3 }, [ { b: 4 } ] ] }); + testForProjection({ 'a.b': 0, _id: 0 }, + {a: [ [ { c: 2 }, { c: 4 } ], { c: 5 }, [ { c: 9 } ] ] }); +}); + +Tinytest.add("minimongo - fetch with projection, deep copy", function (test) { + // Compiled fields projection defines the contract: returned document doesn't + // retain anything from the passed argument. + var doc = { + a: { x: 42 }, + b: { + y: { z: 33 } + }, + c: "asdf" + }; + + var fields = { + 'a': 1, + 'b.y': 1 + }; + + var projectionFn = LocalCollection._compileProjection(fields); + var filteredDoc = projectionFn(doc); + doc.a.x++; + doc.b.y.z--; + test.equal(filteredDoc.a.x, 42, "projection returning deep copy - including"); + test.equal(filteredDoc.b.y.z, 33, "projection returning deep copy - including"); + + fields = { c: 0 }; + projectionFn = LocalCollection._compileProjection(fields); + filteredDoc = projectionFn(doc); + + doc.a.x = 5; + test.equal(filteredDoc.a.x, 43, "projection returning deep copy - excluding"); +}); + +Tinytest.add("minimongo - observe ordered with projection", function (test) { + // These tests are copy-paste from "minimongo -observe ordered", + // slightly modified to test projection + var operations = []; + var cbs = log_callbacks(operations); + var handle; + + var c = new LocalCollection(); + handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(cbs); + test.isTrue(handle.collection === c); + + c.insert({_id: 'foo', a:1, b:2}); + test.equal(operations.shift(), ['added', {a:1}, 0, null]); + c.update({a:1}, {$set: {a: 2, b: 1}}); + test.equal(operations.shift(), ['changed', {a:2}, 0, {a:1}]); + c.insert({_id: 'bar', a:10, c: 33}); + test.equal(operations.shift(), ['added', {a:10}, 1, null]); + c.update({}, {$inc: {a: 1}}, {multi: true}); + c.update({}, {$inc: {c: 1}}, {multi: true}); + test.equal(operations.shift(), ['changed', {a:3}, 0, {a:2}]); + test.equal(operations.shift(), ['changed', {a:11}, 1, {a:10}]); + c.update({a:11}, {a:1, b:44}); + test.equal(operations.shift(), ['changed', {a:1}, 1, {a:11}]); + test.equal(operations.shift(), ['moved', {a:1}, 1, 0, 'foo']); + c.remove({a:2}); + test.equal(operations.shift(), undefined); + c.remove({a:3}); + test.equal(operations.shift(), ['removed', 'foo', 1, {a:3}]); + + // test stop + handle.stop(); + var idA2 = Random.id(); + c.insert({_id: idA2, a:2}); + test.equal(operations.shift(), undefined); + + var cursor = c.find({}, {fields: {a: 1, _id: 0}}); + test.throws(function () { + cursor.observeChanges({added: function () {}}); + }); + test.throws(function () { + cursor.observe({added: function () {}}); + }); + + // test initial inserts (and backwards sort) + handle = c.find({}, {sort: {a: -1}, fields: { a: 1 } }).observe(cbs); + test.equal(operations.shift(), ['added', {a:2}, 0, null]); + test.equal(operations.shift(), ['added', {a:1}, 1, null]); + handle.stop(); + + // test _suppress_initial + handle = c.find({}, {sort: {a: -1}, fields: { a: 1 }}).observe(Object.assign(cbs, {_suppress_initial: true})); + test.equal(operations.shift(), undefined); + c.insert({a:100, b: { foo: "bar" }}); + test.equal(operations.shift(), ['added', {a:100}, 0, idA2]); + handle.stop(); + + // test skip and limit. + c.remove({}); + handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2, fields: { 'blacklisted': 0 }}).observe(cbs); + test.equal(operations.shift(), undefined); + c.insert({a:1, blacklisted:1324}); + test.equal(operations.shift(), undefined); + c.insert({_id: 'foo', a:2, blacklisted:["something"]}); + test.equal(operations.shift(), ['added', {a:2}, 0, null]); + c.insert({a:3, blacklisted: { 2: 3 }}); + test.equal(operations.shift(), ['added', {a:3}, 1, null]); + c.insert({a:4, blacklisted: 6}); + test.equal(operations.shift(), undefined); + c.update({a:1}, {a:0, blacklisted:4444}); + test.equal(operations.shift(), undefined); + c.update({a:0}, {a:5, blacklisted:11111}); + test.equal(operations.shift(), ['removed', 'foo', 0, {a:2}]); + test.equal(operations.shift(), ['added', {a:4}, 1, null]); + c.update({a:3}, {a:3.5, blacklisted:333.4444}); + test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]); + handle.stop(); + + // test _no_indices + + c.remove({}); + handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(Object.assign(cbs, {_no_indices: true})); + c.insert({_id: 'foo', a:1, zoo: "crazy"}); + test.equal(operations.shift(), ['added', {a:1}, -1, null]); + c.update({a:1}, {$set: {a: 2, foobar: "player"}}); + test.equal(operations.shift(), ['changed', {a:2}, -1, {a:1}]); + c.insert({a:10, b:123.45}); + test.equal(operations.shift(), ['added', {a:10}, -1, null]); + c.update({}, {$inc: {a: 1, b:2}}, {multi: true}); + test.equal(operations.shift(), ['changed', {a:3}, -1, {a:2}]); + test.equal(operations.shift(), ['changed', {a:11}, -1, {a:10}]); + c.update({a:11, b:125.45}, {a:1, b:444}); + test.equal(operations.shift(), ['changed', {a:1}, -1, {a:11}]); + test.equal(operations.shift(), ['moved', {a:1}, -1, -1, 'foo']); + c.remove({a:2}); + test.equal(operations.shift(), undefined); + c.remove({a:3}); + test.equal(operations.shift(), ['removed', 'foo', -1, {a:3}]); + handle.stop(); +}); + + +Tinytest.add("minimongo - ordering", function (test) { + var shortBinary = EJSON.newBinary(1); + shortBinary[0] = 128; + var longBinary1 = EJSON.newBinary(2); + longBinary1[1] = 42; + var longBinary2 = EJSON.newBinary(2); + longBinary2[1] = 50; + + var date1 = new Date; + var date2 = new Date(date1.getTime() + 1000); + + // value ordering + assert_ordering(test, LocalCollection._f._cmp, [ + null, + 1, 2.2, 3, + "03", "1", "11", "2", "a", "aaa", + {}, {a: 2}, {a: 3}, {a: 3, b: 4}, {b: 4}, {b: 4, a: 3}, + {b: {}}, {b: [1, 2, 3]}, {b: [1, 2, 4]}, + [], [1, 2], [1, 2, 3], [1, 2, 4], [1, 2, "4"], [1, 2, [4]], + shortBinary, longBinary1, longBinary2, + new MongoID.ObjectID("1234567890abcd1234567890"), + new MongoID.ObjectID("abcd1234567890abcd123456"), + false, true, + date1, date2 + ]); + + // document ordering under a sort specification + var verify = function (sorts, docs) { + (Array.isArray(sorts) ? sorts : [sorts]).forEach(function (sort) { + var sorter = new Minimongo.Sorter(sort); + assert_ordering(test, sorter.getComparator(), docs); + }); + }; + + // note: [] doesn't sort with "arrays", it sorts as "undefined". the position + // of arrays in _typeorder only matters for things like $lt. (This behavior + // verified with MongoDB 2.2.1.) We don't define the relative order of {a: []} + // and {c: 1} is undefined (MongoDB does seem to care but it's not clear how + // or why). + verify([{"a" : 1}, ["a"], [["a", "asc"]]], + [{a: []}, {a: 1}, {a: {}}, {a: true}]); + verify([{"a" : 1}, ["a"], [["a", "asc"]]], + [{c: 1}, {a: 1}, {a: {}}, {a: true}]); + verify([{"a" : -1}, [["a", "desc"]]], + [{a: true}, {a: {}}, {a: 1}, {c: 1}]); + verify([{"a" : -1}, [["a", "desc"]]], + [{a: true}, {a: {}}, {a: 1}, {a: []}]); + + verify([{"a" : 1, "b": -1}, ["a", ["b", "desc"]], + [["a", "asc"], ["b", "desc"]]], + [{c: 1}, {a: 1, b: 3}, {a: 1, b: 2}, {a: 2, b: 0}]); + + verify([{"a" : 1, "b": 1}, ["a", "b"], + [["a", "asc"], ["b", "asc"]]], + [{c: 1}, {a: 1, b: 2}, {a: 1, b: 3}, {a: 2, b: 0}]); + + test.throws(function () { + new Minimongo.Sorter("a"); + }); + + test.throws(function () { + new Minimongo.Sorter(123); + }); + + // We don't support $natural:1 (since we don't actually have Mongo's on-disk + // ordering available!) + test.throws(function () { + new Minimongo.Sorter({$natural: 1}); + }); + + // No sort spec implies everything equal. + test.equal(new Minimongo.Sorter({}).getComparator()({a:1}, {a:2}), 0); + + // All sorts of array edge cases! + // Increasing sort sorts by the smallest element it finds; 1 < 2. + verify({a: 1}, [ + {a: [1, 10, 20]}, + {a: [5, 2, 99]} + ]); + // Decreasing sorts by largest it finds; 99 > 20. + verify({a: -1}, [ + {a: [5, 2, 99]}, + {a: [1, 10, 20]} + ]); + // Can also sort by specific array indices. + verify({'a.1': 1}, [ + {a: [5, 2, 99]}, + {a: [1, 10, 20]} + ]); + // We do NOT expand sub-arrays, so the minimum in the second doc is 5, not + // -20. (Numbers always sort before arrays.) + verify({a: 1}, [ + {a: [1, [10, 15], 20]}, + {a: [5, [-5, -20], 18]} + ]); + // The maximum in each of these is the array, since arrays are "greater" than + // numbers. And [10, 15] is greater than [-5, -20]. + verify({a: -1}, [ + {a: [1, [10, 15], 20]}, + {a: [5, [-5, -20], 18]} + ]); + // 'a.0' here ONLY means "first element of a", not "first element of something + // found in a", so it CANNOT find the 10 or -5. + verify({'a.0': 1}, [ + {a: [1, [10, 15], 20]}, + {a: [5, [-5, -20], 18]} + ]); + verify({'a.0': -1}, [ + {a: [5, [-5, -20], 18]}, + {a: [1, [10, 15], 20]} + ]); + // Similarly, this is just comparing [-5,-20] to [10, 15]. + verify({'a.1': 1}, [ + {a: [5, [-5, -20], 18]}, + {a: [1, [10, 15], 20]} + ]); + verify({'a.1': -1}, [ + {a: [1, [10, 15], 20]}, + {a: [5, [-5, -20], 18]} + ]); + // Here we are just comparing [10,15] directly to [19,3] (and NOT also + // iterating over the numbers; this is implemented by setting dontIterate in + // makeLookupFunction). So [10,15]<[19,3] even though 3 is the smallest + // number you can find there. + verify({'a.1': 1}, [ + {a: [1, [10, 15], 20]}, + {a: [5, [19, 3], 18]} + ]); + verify({'a.1': -1}, [ + {a: [5, [19, 3], 18]}, + {a: [1, [10, 15], 20]} + ]); + // Minimal elements are 1 and 5. + verify({a: 1}, [ + {a: [1, [10, 15], 20]}, + {a: [5, [19, 3], 18]} + ]); + // Maximal elements are [19,3] and [10,15] (because arrays sort higher than + // numbers), even though there's a 20 floating around. + verify({a: -1}, [ + {a: [5, [19, 3], 18]}, + {a: [1, [10, 15], 20]} + ]); + // Maximal elements are [10,15] and [3,19]. [10,15] is bigger even though 19 + // is the biggest number in them, because array comparison is lexicographic. + verify({a: -1}, [ + {a: [1, [10, 15], 20]}, + {a: [5, [3, 19], 18]} + ]); + + // (0,4) < (0,5), so they go in this order. It's not correct to consider + // (0,3) as a sort key for the second document because they come from + // different a-branches. + verify({'a.x': 1, 'a.y': 1}, [ + {a: [{x: 0, y: 4}]}, + {a: [{x: 0, y: 5}, {x: 1, y: 3}]} + ]); + + verify({'a.0.s': 1}, [ + {a: [ {s: 1} ]}, + {a: [ {s: 2} ]} + ]); +}); + +Tinytest.add("minimongo - sort", function (test) { + var c = new LocalCollection(); + for (var i = 0; i < 50; i++) + for (var j = 0; j < 2; j++) + c.insert({a: i, b: j, _id: i + "_" + j}); + + test.equal( + c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, limit: 5}).fetch(), [ + {a: 11, b: 1, _id: "11_1"}, + {a: 12, b: 1, _id: "12_1"}, + {a: 13, b: 1, _id: "13_1"}, + {a: 14, b: 1, _id: "14_1"}, + {a: 15, b: 1, _id: "15_1"}]); + + test.equal( + c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, skip: 3, limit: 5}).fetch(), [ + {a: 14, b: 1, _id: "14_1"}, + {a: 15, b: 1, _id: "15_1"}, + {a: 16, b: 1, _id: "16_1"}, + {a: 17, b: 1, _id: "17_1"}, + {a: 18, b: 1, _id: "18_1"}]); + + test.equal( + c.find({a: {$gte: 20}}, {sort: {a: 1, b: -1}, skip: 50, limit: 5}).fetch(), [ + {a: 45, b: 1, _id: "45_1"}, + {a: 45, b: 0, _id: "45_0"}, + {a: 46, b: 1, _id: "46_1"}, + {a: 46, b: 0, _id: "46_0"}, + {a: 47, b: 1, _id: "47_1"}]); +}); + +Tinytest.add("minimongo - subkey sort", function (test) { + var c = new LocalCollection(); + + // normal case + c.insert({a: {b: 2}}); + c.insert({a: {b: 1}}); + c.insert({a: {b: 3}}); + test.equal( + c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), + [{b: 3}, {b: 2}, {b: 1}]); + + // isn't an object + c.insert({a: 1}); + test.equal( + c.find({}, {sort: {'a.b': 1}}).fetch().map(function (doc) { return doc.a; }), + [1, {b: 1}, {b: 2}, {b: 3}]); + + // complex object + c.insert({a: {b: {c: 1}}}); + test.equal( + c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), + [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1]); + + // no such top level prop + c.insert({c: 1}); + test.equal( + c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), + [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1, undefined]); + + // no such mid level prop. just test that it doesn't throw. + test.equal(c.find({}, {sort: {'a.nope.c': -1}}).count(), 6); +}); + +Tinytest.add("minimongo - array sort", function (test) { + var c = new LocalCollection(); + + // "up" and "down" are the indices that the docs should have when sorted + // ascending and descending by "a.x" respectively. They are not reverses of + // each other: when sorting ascending, you use the minimum value you can find + // in the document, and when sorting descending, you use the maximum value you + // can find. So [1, 4] shows up in the 1 slot when sorting ascending and the 4 + // slot when sorting descending. + // + // Similarly, "selected" is the index that the doc should have in the query + // that sorts ascending on "a.x" and selects {'a.x': {$gt: 1}}. In this case, + // the 1 in [1, 4] may not be used as a sort key. + c.insert({up: 1, down: 1, selected: 2, a: {x: [1, 4]}}); + c.insert({up: 2, down: 2, selected: 0, a: [{x: [2]}, {x: 3}]}); + c.insert({up: 0, down: 4, a: {x: 0}}); + c.insert({up: 3, down: 3, selected: 1, a: {x: 2.5}}); + c.insert({up: 4, down: 0, selected: 3, a: {x: 5}}); + + // Test that the the documents in "cursor" contain values with the name + // "field" running from 0 to the max value of that name in the collection. + var testCursorMatchesField = function (cursor, field) { + var fieldValues = []; + c.find().forEach(function (doc) { + if (doc.hasOwnProperty(field)) + fieldValues.push(doc[field]); + }); + test.equal(cursor.fetch().map(function (doc) { return doc[field]; }), + Array.from({length: Math.max.apply(null, fieldValues) + 1}, function (x, i) { return i; })); + }; + + testCursorMatchesField(c.find({}, {sort: {'a.x': 1}}), 'up'); + testCursorMatchesField(c.find({}, {sort: {'a.x': -1}}), 'down'); + testCursorMatchesField(c.find({'a.x': {$gt: 1}}, {sort: {'a.x': 1}}), + 'selected'); +}); + +Tinytest.add("minimongo - sort keys", function (test) { + var keyListToObject = function (keyList) { + var obj = {}; + keyList.forEach(function (key) { + obj[EJSON.stringify(key)] = true; + }); + return obj; + }; + + var testKeys = function (sortSpec, doc, expectedKeyList) { + var expectedKeys = keyListToObject(expectedKeyList); + var sorter = new Minimongo.Sorter(sortSpec); + + var actualKeyList = []; + sorter._generateKeysFromDoc(doc, function (key) { + actualKeyList.push(key); + }); + var actualKeys = keyListToObject(actualKeyList); + test.equal(actualKeys, expectedKeys); + }; + + var testParallelError = function (sortSpec, doc) { + var sorter = new Minimongo.Sorter(sortSpec); + test.throws(function () { + sorter._generateKeysFromDoc(doc, function (){}); + }, /parallel arrays/); + }; + + // Just non-array fields. + testKeys({'a.x': 1, 'a.y': 1}, + {a: {x: 0, y: 5}}, + [[0,5]]); + + // Ensure that we don't get [0,3] and [1,5]. + testKeys({'a.x': 1, 'a.y': 1}, + {a: [{x: 0, y: 5}, {x: 1, y: 3}]}, + [[0,5], [1,3]]); + + // Ensure we can combine "array fields" with "non-array fields". + testKeys({'a.x': 1, 'a.y': 1, b: -1}, + {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, + [[0,5,42], [1,3,42]]); + testKeys({b: -1, 'a.x': 1, 'a.y': 1}, + {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, + [[42,0,5], [42,1,3]]); + testKeys({'a.x': 1, b: -1, 'a.y': 1}, + {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, + [[0,42,5], [1,42,3]]); + testKeys({a: 1, b: 1}, + {a: [1, 2, 3], b: 42}, + [[1,42], [2,42], [3,42]]); + + // Don't support multiple arrays at the same level. + testParallelError({a: 1, b: 1}, + {a: [1, 2, 3], b: [42]}); + + // We are MORE STRICT than Mongo here; Mongo supports this! + // XXX support this too #NestedArraySort + testParallelError({'a.x': 1, 'a.y': 1}, + {a: [{x: 1, y: [2, 3]}, + {x: 2, y: [4, 5]}]}); +}); + +Tinytest.add("minimongo - sort key filter", function (test) { + var testOrder = function (sortSpec, selector, doc1, doc2) { + var matcher = new Minimongo.Matcher(selector); + var sorter = new Minimongo.Sorter(sortSpec, {matcher: matcher}); + var comparator = sorter.getComparator(); + var comparison = comparator(doc1, doc2); + test.isTrue(comparison < 0); + }; + + testOrder({'a.x': 1}, {'a.x': {$gt: 1}}, + {a: {x: 3}}, + {a: {x: [1, 4]}}); + testOrder({'a.x': 1}, {'a.x': {$gt: 0}}, + {a: {x: [1, 4]}}, + {a: {x: 3}}); + + var keyCompatible = function (sortSpec, selector, key, compatible) { + var matcher = new Minimongo.Matcher(selector); + var sorter = new Minimongo.Sorter(sortSpec, {matcher: matcher}); + var actual = sorter._keyCompatibleWithSelector(key); + test.equal(actual, compatible); + }; + + keyCompatible({a: 1}, {a: 5}, [5], true); + keyCompatible({a: 1}, {a: 5}, [8], false); + keyCompatible({a: 1}, {a: {x: 5}}, [{x: 5}], true); + keyCompatible({a: 1}, {a: {x: 5}}, [{x: 5, y: 9}], false); + keyCompatible({'a.x': 1}, {a: {x: 5}}, [5], true); + // To confirm this: + // > db.x.insert({_id: "q", a: [{x:1}, {x:5}], b: 2}) + // > db.x.insert({_id: "w", a: [{x:5}, {x:10}], b: 1}) + // > db.x.find({}).sort({'a.x': 1, b: 1}) + // { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 } + // { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 } + // > db.x.find({a: {x:5}}).sort({'a.x': 1, b: 1}) + // { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 } + // { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 } + // > db.x.find({'a.x': 5}).sort({'a.x': 1, b: 1}) + // { "_id" : "w", "a" : [ { "x" : 5 }, { "x" : 10 } ], "b" : 1 } + // { "_id" : "q", "a" : [ { "x" : 1 }, { "x" : 5 } ], "b" : 2 } + // ie, only the last one manages to trigger the key compatibility code, + // not the previous one. (The "b" sort is necessary because when the key + // compatibility code *does* kick in, both documents only end up with "5" + // for the first field as their only sort key, and we need to differentiate + // somehow...) + keyCompatible({'a.x': 1}, {a: {x: 5}}, [1], true); + keyCompatible({'a.x': 1}, {'a.x': 5}, [5], true); + keyCompatible({'a.x': 1}, {'a.x': 5}, [1], false); + + // Regex key check. + keyCompatible({a: 1}, {a: /^foo+/}, ['foo'], true); + keyCompatible({a: 1}, {a: /^foo+/}, ['foooo'], true); + keyCompatible({a: 1}, {a: /^foo+/}, ['foooobar'], true); + keyCompatible({a: 1}, {a: /^foo+/}, ['afoooo'], false); + keyCompatible({a: 1}, {a: /^foo+/}, [''], false); + keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foo'], true); + keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foooo'], true); + keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foooobar'], true); + keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['afoooo'], false); + keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, [''], false); + + keyCompatible({a: 1}, {a: /^foo+/i}, ['foo'], true); + // Key compatibility check appears to be turned off for regexps with flags. + keyCompatible({a: 1}, {a: /^foo+/i}, ['bar'], true); + keyCompatible({a: 1}, {a: /^foo+/m}, ['bar'], true); + keyCompatible({a: 1}, {a: {$regex: "^foo+", $options: "i"}}, ['bar'], true); + keyCompatible({a: 1}, {a: {$regex: "^foo+", $options: "m"}}, ['bar'], true); + + // Multiple keys! + keyCompatible({a: 1, b: 1, c: 1}, + {a: {$gt: 5}, c: {$lt: 3}}, [6, "bla", 2], true); + keyCompatible({a: 1, b: 1, c: 1}, + {a: {$gt: 5}, c: {$lt: 3}}, [6, "bla", 4], false); + keyCompatible({a: 1, b: 1, c: 1}, + {a: {$gt: 5}, c: {$lt: 3}}, [3, "bla", 1], false); + // No filtering is done (ie, all keys are compatible) if the first key isn't + // constrained. + keyCompatible({a: 1, b: 1, c: 1}, + {c: {$lt: 3}}, [3, "bla", 4], true); +}); + +Tinytest.add("minimongo - sort function", function (test) { + var c = new LocalCollection(); + + c.insert({a: 1}); + c.insert({a: 10}); + c.insert({a: 5}); + c.insert({a: 7}); + c.insert({a: 2}); + c.insert({a: 4}); + c.insert({a: 3}); + + var sortFunction = function (doc1, doc2) { + return doc2.a - doc1.a; + }; + + test.equal(c.find({}, {sort: sortFunction}).fetch(), c.find({}).fetch().sort(sortFunction)); + test.notEqual(c.find({}).fetch(), c.find({}).fetch().sort(sortFunction)); + test.equal(c.find({}, {sort: {a: -1}}).fetch(), c.find({}).fetch().sort(sortFunction)); +}); + +Tinytest.add("minimongo - binary search", function (test) { + var forwardCmp = function (a, b) { + return a - b; + }; + + var backwardCmp = function (a, b) { + return -1 * forwardCmp(a, b); + }; + + var checkSearch = function (cmp, array, value, expected, message) { + var actual = LocalCollection._binarySearch(cmp, array, value); + if (expected != actual) { + test.fail({type: "minimongo-binary-search", + message: message + " : Expected index " + expected + + " but had " + actual + }); + } + }; + + var checkSearchForward = function (array, value, expected, message) { + checkSearch(forwardCmp, array, value, expected, message); + }; + var checkSearchBackward = function (array, value, expected, message) { + checkSearch(backwardCmp, array, value, expected, message); + }; + + checkSearchForward([1, 2, 5, 7], 4, 2, "Inner insert"); + checkSearchForward([1, 2, 3, 4], 3, 3, "Inner insert, equal value"); + checkSearchForward([1, 2, 5], 4, 2, "Inner insert, odd length"); + checkSearchForward([1, 3, 5, 6], 9, 4, "End insert"); + checkSearchForward([1, 3, 5, 6], 0, 0, "Beginning insert"); + checkSearchForward([1], 0, 0, "Single array, less than."); + checkSearchForward([1], 1, 1, "Single array, equal."); + checkSearchForward([1], 2, 1, "Single array, greater than."); + checkSearchForward([], 1, 0, "Empty array"); + checkSearchForward([1, 1, 1, 2, 2, 2, 2], 1, 3, "Highly degenerate array, lower"); + checkSearchForward([1, 1, 1, 2, 2, 2, 2], 2, 7, "Highly degenerate array, upper"); + checkSearchForward([2, 2, 2, 2, 2, 2, 2], 1, 0, "Highly degenerate array, lower"); + checkSearchForward([2, 2, 2, 2, 2, 2, 2], 2, 7, "Highly degenerate array, equal"); + checkSearchForward([2, 2, 2, 2, 2, 2, 2], 3, 7, "Highly degenerate array, upper"); + + checkSearchBackward([7, 5, 2, 1], 4, 2, "Backward: Inner insert"); + checkSearchBackward([4, 3, 2, 1], 3, 2, "Backward: Inner insert, equal value"); + checkSearchBackward([5, 2, 1], 4, 1, "Backward: Inner insert, odd length"); + checkSearchBackward([6, 5, 3, 1], 9, 0, "Backward: Beginning insert"); + checkSearchBackward([6, 5, 3, 1], 0, 4, "Backward: End insert"); + checkSearchBackward([1], 0, 1, "Backward: Single array, less than."); + checkSearchBackward([1], 1, 1, "Backward: Single array, equal."); + checkSearchBackward([1], 2, 0, "Backward: Single array, greater than."); + checkSearchBackward([], 1, 0, "Backward: Empty array"); + checkSearchBackward([2, 2, 2, 2, 1, 1, 1], 1, 7, "Backward: Degenerate array, lower"); + checkSearchBackward([2, 2, 2, 2, 1, 1, 1], 2, 4, "Backward: Degenerate array, upper"); + checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 1, 7, "Backward: Highly degenerate array, upper"); + checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 2, 7, "Backward: Highly degenerate array, upper"); + checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 3, 0, "Backward: Highly degenerate array, upper"); +}); + +Tinytest.add("minimongo - modify", function (test) { + var modifyWithQuery = function (doc, query, mod, expected) { + var coll = new LocalCollection; + coll.insert(doc); + // The query is relevant for 'a.$.b'. + coll.update(query, mod); + var actual = coll.findOne(); + delete actual._id; // added by insert + + if (typeof expected === "function") { + expected(actual, EJSON.stringify({input: doc, mod: mod})); + } else { + test.equal(actual, expected, EJSON.stringify({input: doc, mod: mod})); + } + }; + var modify = function (doc, mod, expected) { + modifyWithQuery(doc, {}, mod, expected); + }; + var exceptionWithQuery = function (doc, query, mod) { + var coll = new LocalCollection; + coll.insert(doc); + test.throws(function () { + coll.update(query, mod); + }); + }; + var exception = function (doc, mod) { + exceptionWithQuery(doc, {}, mod); + }; + + var upsert = function (query, mod, expected) { + var coll = new LocalCollection; + + var result = coll.upsert(query, mod); + + var actual = coll.findOne(); + + if (expected._id) { + test.equal(result.insertedId, expected._id); + } + else { + delete actual._id; + } + + test.equal(actual, expected); + }; + + var upsertException = function (query, mod) { + var coll = new LocalCollection; + test.throws(function(){ + coll.upsert(query, mod); + }); + }; + + // document replacement + modify({}, {}, {}); + modify({a: 12}, {}, {}); // tested against mongodb + modify({a: 12}, {a: 13}, {a:13}); + modify({a: 12, b: 99}, {a: 13}, {a:13}); + exception({a: 12}, {a: 13, $set: {b: 13}}); + exception({a: 12}, {$set: {b: 13}, a: 13}); + + exception({a: 12}, {$a: 13}); //invalid operator + exception({a: 12}, {b:{$a: 13}}); + exception({a: 12}, {b:{'a.b': 13}}); + exception({a: 12}, {b:{'\0a': 13}}); + + // keys + modify({}, {$set: {'a': 12}}, {a: 12}); + modify({}, {$set: {'a.b': 12}}, {a: {b: 12}}); + modify({}, {$set: {'a.b.c': 12}}, {a: {b: {c: 12}}}); + modify({a: {d: 99}}, {$set: {'a.b.c': 12}}, {a: {d: 99, b: {c: 12}}}); + modify({}, {$set: {'a.b.3.c': 12}}, {a: {b: {3: {c: 12}}}}); + modify({a: {b: []}}, {$set: {'a.b.3.c': 12}}, { + a: {b: [null, null, null, {c: 12}]}}); + exception({a: [null, null, null]}, {$set: {'a.1.b': 12}}); + exception({a: [null, 1, null]}, {$set: {'a.1.b': 12}}); + exception({a: [null, "x", null]}, {$set: {'a.1.b': 12}}); + exception({a: [null, [], null]}, {$set: {'a.1.b': 12}}); + modify({a: [null, null, null]}, {$set: {'a.3.b': 12}}, { + a: [null, null, null, {b: 12}]}); + exception({a: []}, {$set: {'a.b': 12}}); + exception({a: 12}, {$set: {'a.b': 99}}); // tested on mongo + exception({a: 'x'}, {$set: {'a.b': 99}}); + exception({a: true}, {$set: {'a.b': 99}}); + exception({a: null}, {$set: {'a.b': 99}}); + modify({a: {}}, {$set: {'a.3': 12}}, {a: {'3': 12}}); + modify({a: []}, {$set: {'a.3': 12}}, {a: [null, null, null, 12]}); + exception({}, {$set: {'': 12}}); // tested on mongo + exception({}, {$set: {'.': 12}}); // tested on mongo + exception({}, {$set: {'a.': 12}}); // tested on mongo + exception({}, {$set: {'. ': 12}}); // tested on mongo + exception({}, {$inc: {'... ': 12}}); // tested on mongo + exception({}, {$set: {'a..b': 12}}); // tested on mongo + modify({a: [1,2,3]}, {$set: {'a.01': 99}}, {a: [1, 99, 3]}); + modify({a: [1,{a: 98},3]}, {$set: {'a.01.b': 99}}, {a: [1,{a:98, b: 99},3]}); + modify({}, {$set: {'2.a.b': 12}}, {'2': {'a': {'b': 12}}}); // tested + exception({x: []}, {$set: {'x.2..a': 99}}); + modify({x: [null, null]}, {$set: {'x.2.a': 1}}, {x: [null, null, {a: 1}]}); + exception({x: [null, null]}, {$set: {'x.1.a': 1}}); + + // a.$.b + modifyWithQuery({a: [{x: 2}, {x: 4}]}, {'a.x': 4}, {$set: {'a.$.z': 9}}, + {a: [{x: 2}, {x: 4, z: 9}]}); + exception({a: [{x: 2}, {x: 4}]}, {$set: {'a.$.z': 9}}); + exceptionWithQuery({a: [{x: 2}, {x: 4}], b: 5}, {b: 5}, {$set: {'a.$.z': 9}}); + // can't have two $ + exceptionWithQuery({a: [{x: [2]}]}, {'a.x': 2}, {$set: {'a.$.x.$': 9}}); + modifyWithQuery({a: [5, 6, 7]}, {a: 6}, {$set: {'a.$': 9}}, {a: [5, 9, 7]}); + modifyWithQuery({a: [{b: [{c: 9}, {c: 10}]}, {b: {c: 11}}]}, {'a.b.c': 10}, + {$unset: {'a.$.b': 1}}, {a: [{}, {b: {c: 11}}]}); + modifyWithQuery({a: [{b: [{c: 9}, {c: 10}]}, {b: {c: 11}}]}, {'a.b.c': 11}, + {$unset: {'a.$.b': 1}}, + {a: [{b: [{c: 9}, {c: 10}]}, {}]}); + modifyWithQuery({a: [1]}, {'a.0': 1}, {$set: {'a.$': 5}}, {a: [5]}); + modifyWithQuery({a: [9]}, {a: {$mod: [2, 1]}}, {$set: {'a.$': 5}}, {a: [5]}); + // Negatives don't set '$'. + exceptionWithQuery({a: [1]}, {$not: {a: 2}}, {$set: {'a.$': 5}}); + exceptionWithQuery({a: [1]}, {'a.0': {$ne: 2}}, {$set: {'a.$': 5}}); + // One $or clause works. + modifyWithQuery({a: [{x: 2}, {x: 4}]}, + {$or: [{'a.x': 4}]}, {$set: {'a.$.z': 9}}, + {a: [{x: 2}, {x: 4, z: 9}]}); + // More $or clauses throw. + exceptionWithQuery({a: [{x: 2}, {x: 4}]}, + {$or: [{'a.x': 4}, {'a.x': 4}]}, + {$set: {'a.$.z': 9}}); + // $and uses the last one. + modifyWithQuery({a: [{x: 1}, {x: 3}]}, + {$and: [{'a.x': 1}, {'a.x': 3}]}, + {$set: {'a.$.x': 5}}, + {a: [{x: 1}, {x: 5}]}); + modifyWithQuery({a: [{x: 1}, {x: 3}]}, + {$and: [{'a.x': 3}, {'a.x': 1}]}, + {$set: {'a.$.x': 5}}, + {a: [{x: 5}, {x: 3}]}); + // Same goes for the implicit AND of a document selector. + modifyWithQuery({a: [{x: 1}, {y: 3}]}, + {'a.x': 1, 'a.y': 3}, + {$set: {'a.$.z': 5}}, + {a: [{x: 1}, {y: 3, z: 5}]}); + modifyWithQuery({a: [{x: 1}, {y: 1}, {x: 1, y: 1}]}, + {a: {$elemMatch: {x: 1, y: 1}}}, + {$set: {'a.$.x': 2}}, + {a: [{x: 1}, {y: 1}, {x: 2, y: 1}]}); + modifyWithQuery({a: [{b: [{x: 1}, {y: 1}, {x: 1, y: 1}]}]}, + {'a.b': {$elemMatch: {x: 1, y: 1}}}, + {$set: {'a.$.b': 3}}, + {a: [{b: 3}]}); + // with $near, make sure it does not find the closest one (#3599) + modifyWithQuery({a: []}, + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[]}); + modifyWithQuery({a: [{b: [ [3,3], [4,4] ]}]}, + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"}]}); + modifyWithQuery({a: [{b: [1,1]}, + {b: [ [3,3], [4,4] ]}, + {b: [9,9]}]}, + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); + modifyWithQuery({a: [{b: [1,1]}, + {b: [ [3,3], [4,4] ]}, + {b: [9,9]}]}, + {'a.b': {$near: [9, 9], $maxDistance: 1}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); + modifyWithQuery({a: [{b: [1,1]}, + {b: [ [3,3], [4,4] ]}, + {b: [9,9]}]}, + {'a.b': {$near: [9, 9]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); + modifyWithQuery({a: [{b: [9,9]}, + {b: [ [3,3], [4,4] ]}, + {b: [9,9]}]}, + {'a.b': {$near: [9, 9]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); + modifyWithQuery({a: [{b:[4,3]}, + {c: [1,1]}]}, + {'a.c': {$near: [1, 1]}}, + {$set: {'a.$.c': 'k'}}, + {"a":[{"c": "k", "b":[4,3]},{"c":[1,1]}]}); + modifyWithQuery({a: [{c: [9,9]}, + {b: [ [3,3], [4,4] ]}, + {b: [1,1]}]}, + {'a.b': {$near: [1, 1]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"c": [9,9], "b":"k"},{"b": [ [3,3], [4,4]]},{"b":[1,1]}]}); + modifyWithQuery({a: [{c: [9,9], b:[4,3]}, + {b: [ [3,3], [4,4] ]}, + {b: [1,1]}]}, + {'a.b': {$near: [1, 1]}}, + {$set: {'a.$.b': 'k'}}, + {"a":[{"c": [9,9], "b":"k"},{"b": [ [3,3], [4,4]]},{"b":[1,1]}]}); + + // $inc + modify({a: 1, b: 2}, {$inc: {a: 10}}, {a: 11, b: 2}); + modify({a: 1, b: 2}, {$inc: {c: 10}}, {a: 1, b: 2, c: 10}); + exception({a: 1}, {$inc: {a: '10'}}); + exception({a: 1}, {$inc: {a: true}}); + exception({a: 1}, {$inc: {a: [10]}}); + exception({a: '1'}, {$inc: {a: 10}}); + exception({a: [1]}, {$inc: {a: 10}}); + exception({a: {}}, {$inc: {a: 10}}); + exception({a: false}, {$inc: {a: 10}}); + exception({a: null}, {$inc: {a: 10}}); + modify({a: [1, 2]}, {$inc: {'a.1': 10}}, {a: [1, 12]}); + modify({a: [1, 2]}, {$inc: {'a.2': 10}}, {a: [1, 2, 10]}); + modify({a: [1, 2]}, {$inc: {'a.3': 10}}, {a: [1, 2, null, 10]}); + modify({a: {b: 2}}, {$inc: {'a.b': 10}}, {a: {b: 12}}); + modify({a: {b: 2}}, {$inc: {'a.c': 10}}, {a: {b: 2, c: 10}}); + exception({}, {$inc: {_id: 1}}); + + // $currentDate + modify({}, {$currentDate: {a: true}}, (result, msg) => { test.instanceOf(result.a,Date,msg) }); + modify({}, {$currentDate: {a: {$type: "date"}}}, (result, msg) => { test.instanceOf(result.a,Date,msg) }); + exception({}, {$currentDate: {a: false}}); + exception({}, {$currentDate: {a: {}}}); + exception({}, {$currentDate: {a: {$type: "timestamp"}}}); + + // $min + modify({a: 1, b: 2}, {$min: {b: 1}}, {a: 1, b: 1}); + modify({a: 1, b: 2}, {$min: {b: 3}}, {a: 1, b: 2}); + modify({a: 1, b: 2}, {$min: {c: 10}}, {a: 1, b: 2, c: 10}); + exception({a: 1}, {$min: {a: '10'}}); + exception({a: 1}, {$min: {a: true}}); + exception({a: 1}, {$min: {a: [10]}}); + exception({a: '1'}, {$min: {a: 10}}); + exception({a: [1]}, {$min: {a: 10}}); + exception({a: {}}, {$min: {a: 10}}); + exception({a: false}, {$min: {a: 10}}); + exception({a: null}, {$min: {a: 10}}); + modify({a: [1, 2]}, {$min: {'a.1': 1}}, {a: [1, 1]}); + modify({a: [1, 2]}, {$min: {'a.1': 3}}, {a: [1, 2]}); + modify({a: [1, 2]}, {$min: {'a.2': 10}}, {a: [1, 2, 10]}); + modify({a: [1, 2]}, {$min: {'a.3': 10}}, {a: [1, 2, null, 10]}); + modify({a: {b: 2}}, {$min: {'a.b': 1}}, {a: {b: 1}}); + modify({a: {b: 2}}, {$min: {'a.c': 10}}, {a: {b: 2, c: 10}}); + exception({}, {$min: {_id: 1}}); + + // $max + modify({a: 1, b: 2}, {$max: {b: 1}}, {a: 1, b: 2}); + modify({a: 1, b: 2}, {$max: {b: 3}}, {a: 1, b: 3}); + modify({a: 1, b: 2}, {$max: {c: 10}}, {a: 1, b: 2, c: 10}); + exception({a: 1}, {$max: {a: '10'}}); + exception({a: 1}, {$max: {a: true}}); + exception({a: 1}, {$max: {a: [10]}}); + exception({a: '1'}, {$max: {a: 10}}); + exception({a: [1]}, {$max: {a: 10}}); + exception({a: {}}, {$max: {a: 10}}); + exception({a: false}, {$max: {a: 10}}); + exception({a: null}, {$max: {a: 10}}); + modify({a: [1, 2]}, {$max: {'a.1': 3}}, {a: [1, 3]}); + modify({a: [1, 2]}, {$max: {'a.1': 1}}, {a: [1, 2]}); + modify({a: [1, 2]}, {$max: {'a.2': 10}}, {a: [1, 2, 10]}); + modify({a: [1, 2]}, {$max: {'a.3': 10}}, {a: [1, 2, null, 10]}); + modify({a: {b: 2}}, {$max: {'a.b': 3}}, {a: {b: 3}}); + modify({a: {b: 2}}, {$max: {'a.c': 10}}, {a: {b: 2, c: 10}}); + exception({}, {$max: {_id: 1}}); + + // $set + modify({a: 1, b: 2}, {$set: {a: 10}}, {a: 10, b: 2}); + modify({a: 1, b: 2}, {$set: {c: 10}}, {a: 1, b: 2, c: 10}); + modify({a: 1, b: 2}, {$set: {a: {c: 10}}}, {a: {c: 10}, b: 2}); + modify({a: [1, 2], b: 2}, {$set: {a: [3, 4]}}, {a: [3, 4], b: 2}); + modify({a: [1, 2, 3], b: 2}, {$set: {'a.1': [3, 4]}}, + {a: [1, [3, 4], 3], b:2}); + modify({a: [1], b: 2}, {$set: {'a.1': 9}}, {a: [1, 9], b: 2}); + modify({a: [1], b: 2}, {$set: {'a.2': 9}}, {a: [1, null, 9], b: 2}); + modify({a: {b: 1}}, {$set: {'a.c': 9}}, {a: {b: 1, c: 9}}); + modify({}, {$set: {'x._id': 4}}, {x: {_id: 4}}); + exception({}, {$set: {_id: 4}}); + exception({_id: 4}, {$set: {_id: 4}}); // even not-changing _id is bad + //restricted field names + exception({a:{}}, {$set:{a:{$a:1}}}); + exception({ a: {} }, { $set: { a: { c: + [{ b: { $a: 1 } }] } } }); + exception({a:{}}, {$set:{a:{'\0a':1}}}); + exception({a:{}}, {$set:{a:{'a.b':1}}}); + + // $unset + modify({}, {$unset: {a: 1}}, {}); + modify({a: 1}, {$unset: {a: 1}}, {}); + modify({a: 1, b: 2}, {$unset: {a: 1}}, {b: 2}); + modify({a: 1, b: 2}, {$unset: {a: 0}}, {b: 2}); + modify({a: 1, b: 2}, {$unset: {a: false}}, {b: 2}); + modify({a: 1, b: 2}, {$unset: {a: null}}, {b: 2}); + modify({a: 1, b: 2}, {$unset: {a: [1]}}, {b: 2}); + modify({a: 1, b: 2}, {$unset: {a: {}}}, {b: 2}); + modify({a: {b: 2, c: 3}}, {$unset: {'a.b': 1}}, {a: {c: 3}}); + modify({a: [1, 2, 3]}, {$unset: {'a.1': 1}}, {a: [1, null, 3]}); // tested + modify({a: [1, 2, 3]}, {$unset: {'a.2': 1}}, {a: [1, 2, null]}); // tested + modify({a: [1, 2, 3]}, {$unset: {'a.x': 1}}, {a: [1, 2, 3]}); // tested + modify({a: {b: 1}}, {$unset: {'a.b.c.d': 1}}, {a: {b: 1}}); + modify({a: {b: 1}}, {$unset: {'a.x.c.d': 1}}, {a: {b: 1}}); + modify({a: {b: {c: 1}}}, {$unset: {'a.b.c': 1}}, {a: {b: {}}}); + exception({}, {$unset: {_id: 1}}); + + // $push + modify({}, {$push: {a: 1}}, {a: [1]}); + modify({a: []}, {$push: {a: 1}}, {a: [1]}); + modify({a: [1]}, {$push: {a: 2}}, {a: [1, 2]}); + exception({a: true}, {$push: {a: 1}}); + modify({a: [1]}, {$push: {a: [2]}}, {a: [1, [2]]}); + modify({a: []}, {$push: {'a.1': 99}}, {a: [null, [99]]}); // tested + modify({a: {}}, {$push: {'a.x': 99}}, {a: {x: [99]}}); + modify({}, {$push: {a: {$each: [1, 2, 3]}}}, + {a: [1, 2, 3]}); + modify({a: []}, {$push: {a: {$each: [1, 2, 3]}}}, + {a: [1, 2, 3]}); + modify({a: [true]}, {$push: {a: {$each: [1, 2, 3]}}}, + {a: [true, 1, 2, 3]}); + modify({a: [true]}, {$push: {a: {$each: [1, 2, 3], $slice: -2}}}, + {a: [2, 3]}); + modify({a: [false, true]}, {$push: {a: {$each: [1], $slice: -2}}}, + {a: [true, 1]}); + modify( + {a: [{x: 3}, {x: 1}]}, + {$push: {a: { + $each: [{x: 4}, {x: 2}], + $slice: -2, + $sort: {x: 1} + }}}, + {a: [{x: 3}, {x: 4}]}); + modify({}, {$push: {a: {$each: [1, 2, 3], $slice: 0}}}, {a: []}); + modify({a: [1, 2]}, {$push: {a: {$each: [1, 2, 3], $slice: 0}}}, {a: []}); + // $push with $position modifier + // No negative number for $position + exception({a: []}, {$push: {a: {$each: [0], $position: -1}}}); + modify({a: [1, 2]}, {$push: {a: {$each: [0], $position: 0}}}, + {a: [0, 1, 2]}); + modify({a: [1, 2]}, {$push: {a: {$each: [-1, 0], $position: 0}}}, + {a: [-1, 0, 1, 2]}); + modify({a: [1, 3]}, {$push: {a: {$each: [2], $position: 1}}}, {a: [1, 2, 3]}); + modify({a: [1, 4]}, {$push: {a: {$each: [2, 3], $position: 1}}}, + {a: [1, 2, 3, 4]}); + modify({a: [1, 2]}, {$push: {a: {$each: [3], $position: 3}}}, {a: [1, 2, 3]}); + modify({a: [1, 2]}, {$push: {a: {$each: [3], $position: 99}}}, + {a: [1, 2, 3]}); + modify({a: [1, 2]}, {$push: {a: {$each: [3], $position: 99, $slice: -2}}}, + {a: [2, 3]}); + modify( + {a: [{x: 1}, {x: 2}]}, + {$push: {a: {$each: [{x: 3}], $position: 0, $sort: {x: 1}, $slice: -3}}}, + {a: [{x: 1}, {x: 2}, {x: 3}]} + ); + modify( + {a: [{x: 1}, {x: 2}]}, + {$push: {a: {$each: [{x: 3}], $position: 0, $sort: {x: 1}, $slice: 0}}}, + {a: []} + ); + //restricted field names + exception({}, {$push: {$a: 1}}); + exception({}, {$push: {'\0a': 1}}); + exception({}, {$push: {a: {$a:1}}}); + exception({}, {$push: {a: {$each: [{$a:1}]}}}); + exception({}, {$push: {a: {$each: [{"a.b":1}]}}}); + exception({}, {$push: {a: {$each: [{'\0a':1}]}}}); + modify({}, {$push: {a: {$each: [{'':1}]}}}, {a: [ { '': 1 } ]}); + modify({}, {$push: {a: {$each: [{' ':1}]}}}, {a: [ { ' ': 1 } ]}); + exception({}, {$push: {a: {$each: [{'.':1}]}}}); + + // #issue 5167 + // $push $slice with positive numbers + modify({}, {$push: {a: {$each: [], $slice: 5}}}, {a:[]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [], $slice: 1}}}, {a:[1]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 1}}}, {a:[1]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 2}}}, {a:[1,2]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 4}}}, {a:[1,2,3,4]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 5}}}, {a:[1,2,3,4,5]}); + modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 10}}}, {a:[1,2,3,4,5]}); + + + // $pushAll + modify({}, {$pushAll: {a: [1]}}, {a: [1]}); + modify({a: []}, {$pushAll: {a: [1]}}, {a: [1]}); + modify({a: [1]}, {$pushAll: {a: [2]}}, {a: [1, 2]}); + modify({}, {$pushAll: {a: [1, 2]}}, {a: [1, 2]}); + modify({a: []}, {$pushAll: {a: [1, 2]}}, {a: [1, 2]}); + modify({a: [1]}, {$pushAll: {a: [2, 3]}}, {a: [1, 2, 3]}); + modify({}, {$pushAll: {a: []}}, {a: []}); + modify({a: []}, {$pushAll: {a: []}}, {a: []}); + modify({a: [1]}, {$pushAll: {a: []}}, {a: [1]}); + exception({a: true}, {$pushAll: {a: [1]}}); + exception({a: []}, {$pushAll: {a: 1}}); + modify({a: []}, {$pushAll: {'a.1': [99]}}, {a: [null, [99]]}); + modify({a: []}, {$pushAll: {'a.1': []}}, {a: [null, []]}); + modify({a: {}}, {$pushAll: {'a.x': [99]}}, {a: {x: [99]}}); + modify({a: {}}, {$pushAll: {'a.x': []}}, {a: {x: []}}); + exception({a: [1]}, {$pushAll: {a: [{$a:1}]}}); + exception({a: [1]}, {$pushAll: {a: [{'\0a':1}]}}); + exception({a: [1]}, {$pushAll: {a: [{"a.b":1}]}}); + + // $addToSet + modify({}, {$addToSet: {a: 1}}, {a: [1]}); + modify({a: []}, {$addToSet: {a: 1}}, {a: [1]}); + modify({a: [1]}, {$addToSet: {a: 2}}, {a: [1, 2]}); + modify({a: [1, 2]}, {$addToSet: {a: 1}}, {a: [1, 2]}); + modify({a: [1, 2]}, {$addToSet: {a: 2}}, {a: [1, 2]}); + modify({a: [1, 2]}, {$addToSet: {a: 3}}, {a: [1, 2, 3]}); + exception({a: true}, {$addToSet: {a: 1}}); + modify({a: [1]}, {$addToSet: {a: [2]}}, {a: [1, [2]]}); + modify({}, {$addToSet: {a: {x: 1}}}, {a: [{x: 1}]}); + modify({a: [{x: 1}]}, {$addToSet: {a: {x: 1}}}, {a: [{x: 1}]}); + modify({a: [{x: 1}]}, {$addToSet: {a: {x: 2}}}, {a: [{x: 1}, {x: 2}]}); + modify({a: [{x: 1, y: 2}]}, {$addToSet: {a: {x: 1, y: 2}}}, + {a: [{x: 1, y: 2}]}); + modify({a: [{x: 1, y: 2}]}, {$addToSet: {a: {y: 2, x: 1}}}, + {a: [{x: 1, y: 2}, {y: 2, x: 1}]}); + modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4]}}}, {a: [1, 2, 3, 4]}); + modify({}, {$addToSet: {a: {$each: []}}}, {a: []}); + modify({}, {$addToSet: {a: {$each: [1]}}}, {a: [1]}); + modify({a: []}, {$addToSet: {'a.1': 99}}, {a: [null, [99]]}); + modify({a: {}}, {$addToSet: {'a.x': 99}}, {a: {x: [99]}}); + + // invalid field names + exception({}, {$addToSet: {a: {$b:1}}}); + exception({}, {$addToSet: {a: {"a.b":1}}}); + exception({}, {$addToSet: {a: {"a.":1}}}); + exception({}, {$addToSet: {a: {'\u0000a':1}}}); + exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, {$a:1}]}}}); + exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, {'\0a':1}]}}}); + exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, [{$a:1}]]}}}); + exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, [{b:{c:[{a:1},{"d.s":2}]}}]]}}}); + exception({a: [1, 2]}, {$addToSet: {a:{b: [3, 1, [{b:{c:[{a:1},{"d.s":2}]}}]]}}}); + //$each is first element and thus an operator + modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4], b: 12}}},{a: [ 1, 2, 3, 4 ]}); + // this should fail because $each is now a field name (not first in object) and thus invalid field name with $ + exception({a: [1, 2]}, {$addToSet: {a: {b: 12, $each: [3, 1, 4]}}}); + + // $pop + modify({}, {$pop: {a: 1}}, {}); // tested + modify({}, {$pop: {a: -1}}, {}); // tested + modify({a: []}, {$pop: {a: 1}}, {a: []}); + modify({a: []}, {$pop: {a: -1}}, {a: []}); + modify({a: [1, 2, 3]}, {$pop: {a: 1}}, {a: [1, 2]}); + modify({a: [1, 2, 3]}, {$pop: {a: 10}}, {a: [1, 2]}); + modify({a: [1, 2, 3]}, {$pop: {a: .001}}, {a: [1, 2]}); + modify({a: [1, 2, 3]}, {$pop: {a: 0}}, {a: [1, 2]}); + modify({a: [1, 2, 3]}, {$pop: {a: "stuff"}}, {a: [1, 2]}); + modify({a: [1, 2, 3]}, {$pop: {a: -1}}, {a: [2, 3]}); + modify({a: [1, 2, 3]}, {$pop: {a: -10}}, {a: [2, 3]}); + modify({a: [1, 2, 3]}, {$pop: {a: -.001}}, {a: [2, 3]}); + exception({a: true}, {$pop: {a: 1}}); + exception({a: true}, {$pop: {a: -1}}); + modify({a: []}, {$pop: {'a.1': 1}}, {a: []}); // tested + modify({a: [1, [2, 3], 4]}, {$pop: {'a.1': 1}}, {a: [1, [2], 4]}); + modify({a: {}}, {$pop: {'a.x': 1}}, {a: {}}); // tested + modify({a: {x: [2, 3]}}, {$pop: {'a.x': 1}}, {a: {x: [2]}}); + + // $pull + modify({}, {$pull: {a: 1}}, {}); + modify({}, {$pull: {'a.x': 1}}, {}); + modify({a: {}}, {$pull: {'a.x': 1}}, {a: {}}); + exception({a: true}, {$pull: {a: 1}}); + modify({a: [2, 1, 2]}, {$pull: {a: 1}}, {a: [2, 2]}); + modify({a: [2, 1, 2]}, {$pull: {a: 2}}, {a: [1]}); + modify({a: [2, 1, 2]}, {$pull: {a: 3}}, {a: [2, 1, 2]}); + modify({a: [1, null, 2, null]}, {$pull: {a: null}}, {a: [1, 2]}); + modify({a: []}, {$pull: {a: 3}}, {a: []}); + modify({a: [[2], [2, 1], [3]]}, {$pull: {a: [2, 1]}}, + {a: [[2], [3]]}); // tested + modify({a: [{b: 1, c: 2}, {b: 2, c: 2}]}, {$pull: {a: {b: 1}}}, + {a: [{b: 2, c: 2}]}); + modify({a: [{b: 1, c: 2}, {b: 2, c: 2}]}, {$pull: {a: {c: 2}}}, + {a: []}); + // XXX implement this functionality! + // probably same refactoring as $elemMatch? + // modify({a: [1, 2, 3, 4]}, {$pull: {$gt: 2}}, {a: [1,2]}); fails! + + // $pullAll + modify({}, {$pullAll: {a: [1]}}, {}); + modify({a: [1, 2, 3]}, {$pullAll: {a: []}}, {a: [1, 2, 3]}); + modify({a: [1, 2, 3]}, {$pullAll: {a: [2]}}, {a: [1, 3]}); + modify({a: [1, 2, 3]}, {$pullAll: {a: [2, 1]}}, {a: [3]}); + modify({a: [1, 2, 3]}, {$pullAll: {a: [1, 2]}}, {a: [3]}); + modify({}, {$pullAll: {'a.b.c': [2]}}, {}); + exception({a: true}, {$pullAll: {a: [1]}}); + exception({a: [1, 2, 3]}, {$pullAll: {a: 1}}); + modify({x: [{a: 1}, {a: 1, b: 2}]}, {$pullAll: {x: [{a: 1}]}}, + {x: [{a: 1, b: 2}]}); + + // $rename + modify({}, {$rename: {a: 'b'}}, {}); + modify({a: [12]}, {$rename: {a: 'b'}}, {b: [12]}); + modify({a: {b: 12}}, {$rename: {a: 'c'}}, {c: {b: 12}}); + modify({a: {b: 12}}, {$rename: {'a.b': 'a.c'}}, {a: {c: 12}}); + modify({a: {b: 12}}, {$rename: {'a.b': 'x'}}, {a: {}, x: 12}); // tested + modify({a: {b: 12}}, {$rename: {'a.b': 'q.r'}}, {a: {}, q: {r: 12}}); + modify({a: {b: 12}}, {$rename: {'a.b': 'q.2.r'}}, {a: {}, q: {2: {r: 12}}}); + modify({a: {b: 12}, q: {}}, {$rename: {'a.b': 'q.2.r'}}, + {a: {}, q: {2: {r: 12}}}); + exception({a: {b: 12}, q: []}, {$rename: {'a.b': 'q.2'}}); // tested + exception({a: {b: 12}, q: []}, {$rename: {'a.b': 'q.2.r'}}); // tested + // These strange MongoDB behaviors throw. + // modify({a: {b: 12}, q: []}, {$rename: {'q.1': 'x'}}, + // {a: {b: 12}, x: []}); // tested + // modify({a: {b: 12}, q: []}, {$rename: {'q.1.j': 'x'}}, + // {a: {b: 12}, x: []}); // tested + exception({}, {$rename: {'a': 'a'}}); + exception({}, {$rename: {'a.b': 'a.b'}}); + modify({a: 12, b: 13}, {$rename: {a: 'b'}}, {b: 12}); + exception({a: [12]}, {$rename: {a: '$b'}}); + exception({a: [12]}, {$rename: {a: '\0a'}}); + + // $setOnInsert + modify({a: 0}, {$setOnInsert: {a: 12}}, {a: 0}); + upsert({a: 12}, {$setOnInsert: {b: 12}}, {a: 12, b: 12}); + upsert({a: 12}, {$setOnInsert: {_id: 'test'}}, {_id: 'test', a: 12}); + upsert({"a.b": 10}, {$setOnInsert: {a: {b: 10, c: 12}}}, {a: {b: 10, c: 12}}); + upsert({"a.b": 10}, {$setOnInsert: {c: 12}}, {a: {b: 10}, c: 12}); + upsert({"_id": 'test'}, {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); + upsert('test', {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); + upsertException({a: 0}, {$setOnInsert: {$a: 12}}); + upsertException({a: 0}, {$setOnInsert: {'\0a': 12}}); + upsert({a: 0}, {$setOnInsert: {b: {a:1}}}, {a:0, b:{a:1}}); + upsertException({a: 0}, {$setOnInsert: {b: {$a:1}}}); + upsertException({a: 0}, {$setOnInsert: {b: {'a.b':1}}}); + upsertException({a: 0}, {$setOnInsert: {b: {'\0a':1}}}); + + // Test for https://github.com/meteor/meteor/issues/8775. + upsert( + { a: { $exists: true }}, + { $setOnInsert: { a: 123 }}, + { a: 123 } + ); + + // Tests for https://github.com/meteor/meteor/issues/8794. + const testObjectId = new MongoID.ObjectID(); + upsert( + { _id: testObjectId }, + { $setOnInsert: { a: 123 } }, + { _id: testObjectId, a: 123 }, + ); + upsert( + { someOtherId: testObjectId }, + { $setOnInsert: { a: 123 } }, + { someOtherId: testObjectId, a: 123 }, + ); + upsert( + { a: { $eq: testObjectId } }, + { $setOnInsert: { a: 123 } }, + { a: 123 }, + ); + const testDate = new Date('2017-01-01'); + upsert( + { someDate: testDate }, + { $setOnInsert: { a: 123 } }, + { someDate: testDate, a: 123 }, + ); + upsert( + { + a: Object.create(null, { + $exists: { + writable: true, + configurable: true, + value: true + } + }), + }, + { $setOnInsert: { a: 123 } }, + { a: 123 }, + ); + upsert( + { foo: { $exists: true, $type: 2 }}, + { $setOnInsert: { bar: 'baz' } }, + { bar: 'baz' } + ); + upsert( + { foo: {} }, + { $setOnInsert: { bar: 'baz' } }, + { foo: {}, bar: 'baz' } + ); + + exception({}, {$set: {_id: 'bad'}}); + + // $bit + // unimplemented + + // XXX test case sensitivity of modops + // XXX for each (most) modop, test that it performs a deep copy +}); + +// XXX test update() (selecting docs, multi, upsert..) + +Tinytest.add("minimongo - observe ordered", function (test) { + var operations = []; + var cbs = log_callbacks(operations); + var handle; + + var c = new LocalCollection(); + handle = c.find({}, {sort: {a: 1}}).observe(cbs); + test.isTrue(handle.collection === c); + + c.insert({_id: 'foo', a:1}); + test.equal(operations.shift(), ['added', {a:1}, 0, null]); + c.update({a:1}, {$set: {a: 2}}); + test.equal(operations.shift(), ['changed', {a:2}, 0, {a:1}]); + c.insert({a:10}); + test.equal(operations.shift(), ['added', {a:10}, 1, null]); + c.update({}, {$inc: {a: 1}}, {multi: true}); + test.equal(operations.shift(), ['changed', {a:3}, 0, {a:2}]); + test.equal(operations.shift(), ['changed', {a:11}, 1, {a:10}]); + c.update({a:11}, {a:1}); + test.equal(operations.shift(), ['changed', {a:1}, 1, {a:11}]); + test.equal(operations.shift(), ['moved', {a:1}, 1, 0, 'foo']); + c.remove({a:2}); + test.equal(operations.shift(), undefined); + c.remove({a:3}); + test.equal(operations.shift(), ['removed', 'foo', 1, {a:3}]); + + // test stop + handle.stop(); + var idA2 = Random.id(); + c.insert({_id: idA2, a:2}); + test.equal(operations.shift(), undefined); + + // test initial inserts (and backwards sort) + handle = c.find({}, {sort: {a: -1}}).observe(cbs); + test.equal(operations.shift(), ['added', {a:2}, 0, null]); + test.equal(operations.shift(), ['added', {a:1}, 1, null]); + handle.stop(); + + // test _suppress_initial + handle = c.find({}, {sort: {a: -1}}).observe(Object.assign({ + _suppress_initial: true}, cbs)); + test.equal(operations.shift(), undefined); + c.insert({a:100}); + test.equal(operations.shift(), ['added', {a:100}, 0, idA2]); + handle.stop(); + + // test skip and limit. + c.remove({}); + handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2}).observe(cbs); + test.equal(operations.shift(), undefined); + c.insert({a:1}); + test.equal(operations.shift(), undefined); + c.insert({_id: 'foo', a:2}); + test.equal(operations.shift(), ['added', {a:2}, 0, null]); + c.insert({a:3}); + test.equal(operations.shift(), ['added', {a:3}, 1, null]); + c.insert({a:4}); + test.equal(operations.shift(), undefined); + c.update({a:1}, {a:0}); + test.equal(operations.shift(), undefined); + c.update({a:0}, {a:5}); + test.equal(operations.shift(), ['removed', 'foo', 0, {a:2}]); + test.equal(operations.shift(), ['added', {a:4}, 1, null]); + c.update({a:3}, {a:3.5}); + test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]); + handle.stop(); + + // test observe limit with pre-existing docs + c.remove({}); + c.insert({a: 1}); + c.insert({_id: 'two', a: 2}); + c.insert({a: 3}); + handle = c.find({}, {sort: {a: 1}, limit: 2}).observe(cbs); + test.equal(operations.shift(), ['added', {a:1}, 0, null]); + test.equal(operations.shift(), ['added', {a:2}, 1, null]); + test.equal(operations.shift(), undefined); + c.remove({a: 2}); + test.equal(operations.shift(), ['removed', 'two', 1, {a:2}]); + test.equal(operations.shift(), ['added', {a:3}, 1, null]); + test.equal(operations.shift(), undefined); + handle.stop(); + + // test _no_indices + + c.remove({}); + handle = c.find({}, {sort: {a: 1}}).observe(Object.assign(cbs, {_no_indices: true})); + c.insert({_id: 'foo', a:1}); + test.equal(operations.shift(), ['added', {a:1}, -1, null]); + c.update({a:1}, {$set: {a: 2}}); + test.equal(operations.shift(), ['changed', {a:2}, -1, {a:1}]); + c.insert({a:10}); + test.equal(operations.shift(), ['added', {a:10}, -1, null]); + c.update({}, {$inc: {a: 1}}, {multi: true}); + test.equal(operations.shift(), ['changed', {a:3}, -1, {a:2}]); + test.equal(operations.shift(), ['changed', {a:11}, -1, {a:10}]); + c.update({a:11}, {a:1}); + test.equal(operations.shift(), ['changed', {a:1}, -1, {a:11}]); + test.equal(operations.shift(), ['moved', {a:1}, -1, -1, 'foo']); + c.remove({a:2}); + test.equal(operations.shift(), undefined); + c.remove({a:3}); + test.equal(operations.shift(), ['removed', 'foo', -1, {a:3}]); + handle.stop(); +}); + +[true, false].forEach(function (ordered) { + Tinytest.add("minimongo - observe ordered: " + ordered, function (test) { + var c = new LocalCollection(); + + var ev = ""; + var makecb = function (tag) { + var ret = {}; + ["added", "changed", "removed"].forEach(function (fn) { + var fnName = ordered ? fn + "At" : fn; + ret[fnName] = function (doc) { + ev = (ev + fn.substr(0, 1) + tag + doc._id + "_"); + }; + }); + return ret; + }; + var expect = function (x) { + test.equal(ev, x); + ev = ""; + }; + + c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); + c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); + c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); + + // This should work equally well for ordered and unordered observations + // (because the callbacks don't look at indices and there's no 'moved' + // callback). + var handle = c.find({tags: "flower"}).observe(makecb('a')); + expect("aa3_"); + c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}}); + expect("ra3_"); + c.update({name: "rose"}, {$set: {tags: ["flower", "red", "squishy"]}}); + expect("aa3_"); + c.update({name: "rose"}, {$set: {food: false}}); + expect("ca3_"); + c.remove({}); + expect("ra3_"); + c.insert({_id: 4, name: "daisy", tags: ["flower"]}); + expect("aa4_"); + handle.stop(); + // After calling stop, no more callbacks are called. + c.insert({_id: 5, name: "iris", tags: ["flower"]}); + expect(""); + + // Test that observing a lookup by ID works. + handle = c.find(4).observe(makecb('b')); + expect('ab4_'); + c.update(4, {$set: {eek: 5}}); + expect('cb4_'); + handle.stop(); + + // Test observe with reactive: false. + handle = c.find({tags: "flower"}, {reactive: false}).observe(makecb('c')); + expect('ac4_ac5_'); + // This insert shouldn't trigger a callback because it's not reactive. + c.insert({_id: 6, name: "river", tags: ["flower"]}); + expect(''); + handle.stop(); + }); +}); + + +Tinytest.add("minimongo - saveOriginals", function (test) { + // set up some data + var c = new LocalCollection(), + count; + c.insert({_id: 'foo', x: 'untouched'}); + c.insert({_id: 'bar', x: 'updateme'}); + c.insert({_id: 'baz', x: 'updateme'}); + c.insert({_id: 'quux', y: 'removeme'}); + c.insert({_id: 'whoa', y: 'removeme'}); + + // Save originals and make some changes. + c.saveOriginals(); + c.insert({_id: "hooray", z: 'insertme'}); + c.remove({y: 'removeme'}); + count = c.update({x: 'updateme'}, {$set: {z: 5}}, {multi: true}); + c.update('bar', {$set: {k: 7}}); // update same doc twice + + // Verify returned count is correct + test.equal(count, 2); + + // Verify the originals. + var originals = c.retrieveOriginals(); + var affected = ['bar', 'baz', 'quux', 'whoa', 'hooray']; + test.equal(originals.size(), affected.length); + affected.forEach(function (id) { + test.isTrue(originals.has(id)); + }); + test.equal(originals.get('bar'), {_id: 'bar', x: 'updateme'}); + test.equal(originals.get('baz'), {_id: 'baz', x: 'updateme'}); + test.equal(originals.get('quux'), {_id: 'quux', y: 'removeme'}); + test.equal(originals.get('whoa'), {_id: 'whoa', y: 'removeme'}); + test.equal(originals.get('hooray'), undefined); + + // Verify that changes actually occured. + test.equal(c.find().count(), 4); + test.equal(c.findOne('foo'), {_id: 'foo', x: 'untouched'}); + test.equal(c.findOne('bar'), {_id: 'bar', x: 'updateme', z: 5, k: 7}); + test.equal(c.findOne('baz'), {_id: 'baz', x: 'updateme', z: 5}); + test.equal(c.findOne('hooray'), {_id: 'hooray', z: 'insertme'}); + + // The next call doesn't get the same originals again. + c.saveOriginals(); + originals = c.retrieveOriginals(); + test.isTrue(originals); + test.isTrue(originals.empty()); + + // Insert and remove a document during the period. + c.saveOriginals(); + c.insert({_id: 'temp', q: 8}); + c.remove('temp'); + originals = c.retrieveOriginals(); + test.equal(originals.size(), 1); + test.isTrue(originals.has('temp')); + test.equal(originals.get('temp'), undefined); +}); + +Tinytest.add("minimongo - saveOriginals errors", function (test) { + var c = new LocalCollection(); + // Can't call retrieve before save. + test.throws(function () { c.retrieveOriginals(); }); + c.saveOriginals(); + // Can't call save twice. + test.throws(function () { c.saveOriginals(); }); +}); + +Tinytest.add("minimongo - objectid transformation", function (test) { + var testId = function (item) { + test.equal(item, MongoID.idParse(MongoID.idStringify(item))); + }; + var randomOid = new MongoID.ObjectID(); + testId(randomOid); + testId("FOO"); + testId("ffffffffffff"); + testId("0987654321abcdef09876543"); + testId(new MongoID.ObjectID()); + testId("--a string"); + + test.equal("ffffffffffff", MongoID.idParse(MongoID.idStringify("ffffffffffff"))); +}); + + +Tinytest.add("minimongo - objectid", function (test) { + var randomOid = new MongoID.ObjectID(); + var anotherRandomOid = new MongoID.ObjectID(); + test.notEqual(randomOid, anotherRandomOid); + test.throws(function() { new MongoID.ObjectID("qqqqqqqqqqqqqqqqqqqqqqqq");}); + test.throws(function() { new MongoID.ObjectID("ABCDEF"); }); + test.equal(randomOid, new MongoID.ObjectID(randomOid.valueOf())); +}); + +Tinytest.add("minimongo - pause", function (test) { + var operations = []; + var cbs = log_callbacks(operations); + + var c = new LocalCollection(); + var h = c.find({}).observe(cbs); + + // remove and add cancel out. + c.insert({_id: 1, a: 1}); + test.equal(operations.shift(), ['added', {a:1}, 0, null]); + + c.pauseObservers(); + + c.remove({_id: 1}); + test.length(operations, 0); + c.insert({_id: 1, a: 1}); + test.length(operations, 0); + + c.resumeObservers(); + test.length(operations, 0); + + + // two modifications become one + c.pauseObservers(); + + c.update({_id: 1}, {a: 2}); + c.update({_id: 1}, {a: 3}); + + c.resumeObservers(); + test.equal(operations.shift(), ['changed', {a:3}, 0, {a:1}]); + test.length(operations, 0); + + // test special case for remove({}) + c.pauseObservers(); + test.equal(c.remove({}), 1); + test.length(operations, 0); + c.resumeObservers(); + test.equal(operations.shift(), ['removed', 1, 0, {a:3}]); + test.length(operations, 0); + + h.stop(); +}); + +Tinytest.add("minimongo - ids matched by selector", function (test) { + var check = function (selector, ids) { + var idsFromSelector = LocalCollection._idsMatchedBySelector(selector); + // XXX normalize order, in a way that also works for ObjectIDs? + test.equal(idsFromSelector, ids); + }; + check("foo", ["foo"]); + check({_id: "foo"}, ["foo"]); + var oid1 = new MongoID.ObjectID(); + check(oid1, [oid1]); + check({_id: oid1}, [oid1]); + check({_id: "foo", x: 42}, ["foo"]); + check({}, null); + check({_id: {$in: ["foo", oid1]}}, ["foo", oid1]); + check({_id: {$ne: "foo"}}, null); + // not actually valid, but works for now... + check({$and: ["foo"]}, ["foo"]); + check({$and: [{x: 42}, {_id: oid1}]}, [oid1]); + check({$and: [{x: 42}, {_id: {$in: [oid1]}}]}, [oid1]); +}); + +Tinytest.add("minimongo - reactive stop", function (test) { + var coll = new LocalCollection(); + coll.insert({_id: 'A'}); + coll.insert({_id: 'B'}); + coll.insert({_id: 'C'}); + + var addBefore = function (str, newChar, before) { + var idx = str.indexOf(before); + if (idx === -1) + return str + newChar; + return str.slice(0, idx) + newChar + str.slice(idx); + }; + + var x, y; + var sortOrder = ReactiveVar(1); + + var c = Tracker.autorun(function () { + var q = coll.find({}, {sort: {_id: sortOrder.get()}}); + x = ""; + q.observe({ addedAt: function (doc, atIndex, before) { + x = addBefore(x, doc._id, before); + }}); + y = ""; + q.observeChanges({ addedBefore: function (id, fields, before) { + y = addBefore(y, id, before); + }}); + }); + + test.equal(x, "ABC"); + test.equal(y, "ABC"); + + sortOrder.set(-1); + test.equal(x, "ABC"); + test.equal(y, "ABC"); + Tracker.flush(); + test.equal(x, "CBA"); + test.equal(y, "CBA"); + + coll.insert({_id: 'D'}); + coll.insert({_id: 'E'}); + test.equal(x, "EDCBA"); + test.equal(y, "EDCBA"); + + c.stop(); + // stopping kills the observes immediately + coll.insert({_id: 'F'}); + test.equal(x, "EDCBA"); + test.equal(y, "EDCBA"); +}); + +Tinytest.add("minimongo - immediate invalidate", function (test) { + var coll = new LocalCollection(); + coll.insert({_id: 'A'}); + + // This has two separate findOnes. findOne() uses skip/limit, which means + // that its response to an update() call involves a recompute. We used to have + // a bug where we would first calculate all the calls that need to be + // recomputed, then recompute them one by one, without checking to see if the + // callbacks from recomputing one query stopped the second query, which + // crashed. + var c = Tracker.autorun(function () { + coll.findOne('A'); + coll.findOne('A'); + }); + + coll.update('A', {$set: {x: 42}}); + + c.stop(); +}); + + +Tinytest.add("minimongo - count on cursor with limit", function(test){ + var coll = new LocalCollection(), count; + + coll.insert({_id: 'A'}); + coll.insert({_id: 'B'}); + coll.insert({_id: 'C'}); + coll.insert({_id: 'D'}); + + var c = Tracker.autorun(function (c) { + var cursor = coll.find({_id: {$exists: true}}, {sort: {_id: 1}, limit: 3}); + count = cursor.count(); + }); + + test.equal(count, 3); + + coll.remove('A'); // still 3 in the collection + Tracker.flush(); + test.equal(count, 3); + + coll.remove('B'); // expect count now 2 + Tracker.flush(); + test.equal(count, 2); + + + coll.insert({_id: 'A'}); // now 3 again + Tracker.flush(); + test.equal(count, 3); + + coll.insert({_id: 'B'}); // now 4 entries, but count should be 3 still + Tracker.flush(); + test.equal(count, 3); + + c.stop(); +}); + +Tinytest.add("minimongo - reactive count with cached cursor", function (test) { + var coll = new LocalCollection; + var cursor = coll.find({}); + var firstAutorunCount, secondAutorunCount; + Tracker.autorun(function(){ + firstAutorunCount = cursor.count(); + }); + Tracker.autorun(function(){ + secondAutorunCount = coll.find({}).count(); + }); + test.equal(firstAutorunCount, 0); + test.equal(secondAutorunCount, 0); + coll.insert({i: 1}); + coll.insert({i: 2}); + coll.insert({i: 3}); + Tracker.flush(); + test.equal(firstAutorunCount, 3); + test.equal(secondAutorunCount, 3); +}); + +Tinytest.add("minimongo - $near operator tests", function (test) { + var coll = new LocalCollection(); + coll.insert({ rest: { loc: [2, 3] } }); + coll.insert({ rest: { loc: [-3, 3] } }); + coll.insert({ rest: { loc: [5, 5] } }); + + test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 30 } }).count(), 3); + test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 1); + var points = coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 6 } }).fetch(); + points.forEach(function (point, i, points) { + test.isTrue(!i || distance([0, 0], point.rest.loc) >= distance([0, 0], points[i - 1].rest.loc)); + }); + + function distance(a, b) { + var x = a[0] - b[0]; + var y = a[1] - b[1]; + return Math.sqrt(x * x + y * y); + } + + // GeoJSON tests + coll = new LocalCollection(); + var data = [{ "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] } }, + { "category" : "WEAPON LAWS", "descript" : "POSS OF PROHIBITED WEAPON", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] } }, + { "category" : "LARCENY/THEFT", "descript" : "GRAND THEFT OF PROPERTY", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.41538270191, 37.774683628213 ] } }, + { "category" : "LARCENY/THEFT", "descript" : "PETTY THEFT FROM LOCKED AUTO", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415396041221, 37.7747879744156 ] } }, + { "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879734156 ] } } + ]; + + data.forEach(function (x, i) { coll.insert(Object.assign(x, { x: i })); }); + + var close15 = coll.find({ location: { $near: { + $geometry: { type: "Point", + coordinates: [-122.4154282, 37.7746115] }, + $maxDistance: 15 } } }).fetch(); + test.length(close15, 1); + test.equal(close15[0].descript, "GRAND THEFT OF PROPERTY"); + + var close20 = coll.find({ location: { $near: { + $geometry: { type: "Point", + coordinates: [-122.4154282, 37.7746115] }, + $maxDistance: 20 } } }).fetch(); + test.length(close20, 4); + test.equal(close20[0].descript, "GRAND THEFT OF PROPERTY"); + test.equal(close20[1].descript, "PETTY THEFT FROM LOCKED AUTO"); + test.equal(close20[2].descript, "POSSESSION OF BURGLARY TOOLS"); + test.equal(close20[3].descript, "POSS OF PROHIBITED WEAPON"); + + // Any combinations of $near with $or/$and/$nor/$not should throw an error + test.throws(function () { + coll.find({ location: { + $not: { + $near: { + $geometry: { + type: "Point", + coordinates: [-122.4154282, 37.7746115] + }, $maxDistance: 20 } } } }); + }); + test.throws(function () { + coll.find({ + $and: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, + { x: 0 }] + }); + }); + test.throws(function () { + coll.find({ + $or: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, + { x: 0 }] + }); + }); + test.throws(function () { + coll.find({ + $nor: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 1 }}}, + { x: 0 }] + }); + }); + test.throws(function () { + coll.find({ + $and: [{ + $and: [{ + location: { + $near: { + $geometry: { + type: "Point", + coordinates: [-122.4154282, 37.7746115] + }, + $maxDistance: 1 + } + } + }] + }] + }); + }); + + // array tests + coll = new LocalCollection(); + coll.insert({ + _id: "x", + k: 9, + a: [ + {b: [ + [100, 100], + [1, 1]]}, + {b: [150, 150]}]}); + coll.insert({ + _id: "y", + k: 9, + a: {b: [5, 5]}}); + var testNear = function (near, md, expected) { + test.equal( + coll.find({'a.b': {$near: near, $maxDistance: md}}).fetch().map(function (doc) { return doc._id }), + expected); + }; + testNear([149, 149], 4, ['x']); + testNear([149, 149], 1000, ['x', 'y']); + // It's important that we figure out that 'x' is closer than 'y' to [2,2] even + // though the first within-1000 point in 'x' (ie, [100,100]) is farther than + // 'y'. + testNear([2, 2], 1000, ['x', 'y']); + + // issue #3599 + // Ensure that distance is not used as a tie-breaker for sort. + test.equal( + coll.find({'a.b': {$near: [1, 1]}}, {sort: {k: 1}}).fetch().map(function (doc) { return doc._id; }), + ['x', 'y']); + test.equal( + coll.find({'a.b': {$near: [5, 5]}}, {sort: {k: 1}}).fetch().map(function (doc) { return doc._id; }), + ['x', 'y']); + + var operations = []; + var cbs = log_callbacks(operations); + var handle = coll.find({'a.b': {$near: [7,7]}}).observe(cbs); + + test.length(operations, 2); + test.equal(operations.shift(), ['added', {k:9, a:{b:[5,5]}}, 0, null]); + test.equal(operations.shift(), + ['added', {k: 9, a:[{b:[[100,100],[1,1]]},{b:[150,150]}]}, + 1, null]); + // This needs to be inserted in the MIDDLE of the two existing ones. + coll.insert({a: {b: [3,3]}}); + test.length(operations, 1); + test.equal(operations.shift(), ['added', {a: {b: [3, 3]}}, 1, 'x']); + + handle.stop(); +}); + +// issue #2077 +Tinytest.add("minimongo - $near and $geometry for legacy coordinates", function(test){ + var coll = new LocalCollection(); + + coll.insert({ + loc: { + x: 1, + y: 1 + } + }); + coll.insert({ + loc: [-1,-1] + }); + coll.insert({ + loc: [40,-10] + }); + coll.insert({ + loc: { + x: -10, + y: 40 + } + }); + + test.equal(coll.find({ 'loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 2); + test.equal(coll.find({ 'loc': { $near: {$geometry: {type: "Point", coordinates: [0, 0]}}} }).count(), 4); + test.equal(coll.find({ 'loc': { $near: {$geometry: {type: "Point", coordinates: [0, 0]}, $maxDistance:200000}}}).count(), 2); + +}); + +// Regression test for #4377. Previously, "replace" updates didn't clone the +// argument. +Tinytest.add("minimongo - update should clone", function (test) { + var x = []; + var coll = new LocalCollection; + var id = coll.insert({}); + coll.update(id, {x: x}); + x.push(1); + test.equal(coll.findOne(id), {_id: id, x: []}); +}); + +// See #2275. +Tinytest.add("minimongo - fetch in observe", function (test) { + var coll = new LocalCollection; + var callbackInvoked = false; + var observe = coll.find().observeChanges({ + added: function (id, fields) { + callbackInvoked = true; + test.equal(fields, {foo: 1}); + var doc = coll.findOne({foo: 1}); + test.isTrue(doc); + test.equal(doc.foo, 1); + } + }); + test.isFalse(callbackInvoked); + var computation = Tracker.autorun(function (computation) { + if (computation.firstRun) { + coll.insert({foo: 1}); + } + }); + test.isTrue(callbackInvoked); + observe.stop(); + computation.stop(); +}); + +// See #2254 +Tinytest.add("minimongo - fine-grained reactivity of observe with fields projection", function (test) { + var X = new LocalCollection; + var id = "asdf"; + X.insert({_id: id, foo: {bar: 123}}); + + var callbackInvoked = false; + var obs = X.find(id, {fields: {'foo.bar': 1}}).observeChanges({ + changed: function (id, fields) { + callbackInvoked = true; + } + }); + + test.isFalse(callbackInvoked); + X.update(id, {$set: {'foo.baz': 456}}); + test.isFalse(callbackInvoked); + + obs.stop(); +}); +Tinytest.add("minimongo - fine-grained reactivity of query with fields projection", function (test) { + var X = new LocalCollection; + var id = "asdf"; + X.insert({_id: id, foo: {bar: 123}}); + + var callbackInvoked = false; + var computation = Tracker.autorun(function () { + callbackInvoked = true; + return X.findOne(id, { fields: { 'foo.bar': 1 } }); + }); + test.isTrue(callbackInvoked); + callbackInvoked = false; + X.update(id, {$set: {'foo.baz': 456}}); + test.isFalse(callbackInvoked); + X.update(id, {$set: {'foo.bar': 124}}); + Tracker.flush(); + test.isTrue(callbackInvoked); + + computation.stop(); +}); + +// Tests that the logic in `LocalCollection.prototype.update` +// correctly deals with count() on a cursor with skip or limit (since +// then the result set is an IdMap, not an array) +Tinytest.add("minimongo - reactive skip/limit count while updating", function(test) { + var X = new LocalCollection; + var count = -1; + + var c = Tracker.autorun(function() { + count = X.find({}, {skip: 1, limit: 1}).count(); + }); + + test.equal(count, 0); + + X.insert({}); + Tracker.flush({_throwFirstError: true}); + test.equal(count, 0); + + X.insert({}); + Tracker.flush({_throwFirstError: true}); + test.equal(count, 1); + + X.update({}, {$set: {foo: 1}}); + Tracker.flush({_throwFirstError: true}); + test.equal(count, 1); + + // Make sure a second update also works + X.update({}, {$set: {foo: 2}}); + Tracker.flush({_throwFirstError: true}); + test.equal(count, 1); + + c.stop(); +}); + +// Makes sure inserts cannot be performed using field names that have +// Mongo restricted characters in them ('.', '$', '\0'): +// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names +Tinytest.add("minimongo - cannot insert using invalid field names", function (test) { + const collection = new LocalCollection(); + + // Quick test to make sure non-dot field inserts are working + collection.insert({ a: 'b' }); + + // Quick test to make sure field values with dots are allowed + collection.insert({ a: 'b.c' }); + + // Verify top level dot-field inserts are prohibited + ['a.b', '.b', 'a.', 'a.b.c'].forEach((field) => { + test.throws(function () { + collection.insert({ [field]: 'c' }); + }, `Key ${field} must not contain '.'`); + }); + + // Verify nested dot-field inserts are prohibited + test.throws(function () { + collection.insert({ a: { b: { 'c.d': 'e' } } }); + }, "Key c.d must not contain '.'"); + + // Verify field names starting with $ are prohibited + test.throws(function () { + collection.insert({ '$a': 'b' }); + }, "Key $a must not start with '$'"); + + // Verify nested field names starting with $ are prohibited + test.throws(function () { + collection.insert({ a: { b: { '$c': 'd' } } }); + }, "Key $c must not start with '$'"); + + // Verify top level fields with null characters are prohibited + ['\0a', 'a\0', 'a\0b', '\u0000a', 'a\u0000', 'a\u0000b'].forEach((field) => { + test.throws(function () { + collection.insert({ [field]: 'c' }); + }, `Key ${field} must not contain null bytes`); + }); + + // Verify nested field names with null characters are prohibited + test.throws(function () { + collection.insert({ a: { b: { '\0c': 'd' } } }); + }, 'Key \0c must not contain null bytes'); +}); + +// Makes sure $set's cannot be performed using null bytes +// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names +Tinytest.add("minimongo - cannot $set with null bytes", function (test) { + const collection = new LocalCollection(); + + // Quick test to make sure non-null byte $set's are working + const id = collection.insert({ a: 'b', 'c': 'd' }); + collection.update({ _id: id }, { $set: { e: 'f' } }); + + // Verify $set's with null bytes throw an exception + test.throws(() => { + collection.update({ _id: id }, { $set: { '\0a': 'b' } }); + }, 'Key \0a must not contain null bytes'); +}); + +// Makes sure $rename's cannot be performed using null bytes +// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names +Tinytest.add("minimongo - cannot $rename with null bytes", function (test) { + const collection = new LocalCollection(); + + // Quick test to make sure non-null byte $rename's are working + let id = collection.insert({ a: 'b', c: 'd' }); + collection.update({ _id: id }, { $rename: { a: 'a1', c: 'c1' } }); + + // Verify $rename's with null bytes throw an exception + collection.remove({}); + id = collection.insert({ a: 'b', c: 'd' }); + test.throws(() => { + collection.update({ _id: id }, { $rename: { a: '\0a', c: 'c\0' } }); + }, "The 'to' field for $rename cannot contain an embedded null byte"); +}); diff --git a/packages/minimongo/minimongo_server_tests.js b/packages/minimongo/minimongo_tests_server.js similarity index 100% rename from packages/minimongo/minimongo_server_tests.js rename to packages/minimongo/minimongo_tests_server.js diff --git a/packages/minimongo/modify.js b/packages/minimongo/modify.js deleted file mode 100644 index 5c5e025f34..0000000000 --- a/packages/minimongo/modify.js +++ /dev/null @@ -1,502 +0,0 @@ -import { assertHasValidFieldNames, assertIsValidFieldName } from './validation.js'; - -// XXX need a strategy for passing the binding of $ into this -// function, from the compiled selector -// -// maybe just {key.up.to.just.before.dollarsign: array_index} -// -// XXX atomicity: if one modification fails, do we roll back the whole -// change? -// -// options: -// - isInsert is set when _modify is being called to compute the document to -// insert as part of an upsert operation. We use this primarily to figure -// out when to set the fields in $setOnInsert, if present. -LocalCollection._modify = function (doc, mod, options) { - options = options || {}; - if (!isPlainObject(mod)) - throw MinimongoError("Modifier must be an object"); - - // Make sure the caller can't mutate our data structures. - mod = EJSON.clone(mod); - - var isModifier = isOperatorObject(mod); - - var newDoc; - - if (!isModifier) { - if (mod._id && !EJSON.equals(doc._id, mod._id)) - throw MinimongoError("Cannot change the _id of a document"); - - // replace the whole document - assertHasValidFieldNames(mod); - newDoc = mod; - } else { - // apply modifiers to the doc. - newDoc = EJSON.clone(doc); - - Object.keys(mod).forEach(function (op) { - var operand = mod[op]; - var modFunc = MODIFIERS[op]; - // Treat $setOnInsert as $set if this is an insert. - if (options.isInsert && op === '$setOnInsert') - modFunc = MODIFIERS['$set']; - if (!modFunc) - throw MinimongoError("Invalid modifier specified " + op); - Object.keys(operand).forEach(function (keypath) { - var arg = operand[keypath]; - if (keypath === '') { - throw MinimongoError("An empty update path is not valid."); - } - - if (keypath === '_id' && op !== '$setOnInsert') { - throw MinimongoError("Mod on _id not allowed"); - } - - var keyparts = keypath.split('.'); - - if (!keyparts.every(Boolean)) { - throw MinimongoError( - "The update path '" + keypath + - "' contains an empty field name, which is not allowed."); - } - - var noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); - var forbidArray = (op === "$rename"); - var target = findModTarget(newDoc, keyparts, { - noCreate: NO_CREATE_MODIFIERS[op], - forbidArray: (op === "$rename"), - arrayIndices: options.arrayIndices - }); - var field = keyparts.pop(); - modFunc(target, field, arg, keypath, newDoc); - }); - }); - } - - // move new document into place. - Object.keys(doc).forEach(function (k) { - // Note: this used to be for (var k in doc) however, this does not - // work right in Opera. Deleting from a doc while iterating over it - // would sometimes cause opera to skip some keys. - if (k !== '_id') - delete doc[k]; - }); - Object.keys(newDoc).forEach(function (k) { - doc[k] = newDoc[k]; - }); -}; - -// for a.b.c.2.d.e, keyparts should be ['a', 'b', 'c', '2', 'd', 'e'], -// and then you would operate on the 'e' property of the returned -// object. -// -// if options.noCreate is falsey, creates intermediate levels of -// structure as necessary, like mkdir -p (and raises an exception if -// that would mean giving a non-numeric property to an array.) if -// options.noCreate is true, return undefined instead. -// -// may modify the last element of keyparts to signal to the caller that it needs -// to use a different value to index into the returned object (for example, -// ['a', '01'] -> ['a', 1]). -// -// if forbidArray is true, return null if the keypath goes through an array. -// -// if options.arrayIndices is set, use its first element for the (first) '$' in -// the path. -var findModTarget = function (doc, keyparts, options) { - options = options || {}; - var usedArrayIndex = false; - for (var i = 0; i < keyparts.length; i++) { - var last = (i === keyparts.length - 1); - var keypart = keyparts[i]; - var indexable = isIndexable(doc); - if (!indexable) { - if (options.noCreate) - return undefined; - var e = MinimongoError( - "cannot use the part '" + keypart + "' to traverse " + doc); - e.setPropertyError = true; - throw e; - } - if (doc instanceof Array) { - if (options.forbidArray) - return null; - if (keypart === '$') { - if (usedArrayIndex) - throw MinimongoError("Too many positional (i.e. '$') elements"); - if (!options.arrayIndices || !options.arrayIndices.length) { - throw MinimongoError("The positional operator did not find the " + - "match needed from the query"); - } - keypart = options.arrayIndices[0]; - usedArrayIndex = true; - } else if (isNumericKey(keypart)) { - keypart = parseInt(keypart); - } else { - if (options.noCreate) - return undefined; - throw MinimongoError( - "can't append to array using string field name [" - + keypart + "]"); - } - if (last) - // handle 'a.01' - keyparts[i] = keypart; - if (options.noCreate && keypart >= doc.length) - return undefined; - while (doc.length < keypart) - doc.push(null); - if (!last) { - if (doc.length === keypart) - doc.push({}); - else if (typeof doc[keypart] !== "object") - throw MinimongoError("can't modify field '" + keyparts[i + 1] + - "' of list value " + JSON.stringify(doc[keypart])); - } - } else { - assertIsValidFieldName(keypart); - if (!(keypart in doc)) { - if (options.noCreate) - return undefined; - if (!last) - doc[keypart] = {}; - } - } - - if (last) - return doc; - doc = doc[keypart]; - } - - // notreached -}; - -var NO_CREATE_MODIFIERS = { - $unset: true, - $pop: true, - $rename: true, - $pull: true, - $pullAll: true -}; - -var MODIFIERS = { - $currentDate: function (target, field, arg) { - if (typeof arg === "object" && arg.hasOwnProperty("$type")) { - if (arg.$type !== "date") { - throw MinimongoError( - "Minimongo does currently only support the date type " + - "in $currentDate modifiers", - { field }); - } - } else if (arg !== true) { - throw MinimongoError("Invalid $currentDate modifier", { field }); - } - target[field] = new Date(); - }, - $min: function (target, field, arg) { - if (typeof arg !== "number") { - throw MinimongoError("Modifier $min allowed for numbers only", { field }); - } - if (field in target) { - if (typeof target[field] !== "number") { - throw MinimongoError( - "Cannot apply $min modifier to non-number", { field }); - } - if (target[field] > arg) { - target[field] = arg; - } - } else { - target[field] = arg; - } - }, - $max: function (target, field, arg) { - if (typeof arg !== "number") { - throw MinimongoError("Modifier $max allowed for numbers only", { field }); - } - if (field in target) { - if (typeof target[field] !== "number") { - throw MinimongoError( - "Cannot apply $max modifier to non-number", { field }); - } - if (target[field] < arg) { - target[field] = arg; - } - } else { - target[field] = arg; - } - }, - $inc: function (target, field, arg) { - if (typeof arg !== "number") - throw MinimongoError("Modifier $inc allowed for numbers only", { field }); - if (field in target) { - if (typeof target[field] !== "number") - throw MinimongoError( - "Cannot apply $inc modifier to non-number", { field }); - target[field] += arg; - } else { - target[field] = arg; - } - }, - $set: function (target, field, arg) { - if (target !== Object(target)) { // not an array or an object - var e = MinimongoError( - "Cannot set property on non-object field", { field }); - e.setPropertyError = true; - throw e; - } - if (target === null) { - var e = MinimongoError("Cannot set property on null", { field }); - e.setPropertyError = true; - throw e; - } - assertHasValidFieldNames(arg); - target[field] = arg; - }, - $setOnInsert: function (target, field, arg) { - // converted to `$set` in `_modify` - }, - $unset: function (target, field, arg) { - if (target !== undefined) { - if (target instanceof Array) { - if (field in target) - target[field] = null; - } else - delete target[field]; - } - }, - $push: function (target, field, arg) { - if (target[field] === undefined) - target[field] = []; - if (!(target[field] instanceof Array)) - throw MinimongoError( - "Cannot apply $push modifier to non-array", { field }); - - if (!(arg && arg.$each)) { - // Simple mode: not $each - assertHasValidFieldNames(arg); - target[field].push(arg); - return; - } - - // Fancy mode: $each (and maybe $slice and $sort and $position) - var toPush = arg.$each; - if (!(toPush instanceof Array)) - throw MinimongoError("$each must be an array", { field }); - assertHasValidFieldNames(toPush); - - // Parse $position - var position = undefined; - if ('$position' in arg) { - if (typeof arg.$position !== "number") - throw MinimongoError("$position must be a numeric value", { field }); - // XXX should check to make sure integer - if (arg.$position < 0) - throw MinimongoError( - "$position in $push must be zero or positive", { field }); - position = arg.$position; - } - - // Parse $slice. - var slice = undefined; - if ('$slice' in arg) { - if (typeof arg.$slice !== "number") - throw MinimongoError("$slice must be a numeric value", { field }); - // XXX should check to make sure integer - slice = arg.$slice; - } - - // Parse $sort. - var sortFunction = undefined; - if (arg.$sort) { - if (slice === undefined) - throw MinimongoError("$sort requires $slice to be present", { field }); - // XXX this allows us to use a $sort whose value is an array, but that's - // actually an extension of the Node driver, so it won't work - // server-side. Could be confusing! - // XXX is it correct that we don't do geo-stuff here? - sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); - for (var i = 0; i < toPush.length; i++) { - if (LocalCollection._f._type(toPush[i]) !== 3) { - throw MinimongoError("$push like modifiers using $sort " + - "require all elements to be objects", { field }); - } - } - } - - // Actually push. - if (position === undefined) { - for (var j = 0; j < toPush.length; j++) - target[field].push(toPush[j]); - } else { - var spliceArguments = [position, 0]; - for (var j = 0; j < toPush.length; j++) - spliceArguments.push(toPush[j]); - Array.prototype.splice.apply(target[field], spliceArguments); - } - - // Actually sort. - if (sortFunction) - target[field].sort(sortFunction); - - // Actually slice. - if (slice !== undefined) { - if (slice === 0) - target[field] = []; // differs from Array.slice! - else if (slice < 0) - target[field] = target[field].slice(slice); - else - target[field] = target[field].slice(0, slice); - } - }, - $pushAll: function (target, field, arg) { - if (!(typeof arg === "object" && arg instanceof Array)) - throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); - assertHasValidFieldNames(arg); - var x = target[field]; - if (x === undefined) - target[field] = arg; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $pushAll modifier to non-array", { field }); - else { - for (var i = 0; i < arg.length; i++) - x.push(arg[i]); - } - }, - $addToSet: function (target, field, arg) { - var isEach = false; - if (typeof arg === "object") { - //check if first key is '$each' - const keys = Object.keys(arg); - if (keys[0] === "$each"){ - isEach = true; - } - } - var values = isEach ? arg["$each"] : [arg]; - assertHasValidFieldNames(values); - var x = target[field]; - if (x === undefined) - target[field] = values; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $addToSet modifier to non-array", { field }); - else { - values.forEach(function (value) { - for (var i = 0; i < x.length; i++) - if (LocalCollection._f._equal(value, x[i])) - return; - x.push(value); - }); - } - }, - $pop: function (target, field, arg) { - if (target === undefined) - return; - var x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $pop modifier to non-array", { field }); - else { - if (typeof arg === 'number' && arg < 0) - x.splice(0, 1); - else - x.pop(); - } - }, - $pull: function (target, field, arg) { - if (target === undefined) - return; - var x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $pull/pullAll modifier to non-array", { field }); - else { - var out = []; - if (arg != null && typeof arg === "object" && !(arg instanceof Array)) { - // XXX would be much nicer to compile this once, rather than - // for each document we modify.. but usually we're not - // modifying that many documents, so we'll let it slide for - // now - - // XXX Minimongo.Matcher isn't up for the job, because we need - // to permit stuff like {$pull: {a: {$gt: 4}}}.. something - // like {$gt: 4} is not normally a complete selector. - // same issue as $elemMatch possibly? - var matcher = new Minimongo.Matcher(arg); - for (var i = 0; i < x.length; i++) - if (!matcher.documentMatches(x[i]).result) - out.push(x[i]); - } else { - for (var i = 0; i < x.length; i++) - if (!LocalCollection._f._equal(x[i], arg)) - out.push(x[i]); - } - target[field] = out; - } - }, - $pullAll: function (target, field, arg) { - if (!(typeof arg === "object" && arg instanceof Array)) - throw MinimongoError( - "Modifier $pushAll/pullAll allowed for arrays only", { field }); - if (target === undefined) - return; - var x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $pull/pullAll modifier to non-array", { field }); - else { - var out = []; - for (var i = 0; i < x.length; i++) { - var exclude = false; - for (var j = 0; j < arg.length; j++) { - if (LocalCollection._f._equal(x[i], arg[j])) { - exclude = true; - break; - } - } - if (!exclude) - out.push(x[i]); - } - target[field] = out; - } - }, - $rename: function (target, field, arg, keypath, doc) { - if (keypath === arg) - // no idea why mongo has this restriction.. - throw MinimongoError("$rename source must differ from target", { field }); - if (target === null) - throw MinimongoError("$rename source field invalid", { field }); - if (typeof arg !== "string") - throw MinimongoError("$rename target must be a string", { field }); - if (arg.indexOf('\0') > -1) { - // Null bytes are not allowed in Mongo field names - // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names - throw MinimongoError( - "The 'to' field for $rename cannot contain an embedded null byte", - { field }); - } - if (target === undefined) - return; - var v = target[field]; - delete target[field]; - - var keyparts = arg.split('.'); - var target2 = findModTarget(doc, keyparts, {forbidArray: true}); - if (target2 === null) - throw MinimongoError("$rename target field invalid", { field }); - var field2 = keyparts.pop(); - target2[field2] = v; - }, - $bit: function (target, field, arg) { - // XXX mongo only supports $bit on integers, and we only support - // native javascript numbers (doubles) so far, so we can't support $bit - throw MinimongoError("$bit is not supported", { field }); - } -}; diff --git a/packages/minimongo/objectid.js b/packages/minimongo/objectid.js deleted file mode 100644 index fdf529a356..0000000000 --- a/packages/minimongo/objectid.js +++ /dev/null @@ -1,57 +0,0 @@ -// Is this selector just shorthand for lookup by _id? -LocalCollection._selectorIsId = function (selector) { - return (typeof selector === "string") || - (typeof selector === "number") || - selector instanceof MongoID.ObjectID; -}; - -// Is the selector just lookup by _id (shorthand or not)? -LocalCollection._selectorIsIdPerhapsAsObject = function (selector) { - return LocalCollection._selectorIsId(selector) || - (selector && typeof selector === "object" && - selector._id && LocalCollection._selectorIsId(selector._id) && - Object.keys(selector).length === 1); -}; - -// If this is a selector which explicitly constrains the match by ID to a finite -// number of documents, returns a list of their IDs. Otherwise returns -// null. Note that the selector may have other restrictions so it may not even -// match those document! We care about $in and $and since those are generated -// access-controlled update and remove. -LocalCollection._idsMatchedBySelector = function (selector) { - // Is the selector just an ID? - if (LocalCollection._selectorIsId(selector)) - return [selector]; - if (!selector) - return null; - - // Do we have an _id clause? - if (selector.hasOwnProperty('_id')) { - // Is the _id clause just an ID? - if (LocalCollection._selectorIsId(selector._id)) - return [selector._id]; - // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? - if (selector._id && selector._id.$in - && Array.isArray(selector._id.$in) - && selector._id.$in.length - && selector._id.$in.every(LocalCollection._selectorIsId)) { - return selector._id.$in; - } - return null; - } - - // If this is a top-level $and, and any of the clauses constrain their - // documents, then the whole selector is constrained by any one clause's - // constraint. (Well, by their intersection, but that seems unlikely.) - if (selector.$and && Array.isArray(selector.$and)) { - for (var i = 0; i < selector.$and.length; ++i) { - var subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); - if (subIds) - return subIds; - } - } - - return null; -}; - - diff --git a/packages/minimongo/observe.js b/packages/minimongo/observe.js deleted file mode 100644 index 6733bf08f7..0000000000 --- a/packages/minimongo/observe.js +++ /dev/null @@ -1,181 +0,0 @@ -// XXX maybe move these into another ObserveHelpers package or something - -// _CachingChangeObserver is an object which receives observeChanges callbacks -// and keeps a cache of the current cursor state up to date in self.docs. Users -// of this class should read the docs field but not modify it. You should pass -// the "applyChange" field as the callbacks to the underlying observeChanges -// call. Optionally, you can specify your own observeChanges callbacks which are -// invoked immediately before the docs field is updated; this object is made -// available as `this` to those callbacks. -LocalCollection._CachingChangeObserver = function (options) { - var self = this; - options = options || {}; - - var orderedFromCallbacks = options.callbacks && - LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); - if (options.hasOwnProperty('ordered')) { - self.ordered = options.ordered; - if (options.callbacks && options.ordered !== orderedFromCallbacks) - throw Error("ordered option doesn't match callbacks"); - } else if (options.callbacks) { - self.ordered = orderedFromCallbacks; - } else { - throw Error("must provide ordered or callbacks"); - } - var callbacks = options.callbacks || {}; - - if (self.ordered) { - self.docs = new OrderedDict(MongoID.idStringify); - self.applyChange = { - addedBefore: function (id, fields, before) { - var doc = EJSON.clone(fields); - doc._id = id; - callbacks.addedBefore && callbacks.addedBefore.call( - self, id, fields, before); - // This line triggers if we provide added with movedBefore. - callbacks.added && callbacks.added.call(self, id, fields); - // XXX could `before` be a falsy ID? Technically - // idStringify seems to allow for them -- though - // OrderedDict won't call stringify on a falsy arg. - self.docs.putBefore(id, doc, before || null); - }, - movedBefore: function (id, before) { - var doc = self.docs.get(id); - callbacks.movedBefore && callbacks.movedBefore.call(self, id, before); - self.docs.moveBefore(id, before || null); - } - }; - } else { - self.docs = new LocalCollection._IdMap; - self.applyChange = { - added: function (id, fields) { - var doc = EJSON.clone(fields); - callbacks.added && callbacks.added.call(self, id, fields); - doc._id = id; - self.docs.set(id, doc); - } - }; - } - - // The methods in _IdMap and OrderedDict used by these callbacks are - // identical. - self.applyChange.changed = function (id, fields) { - var doc = self.docs.get(id); - if (!doc) - throw new Error("Unknown id for changed: " + id); - callbacks.changed && callbacks.changed.call( - self, id, EJSON.clone(fields)); - DiffSequence.applyChanges(doc, fields); - }; - self.applyChange.removed = function (id) { - callbacks.removed && callbacks.removed.call(self, id); - self.docs.remove(id); - }; -}; - -LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) { - var transform = cursor.getTransform() || function (doc) {return doc;}; - var suppressed = !!observeCallbacks._suppress_initial; - - var observeChangesCallbacks; - if (LocalCollection._observeCallbacksAreOrdered(observeCallbacks)) { - // The "_no_indices" option sets all index arguments to -1 and skips the - // linear scans required to generate them. This lets observers that don't - // need absolute indices benefit from the other features of this API -- - // relative order, transforms, and applyChanges -- without the speed hit. - var indices = !observeCallbacks._no_indices; - observeChangesCallbacks = { - addedBefore: function (id, fields, before) { - var self = this; - if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) - return; - var doc = transform(Object.assign(fields, {_id: id})); - if (observeCallbacks.addedAt) { - var index = indices - ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; - observeCallbacks.addedAt(doc, index, before); - } else { - observeCallbacks.added(doc); - } - }, - changed: function (id, fields) { - var self = this; - if (!(observeCallbacks.changedAt || observeCallbacks.changed)) - return; - var doc = EJSON.clone(self.docs.get(id)); - if (!doc) - throw new Error("Unknown id for changed: " + id); - var oldDoc = transform(EJSON.clone(doc)); - DiffSequence.applyChanges(doc, fields); - doc = transform(doc); - if (observeCallbacks.changedAt) { - var index = indices ? self.docs.indexOf(id) : -1; - observeCallbacks.changedAt(doc, oldDoc, index); - } else { - observeCallbacks.changed(doc, oldDoc); - } - }, - movedBefore: function (id, before) { - var self = this; - if (!observeCallbacks.movedTo) - return; - var from = indices ? self.docs.indexOf(id) : -1; - - var to = indices - ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; - // When not moving backwards, adjust for the fact that removing the - // document slides everything back one slot. - if (to > from) - --to; - observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))), - from, to, before || null); - }, - removed: function (id) { - var self = this; - if (!(observeCallbacks.removedAt || observeCallbacks.removed)) - return; - // technically maybe there should be an EJSON.clone here, but it's about - // to be removed from self.docs! - var doc = transform(self.docs.get(id)); - if (observeCallbacks.removedAt) { - var index = indices ? self.docs.indexOf(id) : -1; - observeCallbacks.removedAt(doc, index); - } else { - observeCallbacks.removed(doc); - } - } - }; - } else { - observeChangesCallbacks = { - added: function (id, fields) { - if (!suppressed && observeCallbacks.added) { - var doc = Object.assign(fields, {_id: id}); - observeCallbacks.added(transform(doc)); - } - }, - changed: function (id, fields) { - var self = this; - if (observeCallbacks.changed) { - var oldDoc = self.docs.get(id); - var doc = EJSON.clone(oldDoc); - DiffSequence.applyChanges(doc, fields); - observeCallbacks.changed(transform(doc), - transform(EJSON.clone(oldDoc))); - } - }, - removed: function (id) { - var self = this; - if (observeCallbacks.removed) { - observeCallbacks.removed(transform(self.docs.get(id))); - } - } - }; - } - - var changeObserver = new LocalCollection._CachingChangeObserver( - {callbacks: observeChangesCallbacks}); - var handle = cursor.observeChanges(changeObserver.applyChange); - suppressed = false; - - return handle; -}; diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 17b63b9036..012714325a 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -1,64 +1,46 @@ Package.describe({ - summary: "Meteor's client-side datastore: a port of MongoDB to Javascript", + summary: 'Meteor\'s client-side datastore: a port of MongoDB to Javascript', version: '1.2.1' }); -Package.onUse(function (api) { +Package.onUse(api => { api.export('LocalCollection'); api.export('Minimongo'); + api.export('MinimongoTest', { testOnly: true }); api.export('MinimongoError', { testOnly: true }); + api.use([ + 'diff-sequence', // This package is used to get diff results on arrays and objects + 'ecmascript', 'ejson', + 'geojson-utils', // This package is used for geo-location queries such as $near 'id-map', - 'ordered-dict', - 'tracker', 'mongo-id', + 'ordered-dict', 'random', - 'diff-sequence', - 'ecmascript' - ]); - // This package is used for geo-location queries such as $near - api.use('geojson-utils'); - // This package is used to get diff results on arrays and objects - api.use('diff-sequence'); - - api.addFiles([ - 'minimongo.js', - 'wrap_transform.js', - 'helpers.js', - 'selector.js', - 'sort.js', - 'projection.js', - 'modify.js', - 'diff.js', - 'id_map.js', - 'observe.js', - 'objectid.js' + 'tracker' ]); - // Functionality used only by oplog tailing on the server side - api.addFiles([ - 'selector_projection.js', - 'selector_modifier.js', - 'sorter_projection.js' - ], 'server'); + api.addFiles('minimongo.js'); + api.addFiles('minimongo_server.js', 'server'); }); -Package.onTest(function (api) { - api.use('minimongo', ['client', 'server']); - api.use('test-helpers', 'client'); +Package.onTest(api => { + api.use('minimongo'); api.use([ - 'tinytest', + 'ecmascript', 'ejson', + 'mongo-id', 'ordered-dict', 'random', - 'tracker', 'reactive-var', - 'mongo-id', - 'ecmascript' + 'test-helpers', + 'tinytest', + 'tracker' ]); - api.addFiles('minimongo_tests.js', 'client'); - api.addFiles('wrap_transform_tests.js'); - api.addFiles('minimongo_server_tests.js', 'server'); + + api.addFiles('minimongo_tests.js'); + api.addFiles('minimongo_tests_client.js', 'client'); + api.addFiles('minimongo_tests_server.js', 'server'); }); diff --git a/packages/minimongo/projection.js b/packages/minimongo/projection.js deleted file mode 100644 index d586acbec8..0000000000 --- a/packages/minimongo/projection.js +++ /dev/null @@ -1,176 +0,0 @@ -// Knows how to compile a fields projection to a predicate function. -// @returns - Function: a closure that filters out an object according to the -// fields projection rules: -// @param obj - Object: MongoDB-styled document -// @returns - Object: a document with the fields filtered out -// according to projection rules. Doesn't retain subfields -// of passed argument. -LocalCollection._compileProjection = function (fields) { - LocalCollection._checkSupportedProjection(fields); - - var _idProjection = fields._id === undefined ? true : fields._id; - var details = projectionDetails(fields); - - // returns transformed doc according to ruleTree - var transform = function (doc, ruleTree) { - // Special case for "sets" - if (Array.isArray(doc)) - return doc.map(function (subdoc) { return transform(subdoc, ruleTree); }); - - var res = details.including ? {} : EJSON.clone(doc); - Object.keys(ruleTree).forEach(function (key) { - var rule = ruleTree[key]; - if (!doc.hasOwnProperty(key)) - return; - if (rule === Object(rule)) { - // For sub-objects/subsets we branch - if (doc[key] === Object(doc[key])) - res[key] = transform(doc[key], rule); - // Otherwise we don't even touch this subfield - } else if (details.including) - res[key] = EJSON.clone(doc[key]); - else - delete res[key]; - }); - - return res; - }; - - return function (obj) { - var res = transform(obj, details.tree); - - if (_idProjection && obj.hasOwnProperty('_id')) - res._id = obj._id; - if (!_idProjection && res.hasOwnProperty('_id')) - delete res._id; - return res; - }; -}; - -// Traverses the keys of passed projection and constructs a tree where all -// leaves are either all True or all False -// @returns Object: -// - tree - Object - tree representation of keys involved in projection -// (exception for '_id' as it is a special case handled separately) -// - including - Boolean - "take only certain fields" type of projection -projectionDetails = function (fields) { - // Find the non-_id keys (_id is handled specially because it is included unless - // explicitly excluded). Sort the keys, so that our code to detect overlaps - // like 'foo' and 'foo.bar' can assume that 'foo' comes first. - var fieldsKeys = Object.keys(fields).sort(); - - // If _id is the only field in the projection, do not remove it, since it is - // required to determine if this is an exclusion or exclusion. Also keep an - // inclusive _id, since inclusive _id follows the normal rules about mixing - // inclusive and exclusive fields. If _id is not the only field in the - // projection and is exclusive, remove it so it can be handled later by a - // special case, since exclusive _id is always allowed. - if (fieldsKeys.length > 0 && - !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && - !(fieldsKeys.includes('_id') && fields['_id'])) - fieldsKeys = fieldsKeys.filter(function (key) { return key !== '_id'; }); - - var including = null; // Unknown - - fieldsKeys.forEach(function (keyPath) { - var rule = !!fields[keyPath]; - if (including === null) - including = rule; - if (including !== rule) - // This error message is copied from MongoDB shell - throw MinimongoError("You cannot currently mix including and excluding fields."); - }); - - - var projectionRulesTree = pathsToTree( - fieldsKeys, - function (path) { return including; }, - function (node, path, fullPath) { - // Check passed projection fields' keys: If you have two rules such as - // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If - // that happens, there is a probability you are doing something wrong, - // framework should notify you about such mistake earlier on cursor - // compilation step than later during runtime. Note, that real mongo - // doesn't do anything about it and the later rule appears in projection - // project, more priority it takes. - // - // Example, assume following in mongo shell: - // > db.coll.insert({ a: { b: 23, c: 44 } }) - // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) - // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } } - // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) - // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } - // - // Note, how second time the return set of keys is different. - - var currentPath = fullPath; - var anotherPath = path; - throw MinimongoError("both " + currentPath + " and " + anotherPath + - " found in fields option, using both of them may trigger " + - "unexpected behavior. Did you mean to use only one of them?"); - }); - - return { - tree: projectionRulesTree, - including: including - }; -}; - -// paths - Array: list of mongo style paths -// newLeafFn - Function: of form function(path) should return a scalar value to -// put into list created for that path -// conflictFn - Function: of form function(node, path, fullPath) is called -// when building a tree path for 'fullPath' node on -// 'path' was already a leaf with a value. Must return a -// conflict resolution. -// initial tree - Optional Object: starting tree. -// @returns - Object: tree represented as a set of nested objects -pathsToTree = function (paths, newLeafFn, conflictFn, tree) { - tree = tree || {}; - paths.forEach(function (keyPath) { - var treePos = tree; - var pathArr = keyPath.split('.'); - - // use .every just for iteration with break - var success = pathArr.slice(0, -1).every(function (key, idx) { - if (!treePos.hasOwnProperty(key)) - treePos[key] = {}; - else if (treePos[key] !== Object(treePos[key])) { - treePos[key] = conflictFn(treePos[key], - pathArr.slice(0, idx + 1).join('.'), - keyPath); - // break out of loop if we are failing for this path - if (treePos[key] !== Object(treePos[key])) - return false; - } - - treePos = treePos[key]; - return true; - }); - - if (success) { - var lastKey = pathArr[pathArr.length - 1]; - if (!treePos.hasOwnProperty(lastKey)) - treePos[lastKey] = newLeafFn(keyPath); - else - treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); - } - }); - - return tree; -}; - -LocalCollection._checkSupportedProjection = function (fields) { - if (fields !== Object(fields) || Array.isArray(fields)) - throw MinimongoError("fields option must be an object"); - - Object.keys(fields).forEach(function (keyPath) { - var val = fields[keyPath]; - if (keyPath.split('.').includes('$')) - throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); - if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) - throw MinimongoError("Minimongo doesn't support operators in projections yet."); - if (![1, 0, true, false].includes(val)) - throw MinimongoError("Projection values should be one of 1, 0, true, or false"); - }); -}; diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js deleted file mode 100644 index 89aceab829..0000000000 --- a/packages/minimongo/selector.js +++ /dev/null @@ -1,1284 +0,0 @@ -// The minimongo selector compiler! - -// Terminology: -// - a "selector" is the EJSON object representing a selector -// - a "matcher" is its compiled form (whether a full Minimongo.Matcher -// object or one of the component lambdas that matches parts of it) -// - a "result object" is an object with a "result" field and maybe -// distance and arrayIndices. -// - a "branched value" is an object with a "value" field and maybe -// "dontIterate" and "arrayIndices". -// - a "document" is a top-level object that can be stored in a collection. -// - a "lookup function" is a function that takes in a document and returns -// an array of "branched values". -// - a "branched matcher" maps from an array of branched values to a result -// object. -// - an "element matcher" maps from a single value to a bool. - -// Main entry point. -// var matcher = new Minimongo.Matcher({a: {$gt: 5}}); -// if (matcher.documentMatches({a: 7})) ... -Minimongo.Matcher = function (selector, isUpdate = false) { - var self = this; - // A set (object mapping string -> *) of all of the document paths looked - // at by the selector. Also includes the empty string if it may look at any - // path (eg, $where). - self._paths = {}; - // Set to true if compilation finds a $near. - self._hasGeoQuery = false; - // Set to true if compilation finds a $where. - self._hasWhere = false; - // Set to false if compilation finds anything other than a simple equality or - // one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used with - // scalars as operands. - self._isSimple = true; - // Set to a dummy document which always matches this Matcher. Or set to null - // if such document is too hard to find. - self._matchingDocument = undefined; - // A clone of the original selector. It may just be a function if the user - // passed in a function; otherwise is definitely an object (eg, IDs are - // translated into {_id: ID} first. Used by canBecomeTrueByModifier and - // Sorter._useWithMatcher. - self._selector = null; - self._docMatcher = self._compileSelector(selector); - // Set to true if selection is done for an update operation - // Default is false - // Used for $near array update (issue #3599) - self._isUpdate = isUpdate; -}; - -Object.assign(Minimongo.Matcher.prototype, { - documentMatches: function (doc) { - if (!doc || typeof doc !== "object") { - throw Error("documentMatches needs a document"); - } - return this._docMatcher(doc); - }, - hasGeoQuery: function () { - return this._hasGeoQuery; - }, - hasWhere: function () { - return this._hasWhere; - }, - isSimple: function () { - return this._isSimple; - }, - - // Given a selector, return a function that takes one argument, a - // document. It returns a result object. - _compileSelector: function (selector) { - var self = this; - // you can pass a literal function instead of a selector - if (selector instanceof Function) { - self._isSimple = false; - self._selector = selector; - self._recordPathUsed(''); - return function (doc) { - return {result: !!selector.call(doc)}; - }; - } - - // shorthand -- scalars match _id - if (LocalCollection._selectorIsId(selector)) { - self._selector = {_id: selector}; - self._recordPathUsed('_id'); - return function (doc) { - return {result: EJSON.equals(doc._id, selector)}; - }; - } - - // protect against dangerous selectors. falsey and {_id: falsey} are both - // likely programmer error, and not what you want, particularly for - // destructive operations. - if (!selector || (('_id' in selector) && !selector._id)) { - self._isSimple = false; - return nothingMatcher; - } - - // Top level can't be an array or true or binary. - if (typeof(selector) === 'boolean' || isArray(selector) || - EJSON.isBinary(selector)) - throw new Error("Invalid selector: " + selector); - - self._selector = EJSON.clone(selector); - return compileDocumentSelector(selector, self, {isRoot: true}); - }, - _recordPathUsed: function (path) { - this._paths[path] = true; - }, - // Returns a list of key paths the given selector is looking for. It includes - // the empty string if there is a $where. - _getPaths: function () { - return Object.keys(this._paths); - } -}); - - -// Takes in a selector that could match a full document (eg, the original -// selector). Returns a function mapping document->result object. -// -// matcher is the Matcher object we are compiling. -// -// If this is the root document selector (ie, not wrapped in $and or the like), -// then isRoot is true. (This is used by $near.) -var compileDocumentSelector = function (docSelector, matcher, options) { - options = options || {}; - var docMatchers = []; - Object.keys(docSelector).forEach(function (key) { - var subSelector = docSelector[key]; - if (key.substr(0, 1) === '$') { - // Outer operators are either logical operators (they recurse back into - // this function), or $where. - if (!LOGICAL_OPERATORS.hasOwnProperty(key)) - throw new Error("Unrecognized logical operator: " + key); - matcher._isSimple = false; - docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, - options.inElemMatch)); - } else { - // Record this path, but only if we aren't in an elemMatcher, since in an - // elemMatch this is a path inside an object in an array, not in the doc - // root. - if (!options.inElemMatch) - matcher._recordPathUsed(key); - var lookUpByIndex = makeLookupFunction(key); - var valueMatcher = - compileValueSelector(subSelector, matcher, options.isRoot); - docMatchers.push(function (doc) { - var branchValues = lookUpByIndex(doc); - return valueMatcher(branchValues); - }); - } - }); - - return andDocumentMatchers(docMatchers); -}; - -// Takes in a selector that could match a key-indexed value in a document; eg, -// {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to -// indicate equality). Returns a branched matcher: a function mapping -// [branched value]->result object. -var compileValueSelector = function (valueSelector, matcher, isRoot) { - if (valueSelector instanceof RegExp) { - matcher._isSimple = false; - return convertElementMatcherToBranchedMatcher( - regexpElementMatcher(valueSelector)); - } else if (isOperatorObject(valueSelector)) { - return operatorBranchedMatcher(valueSelector, matcher, isRoot); - } else { - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(valueSelector)); - } -}; - -// Given an element matcher (which evaluates a single value), returns a branched -// value (which evaluates the element matcher on all the branches and returns a -// more structured return value possibly including arrayIndices). -var convertElementMatcherToBranchedMatcher = function ( - elementMatcher, options) { - options = options || {}; - return function (branches) { - var expanded = branches; - if (!options.dontExpandLeafArrays) { - expanded = expandArraysInBranches( - branches, options.dontIncludeLeafArrays); - } - var ret = {}; - ret.result = expanded.some(function (element) { - var matched = elementMatcher(element.value); - - // Special case for $elemMatch: it means "true, and use this as an array - // index if I didn't already have one". - if (typeof matched === 'number') { - // XXX This code dates from when we only stored a single array index - // (for the outermost array). Should we be also including deeper array - // indices from the $elemMatch match? - if (!element.arrayIndices) - element.arrayIndices = [matched]; - matched = true; - } - - // If some element matched, and it's tagged with array indices, include - // those indices in our result object. - if (matched && element.arrayIndices) - ret.arrayIndices = element.arrayIndices; - - return matched; - }); - return ret; - }; -}; - -// Takes a RegExp object and returns an element matcher. -regexpElementMatcher = function (regexp) { - return function (value) { - if (value instanceof RegExp) { - return value.toString() === regexp.toString(); - } - // Regexps only work against strings. - if (typeof value !== 'string') - return false; - - // Reset regexp's state to avoid inconsistent matching for objects with the - // same value on consecutive calls of regexp.test. This happens only if the - // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for - // which we should *not* change the lastIndex but MongoDB doesn't support - // either of these flags. - regexp.lastIndex = 0; - - return regexp.test(value); - }; -}; - -// Takes something that is not an operator object and returns an element matcher -// for equality with that thing. -equalityElementMatcher = function (elementSelector) { - if (isOperatorObject(elementSelector)) - throw Error("Can't create equalityValueSelector for operator object"); - - // Special-case: null and undefined are equal (if you got undefined in there - // somewhere, or if you got it due to some branch being non-existent in the - // weird special case), even though they aren't with EJSON.equals. - if (elementSelector == null) { // undefined or null - return function (value) { - return value == null; // undefined or null - }; - } - - return function (value) { - return LocalCollection._f._equal(elementSelector, value); - }; -}; - -// Takes an operator object (an object with $ keys) and returns a branched -// matcher for it. -var operatorBranchedMatcher = function (valueSelector, matcher, isRoot) { - // Each valueSelector works separately on the various branches. So one - // operator can match one branch and another can match another branch. This - // is OK. - - var operatorMatchers = []; - Object.keys(valueSelector).forEach(function (operator) { - var operand = valueSelector[operator]; - var simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && - typeof operand === 'number'; - var simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); - var simpleInclusion = ['$in', '$nin'].includes(operator) && - Array.isArray(operand) && !operand.some(function (x) { return x === Object(x); }); - - if (! (simpleRange || simpleInclusion || simpleEquality)) { - matcher._isSimple = false; - } - - if (VALUE_OPERATORS.hasOwnProperty(operator)) { - operatorMatchers.push( - VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); - } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { - var options = ELEMENT_OPERATORS[operator]; - operatorMatchers.push( - convertElementMatcherToBranchedMatcher( - options.compileElementSelector( - operand, valueSelector, matcher), - options)); - } else { - throw new Error("Unrecognized operator: " + operator); - } - }); - - return andBranchedMatchers(operatorMatchers); -}; - -var compileArrayOfDocumentSelectors = function ( - selectors, matcher, inElemMatch) { - if (!isArray(selectors) || selectors.length === 0) - throw Error("$and/$or/$nor must be nonempty array"); - return selectors.map(function (subSelector) { - if (!isPlainObject(subSelector)) - throw Error("$or/$and/$nor entries need to be full objects"); - return compileDocumentSelector( - subSelector, matcher, {inElemMatch: inElemMatch}); - }); -}; - -// Operators that appear at the top level of a document selector. -var LOGICAL_OPERATORS = { - $and: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - return andDocumentMatchers(matchers); - }, - - $or: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - - // Special case: if there is only one matcher, use it directly, *preserving* - // any arrayIndices it returns. - if (matchers.length === 1) - return matchers[0]; - - return function (doc) { - var result = matchers.some(function (f) { - return f(doc).result; - }); - // $or does NOT set arrayIndices when it has multiple - // sub-expressions. (Tested against MongoDB.) - return {result: result}; - }; - }, - - $nor: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - return function (doc) { - var result = matchers.every(function (f) { - return !f(doc).result; - }); - // Never set arrayIndices, because we only match if nothing in particular - // "matched" (and because this is consistent with MongoDB). - return {result: result}; - }; - }, - - $where: function (selectorValue, matcher) { - // Record that *any* path may be used. - matcher._recordPathUsed(''); - matcher._hasWhere = true; - if (!(selectorValue instanceof Function)) { - // XXX MongoDB seems to have more complex logic to decide where or or not - // to add "return"; not sure exactly what it is. - selectorValue = Function("obj", "return " + selectorValue); - } - return function (doc) { - // We make the document available as both `this` and `obj`. - // XXX not sure what we should do if this throws - return {result: selectorValue.call(doc, doc)}; - }; - }, - - // This is just used as a comment in the query (in MongoDB, it also ends up in - // query logs); it has no effect on the actual selection. - $comment: function () { - return function () { - return {result: true}; - }; - } -}; - -// Returns a branched matcher that matches iff the given matcher does not. -// Note that this implicitly "deMorganizes" the wrapped function. ie, it -// means that ALL branch values need to fail to match innerBranchedMatcher. -var invertBranchedMatcher = function (branchedMatcher) { - return function (branchValues) { - var invertMe = branchedMatcher(branchValues); - // We explicitly choose to strip arrayIndices here: it doesn't make sense to - // say "update the array element that does not match something", at least - // in mongo-land. - return {result: !invertMe.result}; - }; -}; - -// Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a -// document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as -// "match each branched value independently and combine with -// convertElementMatcherToBranchedMatcher". -var VALUE_OPERATORS = { - $eq: function (operand) { - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(operand)); - }, - $not: function (operand, valueSelector, matcher) { - return invertBranchedMatcher(compileValueSelector(operand, matcher)); - }, - $ne: function (operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( - equalityElementMatcher(operand))); - }, - $nin: function (operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( - ELEMENT_OPERATORS.$in.compileElementSelector(operand))); - }, - $exists: function (operand) { - var exists = convertElementMatcherToBranchedMatcher(function (value) { - return value !== undefined; - }); - return operand ? exists : invertBranchedMatcher(exists); - }, - // $options just provides options for $regex; its logic is inside $regex - $options: function (operand, valueSelector) { - if (!valueSelector.hasOwnProperty('$regex')) - throw Error("$options needs a $regex"); - return everythingMatcher; - }, - // $maxDistance is basically an argument to $near - $maxDistance: function (operand, valueSelector) { - if (!valueSelector.$near) - throw Error("$maxDistance needs a $near"); - return everythingMatcher; - }, - $all: function (operand, valueSelector, matcher) { - if (!isArray(operand)) - throw Error("$all requires array"); - // Not sure why, but this seems to be what MongoDB does. - if (operand.length === 0) - return nothingMatcher; - - var branchedMatchers = []; - operand.forEach(function (criterion) { - // XXX handle $all/$elemMatch combination - if (isOperatorObject(criterion)) - throw Error("no $ expressions in $all"); - // This is always a regexp or equality selector. - branchedMatchers.push(compileValueSelector(criterion, matcher)); - }); - // andBranchedMatchers does NOT require all selectors to return true on the - // SAME branch. - return andBranchedMatchers(branchedMatchers); - }, - $near: function (operand, valueSelector, matcher, isRoot) { - if (!isRoot) - throw Error("$near can't be inside another $ operator"); - matcher._hasGeoQuery = true; - - // There are two kinds of geodata in MongoDB: legacy coordinate pairs and - // GeoJSON. They use different distance metrics, too. GeoJSON queries are - // marked with a $geometry property, though legacy coordinates can be - // matched using $geometry. - - var maxDistance, point, distance; - if (isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { - // GeoJSON "2dsphere" mode. - maxDistance = operand.$maxDistance; - point = operand.$geometry; - distance = function (value) { - // XXX: for now, we don't calculate the actual distance between, say, - // polygon and circle. If people care about this use-case it will get - // a priority. - if (!value) - return null; - if(!value.type) - return GeoJSON.pointDistance(point, - { type: "Point", coordinates: pointToArray(value) }); - if (value.type === "Point") { - return GeoJSON.pointDistance(point, value); - } else { - return GeoJSON.geometryWithinRadius(value, point, maxDistance) - ? 0 : maxDistance + 1; - } - }; - } else { - maxDistance = valueSelector.$maxDistance; - if (!isArray(operand) && !isPlainObject(operand)) - throw Error("$near argument must be coordinate pair or GeoJSON"); - point = pointToArray(operand); - distance = function (value) { - if (!isArray(value) && !isPlainObject(value)) - return null; - return distanceCoordinatePairs(point, value); - }; - } - - return function (branchedValues) { - // There might be multiple points in the document that match the given - // field. Only one of them needs to be within $maxDistance, but we need to - // evaluate all of them and use the nearest one for the implicit sort - // specifier. (That's why we can't just use ELEMENT_OPERATORS here.) - // - // Note: This differs from MongoDB's implementation, where a document will - // actually show up *multiple times* in the result set, with one entry for - // each within-$maxDistance branching point. - branchedValues = expandArraysInBranches(branchedValues); - var result = {result: false}; - branchedValues.every(function (branch) { - // if operation is an update, don't skip branches, just return the first one (#3599) - if (!matcher._isUpdate){ - if (!(typeof branch.value === "object")){ - return true; - } - var curDistance = distance(branch.value); - // Skip branches that aren't real points or are too far away. - if (curDistance === null || curDistance > maxDistance) - return true; - // Skip anything that's a tie. - if (result.distance !== undefined && result.distance <= curDistance) - return true; - } - result.result = true; - result.distance = curDistance; - if (!branch.arrayIndices) - delete result.arrayIndices; - else - result.arrayIndices = branch.arrayIndices; - if (matcher._isUpdate) - return false; - return true; - }); - return result; - }; - } -}; - -// Helpers for $near. -var distanceCoordinatePairs = function (a, b) { - a = pointToArray(a); - b = pointToArray(b); - var x = a[0] - b[0]; - var y = a[1] - b[1]; - if (Number.isNaN(x) || Number.isNaN(y)) - return null; - return Math.sqrt(x * x + y * y); -}; -// Makes sure we get 2 elements array and assume the first one to be x and -// the second one to y no matter what user passes. -// In case user passes { lon: x, lat: y } returns [x, y] -var pointToArray = function (point) { - return Array.isArray(point) ? point.slice() : [point.x, point.y]; -}; - -// Helper for $lt/$gt/$lte/$gte. -var makeInequality = function (cmpValueComparator) { - return { - compileElementSelector: function (operand) { - // Arrays never compare false with non-arrays for any inequality. - // XXX This was behavior we observed in pre-release MongoDB 2.5, but - // it seems to have been reverted. - // See https://jira.mongodb.org/browse/SERVER-11444 - if (isArray(operand)) { - return function () { - return false; - }; - } - - // Special case: consider undefined and null the same (so true with - // $gte/$lte). - if (operand === undefined) - operand = null; - - var operandType = LocalCollection._f._type(operand); - - return function (value) { - if (value === undefined) - value = null; - // Comparisons are never true among things of different type (except - // null vs undefined). - if (LocalCollection._f._type(value) !== operandType) - return false; - return cmpValueComparator(LocalCollection._f._cmp(value, operand)); - }; - } - }; -}; - -// Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear. -var getOperandBitmask = function(operand, selector) { - // numeric bitmask - // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. - // Otherwise, $bitsAllSet will return an error. - if (Number.isInteger(operand) && operand >= 0) { - return new Uint8Array(new Int32Array([operand]).buffer) - } - // bindata bitmask - // You can also use an arbitrarily large BinData instance as a bitmask. - else if (EJSON.isBinary(operand)) { - return new Uint8Array(operand.buffer) - } - // position list - // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. - else if (isArray(operand) && operand.every(function (e) { - return Number.isInteger(e) && e >= 0 - })) { - var buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1) - var view = new Uint8Array(buffer) - operand.forEach(function (x) { - view[x >> 3] |= (1 << (x & 0x7)) - }) - return view - } - // bad operand - else { - throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`) - } -} -var getValueBitmask = function (value, length) { - // The field value must be either numerical or a BinData instance. Otherwise, $bits... will not match the current document. - // numerical - if (Number.isSafeInteger(value)) { - // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer - // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. - var buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); - var view = new Uint32Array(buffer, 0, 2) - view[0] = (value % ((1 << 16) * (1 << 16))) | 0 - view[1] = (value / ((1 << 16) * (1 << 16))) | 0 - // sign extension - if (value < 0) { - view = new Uint8Array(buffer, 2) - view.forEach(function (byte, idx) { - view[idx] = 0xff - }) - } - return new Uint8Array(buffer) - } - // bindata - else if (EJSON.isBinary(value)) { - return new Uint8Array(value.buffer) - } - // no match - return false -} - -// Each element selector contains: -// - compileElementSelector, a function with args: -// - operand - the "right hand side" of the operator -// - valueSelector - the "context" for the operator (so that $regex can find -// $options) -// - matcher - the Matcher this is going into (so that $elemMatch can compile -// more things) -// returning a function mapping a single value to bool. -// - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from -// being called -// - dontIncludeLeafArrays, a bool which causes an argument to be passed to -// expandArraysInBranches if it is called -ELEMENT_OPERATORS = { - $lt: makeInequality(function (cmpValue) { - return cmpValue < 0; - }), - $gt: makeInequality(function (cmpValue) { - return cmpValue > 0; - }), - $lte: makeInequality(function (cmpValue) { - return cmpValue <= 0; - }), - $gte: makeInequality(function (cmpValue) { - return cmpValue >= 0; - }), - $mod: { - compileElementSelector: function (operand) { - if (!(isArray(operand) && operand.length === 2 - && typeof(operand[0]) === 'number' - && typeof(operand[1]) === 'number')) { - throw Error("argument to $mod must be an array of two numbers"); - } - // XXX could require to be ints or round or something - var divisor = operand[0]; - var remainder = operand[1]; - return function (value) { - return typeof value === 'number' && value % divisor === remainder; - }; - } - }, - $in: { - compileElementSelector: function (operand) { - if (!isArray(operand)) - throw Error("$in needs an array"); - - var elementMatchers = []; - operand.forEach(function (option) { - if (option instanceof RegExp) - elementMatchers.push(regexpElementMatcher(option)); - else if (isOperatorObject(option)) - throw Error("cannot nest $ under $in"); - else - elementMatchers.push(equalityElementMatcher(option)); - }); - - return function (value) { - // Allow {a: {$in: [null]}} to match when 'a' does not exist. - if (value === undefined) - value = null; - return elementMatchers.some(function (e) { - return e(value); - }); - }; - } - }, - $size: { - // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we - // don't want to consider the element [5,5] in the leaf array [[5,5]] as a - // possible value. - dontExpandLeafArrays: true, - compileElementSelector: function (operand) { - if (typeof operand === 'string') { - // Don't ask me why, but by experimentation, this seems to be what Mongo - // does. - operand = 0; - } else if (typeof operand !== 'number') { - throw Error("$size needs a number"); - } - return function (value) { - return isArray(value) && value.length === operand; - }; - } - }, - $type: { - // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should - // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: - // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but - // should *not* include it itself. - dontIncludeLeafArrays: true, - compileElementSelector: function (operand) { - if (typeof operand !== 'number') - throw Error("$type needs a number"); - return function (value) { - return value !== undefined - && LocalCollection._f._type(value) === operand; - }; - } - }, - $bitsAllSet: { - compileElementSelector: function (operand) { - var op = getOperandBitmask(operand, '$bitsAllSet') - return function (value) { - var bitmask = getValueBitmask(value, op.length) - return bitmask && op.every(function (byte, idx) { - return ((bitmask[idx] & byte) == byte) - }) - } - } - }, - $bitsAnySet: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAnySet') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.some(function (byte, idx) { - return ((~bitmask[idx] & byte) !== byte) - }) - } - } - }, - $bitsAllClear: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAllClear') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.every(function (byte, idx) { - return !(bitmask[idx] & byte) - }) - } - } - }, - $bitsAnyClear: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAnyClear') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.some(function (byte, idx) { - return ((bitmask[idx] & byte) !== byte) - }) - } - } - }, - $regex: { - compileElementSelector: function (operand, valueSelector) { - if (!(typeof operand === 'string' || operand instanceof RegExp)) - throw Error("$regex has to be a string or RegExp"); - - var regexp; - if (valueSelector.$options !== undefined) { - // Options passed in $options (even the empty string) always overrides - // options in the RegExp object itself. (See also - // Mongo.Collection._rewriteSelector.) - - // Be clear that we only support the JS-supported options, not extended - // ones (eg, Mongo supports x and s). Ideally we would implement x and s - // by transforming the regexp, but not today... - if (/[^gim]/.test(valueSelector.$options)) - throw new Error("Only the i, m, and g regexp options are supported"); - - var regexSource = operand instanceof RegExp ? operand.source : operand; - regexp = new RegExp(regexSource, valueSelector.$options); - } else if (operand instanceof RegExp) { - regexp = operand; - } else { - regexp = new RegExp(operand); - } - return regexpElementMatcher(regexp); - } - }, - $elemMatch: { - dontExpandLeafArrays: true, - compileElementSelector: function (operand, valueSelector, matcher) { - if (!isPlainObject(operand)) - throw Error("$elemMatch need an object"); - - var subMatcher, isDocMatcher; - if (isOperatorObject(Object.keys(operand) - .filter(function (key) { return !Object.keys(LOGICAL_OPERATORS).includes(key); }) - .reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { - subMatcher = compileValueSelector(operand, matcher); - isDocMatcher = false; - } else { - // This is NOT the same as compileValueSelector(operand), and not just - // because of the slightly different calling convention. - // {$elemMatch: {x: 3}} means "an element has a field x:3", not - // "consists only of a field x:3". Also, regexps and sub-$ are allowed. - subMatcher = compileDocumentSelector(operand, matcher, - {inElemMatch: true}); - isDocMatcher = true; - } - - return function (value) { - if (!isArray(value)) - return false; - for (var i = 0; i < value.length; ++i) { - var arrayElement = value[i]; - var arg; - if (isDocMatcher) { - // We can only match {$elemMatch: {b: 3}} against objects. - // (We can also match against arrays, if there's numeric indices, - // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) - if (!isPlainObject(arrayElement) && !isArray(arrayElement)) - return false; - arg = arrayElement; - } else { - // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches - // {a: [8]} but not {a: [[8]]} - arg = [{value: arrayElement, dontIterate: true}]; - } - // XXX support $near in $elemMatch by propagating $distance? - if (subMatcher(arg).result) - return i; // specially understood to mean "use as arrayIndices" - } - return false; - }; - } - } -}; - -// makeLookupFunction(key) returns a lookup function. -// -// A lookup function takes in a document and returns an array of matching -// branches. If no arrays are found while looking up the key, this array will -// have exactly one branches (possibly 'undefined', if some segment of the key -// was not found). -// -// If arrays are found in the middle, this can have more than one element, since -// we "branch". When we "branch", if there are more key segments to look up, -// then we only pursue branches that are plain objects (not arrays or scalars). -// This means we can actually end up with no branches! -// -// We do *NOT* branch on arrays that are found at the end (ie, at the last -// dotted member of the key). We just return that array; if you want to -// effectively "branch" over the array's values, post-process the lookup -// function with expandArraysInBranches. -// -// Each branch is an object with keys: -// - value: the value at the branch -// - dontIterate: an optional bool; if true, it means that 'value' is an array -// that expandArraysInBranches should NOT expand. This specifically happens -// when there is a numeric index in the key, and ensures the -// perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT -// match {a: [[5]]}. -// - arrayIndices: if any array indexing was done during lookup (either due to -// explicit numeric indices or implicit branching), this will be an array of -// the array indices used, from outermost to innermost; it is falsey or -// absent if no array index is used. If an explicit numeric index is used, -// the index will be followed in arrayIndices by the string 'x'. -// -// Note: arrayIndices is used for two purposes. First, it is used to -// implement the '$' modifier feature, which only ever looks at its first -// element. -// -// Second, it is used for sort key generation, which needs to be able to tell -// the difference between different paths. Moreover, it needs to -// differentiate between explicit and implicit branching, which is why -// there's the somewhat hacky 'x' entry: this means that explicit and -// implicit array lookups will have different full arrayIndices paths. (That -// code only requires that different paths have different arrayIndices; it -// doesn't actually "parse" arrayIndices. As an alternative, arrayIndices -// could contain objects with flags like "implicit", but I think that only -// makes the code surrounding them more complex.) -// -// (By the way, this field ends up getting passed around a lot without -// cloning, so never mutate any arrayIndices field/var in this package!) -// -// -// At the top level, you may only pass in a plain object or array. -// -// See the test 'minimongo - lookup' for some examples of what lookup functions -// return. -makeLookupFunction = function (key, options) { - options = options || {}; - var parts = key.split('.'); - var firstPart = parts.length ? parts[0] : ''; - var firstPartIsNumeric = isNumericKey(firstPart); - var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); - var lookupRest; - if (parts.length > 1) { - lookupRest = makeLookupFunction(parts.slice(1).join('.')); - } - - var omitUnnecessaryFields = function (retVal) { - if (!retVal.dontIterate) - delete retVal.dontIterate; - if (retVal.arrayIndices && !retVal.arrayIndices.length) - delete retVal.arrayIndices; - return retVal; - }; - - // Doc will always be a plain object or an array. - // apply an explicit numeric index, an array. - return function (doc, arrayIndices) { - if (!arrayIndices) - arrayIndices = []; - - if (isArray(doc)) { - // If we're being asked to do an invalid lookup into an array (non-integer - // or out-of-bounds), return no results (which is different from returning - // a single undefined result, in that `null` equality checks won't match). - if (!(firstPartIsNumeric && firstPart < doc.length)) - return []; - - // Remember that we used this array index. Include an 'x' to indicate that - // the previous index came from being considered as an explicit array - // index (not branching). - arrayIndices = arrayIndices.concat(+firstPart, 'x'); - } - - // Do our first lookup. - var firstLevel = doc[firstPart]; - - // If there is no deeper to dig, return what we found. - // - // If what we found is an array, most value selectors will choose to treat - // the elements of the array as matchable values in their own right, but - // that's done outside of the lookup function. (Exceptions to this are $size - // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a: - // [[1, 2]]}.) - // - // That said, if we just did an *explicit* array lookup (on doc) to find - // firstLevel, and firstLevel is an array too, we do NOT want value - // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}. - // So in that case, we mark the return value as "don't iterate". - if (!lookupRest) { - return [omitUnnecessaryFields({ - value: firstLevel, - dontIterate: isArray(doc) && isArray(firstLevel), - arrayIndices: arrayIndices})]; - } - - // We need to dig deeper. But if we can't, because what we've found is not - // an array or plain object, we're done. If we just did a numeric index into - // an array, we return nothing here (this is a change in Mongo 2.5 from - // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise, - // return a single `undefined` (which can, for example, match via equality - // with `null`). - if (!isIndexable(firstLevel)) { - if (isArray(doc)) - return []; - return [omitUnnecessaryFields({value: undefined, - arrayIndices: arrayIndices})]; - } - - var result = []; - var appendToResult = function (more) { - Array.prototype.push.apply(result, more); - }; - - // Dig deeper: look up the rest of the parts on whatever we've found. - // (lookupRest is smart enough to not try to do invalid lookups into - // firstLevel if it's an array.) - appendToResult(lookupRest(firstLevel, arrayIndices)); - - // If we found an array, then in *addition* to potentially treating the next - // part as a literal integer lookup, we should also "branch": try to look up - // the rest of the parts on each array element in parallel. - // - // In this case, we *only* dig deeper into array elements that are plain - // objects. (Recall that we only got this far if we have further to dig.) - // This makes sense: we certainly don't dig deeper into non-indexable - // objects. And it would be weird to dig into an array: it's simpler to have - // a rule that explicit integer indexes only apply to an outer array, not to - // an array you find after a branching search. - // - // In the special case of a numeric part in a *sort selector* (not a query - // selector), we skip the branching: we ONLY allow the numeric part to mean - // "look up this index" in that case, not "also look up this index in all - // the elements of the array". - if (isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { - firstLevel.forEach(function (branch, arrayIndex) { - if (isPlainObject(branch)) { - appendToResult(lookupRest( - branch, - arrayIndices.concat(arrayIndex))); - } - }); - } - - return result; - }; -}; -MinimongoTest.makeLookupFunction = makeLookupFunction; - -expandArraysInBranches = function (branches, skipTheArrays) { - var branchesOut = []; - branches.forEach(function (branch) { - var thisIsArray = isArray(branch.value); - // We include the branch itself, *UNLESS* we it's an array that we're going - // to iterate and we're told to skip arrays. (That's right, we include some - // arrays even skipTheArrays is true: these are arrays that were found via - // explicit numerical indices.) - if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { - branchesOut.push({ - value: branch.value, - arrayIndices: branch.arrayIndices - }); - } - if (thisIsArray && !branch.dontIterate) { - branch.value.forEach(function (leaf, i) { - branchesOut.push({ - value: leaf, - arrayIndices: (branch.arrayIndices || []).concat(i) - }); - }); - } - }); - return branchesOut; -}; - -var nothingMatcher = function (docOrBranchedValues) { - return {result: false}; -}; - -var everythingMatcher = function (docOrBranchedValues) { - return {result: true}; -}; - - -// NB: We are cheating and using this function to implement "AND" for both -// "document matchers" and "branched matchers". They both return result objects -// but the argument is different: for the former it's a whole doc, whereas for -// the latter it's an array of "branched values". -var andSomeMatchers = function (subMatchers) { - if (subMatchers.length === 0) - return everythingMatcher; - if (subMatchers.length === 1) - return subMatchers[0]; - - return function (docOrBranches) { - var ret = {}; - ret.result = subMatchers.every(function (f) { - var subResult = f(docOrBranches); - // Copy a 'distance' number out of the first sub-matcher that has - // one. Yes, this means that if there are multiple $near fields in a - // query, something arbitrary happens; this appears to be consistent with - // Mongo. - if (subResult.result && subResult.distance !== undefined - && ret.distance === undefined) { - ret.distance = subResult.distance; - } - // Similarly, propagate arrayIndices from sub-matchers... but to match - // MongoDB behavior, this time the *last* sub-matcher with arrayIndices - // wins. - if (subResult.result && subResult.arrayIndices) { - ret.arrayIndices = subResult.arrayIndices; - } - return subResult.result; - }); - - // If we didn't actually match, forget any extra metadata we came up with. - if (!ret.result) { - delete ret.distance; - delete ret.arrayIndices; - } - return ret; - }; -}; - -var andDocumentMatchers = andSomeMatchers; -var andBranchedMatchers = andSomeMatchers; - - -// helpers used by compiled selector code -LocalCollection._f = { - // XXX for _all and _in, consider building 'inquery' at compile time.. - - _type: function (v) { - if (typeof v === "number") - return 1; - if (typeof v === "string") - return 2; - if (typeof v === "boolean") - return 8; - if (isArray(v)) - return 4; - if (v === null) - return 10; - if (v instanceof RegExp) - // note that typeof(/x/) === "object" - return 11; - if (typeof v === "function") - return 13; - if (v instanceof Date) - return 9; - if (EJSON.isBinary(v)) - return 5; - if (v instanceof MongoID.ObjectID) - return 7; - return 3; // object - - // XXX support some/all of these: - // 14, symbol - // 15, javascript code with scope - // 16, 18: 32-bit/64-bit integer - // 17, timestamp - // 255, minkey - // 127, maxkey - }, - - // deep equality test: use for literal document and array matches - _equal: function (a, b) { - return EJSON.equals(a, b, {keyOrderSensitive: true}); - }, - - // maps a type code to a value that can be used to sort values of - // different types - _typeorder: function (t) { - // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types - // XXX what is the correct sort position for Javascript code? - // ('100' in the matrix below) - // XXX minkey/maxkey - return [-1, // (not a type) - 1, // number - 2, // string - 3, // object - 4, // array - 5, // binary - -1, // deprecated - 6, // ObjectID - 7, // bool - 8, // Date - 0, // null - 9, // RegExp - -1, // deprecated - 100, // JS code - 2, // deprecated (symbol) - 100, // JS code - 1, // 32-bit int - 8, // Mongo timestamp - 1 // 64-bit int - ][t]; - }, - - // compare two values of unknown type according to BSON ordering - // semantics. (as an extension, consider 'undefined' to be less than - // any other value.) return negative if a is less, positive if b is - // less, or 0 if equal - _cmp: function (a, b) { - if (a === undefined) - return b === undefined ? 0 : -1; - if (b === undefined) - return 1; - var ta = LocalCollection._f._type(a); - var tb = LocalCollection._f._type(b); - var oa = LocalCollection._f._typeorder(ta); - var ob = LocalCollection._f._typeorder(tb); - if (oa !== ob) - return oa < ob ? -1 : 1; - if (ta !== tb) - // XXX need to implement this if we implement Symbol or integers, or - // Timestamp - throw Error("Missing type coercion logic in _cmp"); - if (ta === 7) { // ObjectID - // Convert to string. - ta = tb = 2; - a = a.toHexString(); - b = b.toHexString(); - } - if (ta === 9) { // Date - // Convert to millis. - ta = tb = 1; - a = a.getTime(); - b = b.getTime(); - } - - if (ta === 1) // double - return a - b; - if (tb === 2) // string - return a < b ? -1 : (a === b ? 0 : 1); - if (ta === 3) { // Object - // this could be much more efficient in the expected case ... - var to_array = function (obj) { - var ret = []; - for (var key in obj) { - ret.push(key); - ret.push(obj[key]); - } - return ret; - }; - return LocalCollection._f._cmp(to_array(a), to_array(b)); - } - if (ta === 4) { // Array - for (var i = 0; ; i++) { - if (i === a.length) - return (i === b.length) ? 0 : -1; - if (i === b.length) - return 1; - var s = LocalCollection._f._cmp(a[i], b[i]); - if (s !== 0) - return s; - } - } - if (ta === 5) { // binary - // Surprisingly, a small binary blob is always less than a large one in - // Mongo. - if (a.length !== b.length) - return a.length - b.length; - for (i = 0; i < a.length; i++) { - if (a[i] < b[i]) - return -1; - if (a[i] > b[i]) - return 1; - } - return 0; - } - if (ta === 8) { // boolean - if (a) return b ? 0 : 1; - return b ? -1 : 0; - } - if (ta === 10) // null - return 0; - if (ta === 11) // regexp - throw Error("Sorting not supported on regular expression"); // XXX - // 13: javascript code - // 14: symbol - // 15: javascript code with scope - // 16: 32-bit integer - // 17: timestamp - // 18: 64-bit integer - // 255: minkey - // 127: maxkey - if (ta === 13) // javascript code - throw Error("Sorting not supported on Javascript code"); // XXX - throw Error("Unknown type to sort"); - } -}; - -const objectOnlyHasDollarKeys = (object) => { - const keys = Object.keys(object); - return keys.length > 0 && keys.every(key => key.charAt(0) === '$'); -}; - -// When performing an upsert, the incoming selector object can be re-used as -// the upsert modifier object, as long as Mongo query and projection -// operators (prefixed with a $ character) are removed from the newly -// created modifier object. This function attempts to strip all $ based Mongo -// operators when creating the upsert modifier object. -// NOTE: There is a known issue here in that some Mongo $ based opeartors -// should not actually be stripped. -// See https://github.com/meteor/meteor/issues/8806. -LocalCollection._removeDollarOperators = (selector) => { - let cleansed = {}; - Object.keys(selector).forEach((key) => { - const value = selector[key]; - if (key.charAt(0) !== '$' && !objectOnlyHasDollarKeys(value)) { - if (value !== null - && value.constructor - && Object.getPrototypeOf(value) === Object.prototype) { - cleansed[key] = LocalCollection._removeDollarOperators(value); - } else { - cleansed[key] = value; - } - } - }); - return cleansed; -}; diff --git a/packages/minimongo/selector_projection.js b/packages/minimongo/selector_projection.js deleted file mode 100644 index 670c529384..0000000000 --- a/packages/minimongo/selector_projection.js +++ /dev/null @@ -1,71 +0,0 @@ -// Knows how to combine a mongo selector and a fields projection to a new fields -// projection taking into account active fields from the passed selector. -// @returns Object - projection object (same as fields option of mongo cursor) -Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { - var self = this; - var selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); - - // Special case for $where operator in the selector - projection should depend - // on all fields of the document. getSelectorPaths returns a list of paths - // selector depends on. If one of the paths is '' (empty string) representing - // the root or the whole document, complete projection should be returned. - if (selectorPaths.includes('')) - return {}; - - return combineImportantPathsIntoProjection(selectorPaths, projection); -}; - -Minimongo._pathsElidingNumericKeys = function (paths) { - var self = this; - return paths.map(function (path) { - return path.split('.').filter(function (part) { return !isNumericKey(part); }).join('.'); - }); -}; - -combineImportantPathsIntoProjection = function (paths, projection) { - var prjDetails = projectionDetails(projection); - var tree = prjDetails.tree; - var mergedProjection = {}; - - // merge the paths to include - tree = pathsToTree(paths, - function (path) { return true; }, - function (node, path, fullPath) { return true; }, - tree); - mergedProjection = treeToPaths(tree); - if (prjDetails.including) { - // both selector and projection are pointing on fields to include - // so we can just return the merged tree - return mergedProjection; - } else { - // selector is pointing at fields to include - // projection is pointing at fields to exclude - // make sure we don't exclude important paths - var mergedExclProjection = {}; - Object.keys(mergedProjection).forEach(function (path) { - var incl = mergedProjection[path]; - if (!incl) - mergedExclProjection[path] = false; - }); - - return mergedExclProjection; - } -}; - -// Returns a set of key paths similar to -// { 'foo.bar': 1, 'a.b.c': 1 } -var treeToPaths = function (tree, prefix) { - prefix = prefix || ''; - var result = {}; - - Object.keys(tree).forEach(function (key) { - var val = tree[key]; - if (val === Object(val)) - Object.assign(result, treeToPaths(val, prefix + key + '.')); - else - result[prefix + key] = val; - }); - - return result; -}; - diff --git a/packages/minimongo/sort.js b/packages/minimongo/sort.js deleted file mode 100644 index 95ac434379..0000000000 --- a/packages/minimongo/sort.js +++ /dev/null @@ -1,422 +0,0 @@ -// Give a sort spec, which can be in any of these forms: -// {"key1": 1, "key2": -1} -// [["key1", "asc"], ["key2", "desc"]] -// ["key1", ["key2", "desc"]] -// -// (.. with the first form being dependent on the key enumeration -// behavior of your javascript VM, which usually does what you mean in -// this case if the key names don't look like integers ..) -// -// return a function that takes two objects, and returns -1 if the -// first object comes first in order, 1 if the second object comes -// first, or 0 if neither object comes before the other. - -Minimongo.Sorter = function (spec, options) { - var self = this; - options = options || {}; - - self._sortSpecParts = []; - self._sortFunction = null; - - var addSpecPart = function (path, ascending) { - if (!path) - throw Error("sort keys must be non-empty"); - if (path.charAt(0) === '$') - throw Error("unsupported sort key: " + path); - self._sortSpecParts.push({ - path: path, - lookup: makeLookupFunction(path, {forSort: true}), - ascending: ascending - }); - }; - - if (spec instanceof Array) { - for (var i = 0; i < spec.length; i++) { - if (typeof spec[i] === "string") { - addSpecPart(spec[i], true); - } else { - addSpecPart(spec[i][0], spec[i][1] !== "desc"); - } - } - } else if (typeof spec === "object") { - Object.keys(spec).forEach(function (key) { - var value = spec[key]; - addSpecPart(key, value >= 0); - }); - } else if (typeof spec === "function") { - self._sortFunction = spec; - } else { - throw Error("Bad sort specification: " + JSON.stringify(spec)); - } - - // If a function is specified for sorting, we skip the rest. - if (self._sortFunction) - return; - - // To implement affectedByModifier, we piggy-back on top of Matcher's - // affectedByModifier code; we create a selector that is affected by the same - // modifiers as this sort order. This is only implemented on the server. - if (self.affectedByModifier) { - var selector = {}; - self._sortSpecParts.forEach(function (spec) { - selector[spec.path] = 1; - }); - self._selectorForAffectedByModifier = new Minimongo.Matcher(selector); - } - - self._keyComparator = composeComparators( - self._sortSpecParts.map(function (spec, i) { - return self._keyFieldComparator(i); - })); - - // If you specify a matcher for this Sorter, _keyFilter may be set to a - // function which selects whether or not a given "sort key" (tuple of values - // for the different sort spec fields) is compatible with the selector. - self._keyFilter = null; - options.matcher && self._useWithMatcher(options.matcher); -}; - -// In addition to these methods, sorter_project.js defines combineIntoProjection -// on the server only. -Object.assign(Minimongo.Sorter.prototype, { - getComparator: function (options) { - var self = this; - - // If sort is specified or have no distances, just use the comparator from - // the source specification (which defaults to "everything is equal". - // issue #3599 - // https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation - // sort effectively overrides $near - if (self._sortSpecParts.length || !options || !options.distances) { - return self._getBaseComparator(); - } - - var distances = options.distances; - - // Return a comparator which compares using $near distances. - return function (a, b) { - if (!distances.has(a._id)) - throw Error("Missing distance for " + a._id); - if (!distances.has(b._id)) - throw Error("Missing distance for " + b._id); - return distances.get(a._id) - distances.get(b._id); - }; - }, - - _getPaths: function () { - var self = this; - return self._sortSpecParts.map(function (part) { return part.path; }); - }, - - // Finds the minimum key from the doc, according to the sort specs. (We say - // "minimum" here but this is with respect to the sort spec, so "descending" - // sort fields mean we're finding the max for that field.) - // - // Note that this is NOT "find the minimum value of the first field, the - // minimum value of the second field, etc"... it's "choose the - // lexicographically minimum value of the key vector, allowing only keys which - // you can find along the same paths". ie, for a doc {a: [{x: 0, y: 5}, {x: - // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and - // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key. - _getMinKeyFromDoc: function (doc) { - var self = this; - var minKey = null; - - self._generateKeysFromDoc(doc, function (key) { - if (!self._keyCompatibleWithSelector(key)) - return; - - if (minKey === null) { - minKey = key; - return; - } - if (self._compareKeys(key, minKey) < 0) { - minKey = key; - } - }); - - // This could happen if our key filter somehow filters out all the keys even - // though somehow the selector matches. - if (minKey === null) - throw Error("sort selector found no keys in doc?"); - return minKey; - }, - - _keyCompatibleWithSelector: function (key) { - var self = this; - return !self._keyFilter || self._keyFilter(key); - }, - - // Iterates over each possible "key" from doc (ie, over each branch), calling - // 'cb' with the key. - _generateKeysFromDoc: function (doc, cb) { - var self = this; - - if (self._sortSpecParts.length === 0) - throw new Error("can't generate keys without a spec"); - - // maps index -> ({'' -> value} or {path -> value}) - var valuesByIndexAndPath = []; - - var pathFromIndices = function (indices) { - return indices.join(',') + ','; - }; - - var knownPaths = null; - - self._sortSpecParts.forEach(function (spec, whichField) { - // Expand any leaf arrays that we find, and ignore those arrays - // themselves. (We never sort based on an array itself.) - var branches = expandArraysInBranches(spec.lookup(doc), true); - - // If there are no values for a key (eg, key goes to an empty array), - // pretend we found one null value. - if (!branches.length) - branches = [{value: null}]; - - var usedPaths = false; - valuesByIndexAndPath[whichField] = {}; - branches.forEach(function (branch) { - if (!branch.arrayIndices) { - // If there are no array indices for a branch, then it must be the - // only branch, because the only thing that produces multiple branches - // is the use of arrays. - if (branches.length > 1) - throw Error("multiple branches but no array used?"); - valuesByIndexAndPath[whichField][''] = branch.value; - return; - } - - usedPaths = true; - var path = pathFromIndices(branch.arrayIndices); - if (valuesByIndexAndPath[whichField].hasOwnProperty(path)) - throw Error("duplicate path: " + path); - valuesByIndexAndPath[whichField][path] = branch.value; - - // If two sort fields both go into arrays, they have to go into the - // exact same arrays and we have to find the same paths. This is - // roughly the same condition that makes MongoDB throw this strange - // error message. eg, the main thing is that if sort spec is {a: 1, - // b:1} then a and b cannot both be arrays. - // - // (In MongoDB it seems to be OK to have {a: 1, 'a.x.y': 1} where 'a' - // and 'a.x.y' are both arrays, but we don't allow this for now. - // #NestedArraySort - // XXX achieve full compatibility here - if (knownPaths && !knownPaths.hasOwnProperty(path)) { - throw Error("cannot index parallel arrays"); - } - }); - - if (knownPaths) { - // Similarly to above, paths must match everywhere, unless this is a - // non-array field. - if (!valuesByIndexAndPath[whichField].hasOwnProperty('') && - Object.keys(knownPaths).length !== Object.keys(valuesByIndexAndPath[whichField]).length) { - throw Error("cannot index parallel arrays!"); - } - } else if (usedPaths) { - knownPaths = {}; - Object.keys(valuesByIndexAndPath[whichField]).forEach(function (path) { - knownPaths[path] = true; - }); - } - }); - - if (!knownPaths) { - // Easy case: no use of arrays. - var soleKey = valuesByIndexAndPath.map(function (values) { - if (!values.hasOwnProperty('')) - throw Error("no value in sole key case?"); - return values['']; - }); - cb(soleKey); - return; - } - - Object.keys(knownPaths).forEach(function (path) { - var key = valuesByIndexAndPath.map(function (values) { - if (values.hasOwnProperty('')) - return values['']; - if (!values.hasOwnProperty(path)) - throw Error("missing path?"); - return values[path]; - }); - cb(key); - }); - }, - - // Takes in two keys: arrays whose lengths match the number of spec - // parts. Returns negative, 0, or positive based on using the sort spec to - // compare fields. - _compareKeys: function (key1, key2) { - var self = this; - if (key1.length !== self._sortSpecParts.length || - key2.length !== self._sortSpecParts.length) { - throw Error("Key has wrong length"); - } - - return self._keyComparator(key1, key2); - }, - - // Given an index 'i', returns a comparator that compares two key arrays based - // on field 'i'. - _keyFieldComparator: function (i) { - var self = this; - var invert = !self._sortSpecParts[i].ascending; - return function (key1, key2) { - var compare = LocalCollection._f._cmp(key1[i], key2[i]); - if (invert) - compare = -compare; - return compare; - }; - }, - - // Returns a comparator that represents the sort specification (but not - // including a possible geoquery distance tie-breaker). - _getBaseComparator: function () { - var self = this; - - if (self._sortFunction) - return self._sortFunction; - - // If we're only sorting on geoquery distance and no specs, just say - // everything is equal. - if (!self._sortSpecParts.length) { - return function (doc1, doc2) { - return 0; - }; - } - - return function (doc1, doc2) { - var key1 = self._getMinKeyFromDoc(doc1); - var key2 = self._getMinKeyFromDoc(doc2); - return self._compareKeys(key1, key2); - }; - }, - - // In MongoDB, if you have documents - // {_id: 'x', a: [1, 10]} and - // {_id: 'y', a: [5, 15]}, - // then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5). - // But C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not - // match the selector, and 5 comes before 10). - // - // The way this works is pretty subtle! For example, if the documents - // are instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and - // {_id: 'y', a: [{x: 5}, {x: 15}]}), - // then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and - // C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}}) - // both follow this rule (y before x). (ie, you do have to apply this - // through $elemMatch.) - // - // So if you pass a matcher to this sorter's constructor, we will attempt to - // skip sort keys that don't match the selector. The logic here is pretty - // subtle and undocumented; we've gotten as close as we can figure out based - // on our understanding of Mongo's behavior. - _useWithMatcher: function (matcher) { - var self = this; - - if (self._keyFilter) - throw Error("called _useWithMatcher twice?"); - - // If we are only sorting by distance, then we're not going to bother to - // build a key filter. - // XXX figure out how geoqueries interact with this stuff - if (!self._sortSpecParts.length) - return; - - var selector = matcher._selector; - - // If the user just passed a literal function to find(), then we can't get a - // key filter from it. - if (selector instanceof Function) - return; - - var constraintsByPath = {}; - self._sortSpecParts.forEach(function (spec, i) { - constraintsByPath[spec.path] = []; - }); - - Object.keys(selector).forEach(function (key) { - var subSelector = selector[key]; - // XXX support $and and $or - - var constraints = constraintsByPath[key]; - if (!constraints) - return; - - // XXX it looks like the real MongoDB implementation isn't "does the - // regexp match" but "does the value fall into a range named by the - // literal prefix of the regexp", ie "foo" in /^foo(bar|baz)+/ But - // "does the regexp match" is a good approximation. - if (subSelector instanceof RegExp) { - // As far as we can tell, using either of the options that both we and - // MongoDB support ('i' and 'm') disables use of the key filter. This - // makes sense: MongoDB mostly appears to be calculating ranges of an - // index to use, which means it only cares about regexps that match - // one range (with a literal prefix), and both 'i' and 'm' prevent the - // literal prefix of the regexp from actually meaning one range. - if (subSelector.ignoreCase || subSelector.multiline) - return; - constraints.push(regexpElementMatcher(subSelector)); - return; - } - - if (isOperatorObject(subSelector)) { - Object.keys(subSelector).forEach(function (operator) { - var operand = subSelector[operator]; - if (['$lt', '$lte', '$gt', '$gte'].includes(operator)) { - // XXX this depends on us knowing that these operators don't use any - // of the arguments to compileElementSelector other than operand. - constraints.push( - ELEMENT_OPERATORS[operator].compileElementSelector(operand)); - } - - // See comments in the RegExp block above. - if (operator === '$regex' && !subSelector.$options) { - constraints.push( - ELEMENT_OPERATORS.$regex.compileElementSelector( - operand, subSelector)); - } - - // XXX support {$exists: true}, $mod, $type, $in, $elemMatch - }); - return; - } - - // OK, it's an equality thing. - constraints.push(equalityElementMatcher(subSelector)); - }); - - // It appears that the first sort field is treated differently from the - // others; we shouldn't create a key filter unless the first sort field is - // restricted, though after that point we can restrict the other sort fields - // or not as we wish. - if (!constraintsByPath[self._sortSpecParts[0].path].length) - return; - - self._keyFilter = function (key) { - return self._sortSpecParts.every(function (specPart, index) { - return constraintsByPath[specPart.path].every(function (f) { - return f(key[index]); - }); - }); - }; - } -}); - -// Given an array of comparators -// (functions (a,b)->(negative or positive or zero)), returns a single -// comparator which uses each comparator in order and returns the first -// non-zero value. -var composeComparators = function (comparatorArray) { - return function (a, b) { - for (var i = 0; i < comparatorArray.length; ++i) { - var compare = comparatorArray[i](a, b); - if (compare !== 0) - return compare; - } - return 0; - }; -}; diff --git a/packages/minimongo/sorter_projection.js b/packages/minimongo/sorter_projection.js deleted file mode 100644 index dfa18af1b5..0000000000 --- a/packages/minimongo/sorter_projection.js +++ /dev/null @@ -1,5 +0,0 @@ -Minimongo.Sorter.prototype.combineIntoProjection = function (projection) { - var self = this; - var specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); - return combineImportantPathsIntoProjection(specPaths, projection); -}; diff --git a/packages/minimongo/validation.js b/packages/minimongo/validation.js deleted file mode 100644 index 7728361f76..0000000000 --- a/packages/minimongo/validation.js +++ /dev/null @@ -1,24 +0,0 @@ -// Make sure field names do not contain Mongo restricted -// characters ('.', '$', '\0'). -// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -const invalidCharMsg = { - '.': "contain '.'", - '$': "start with '$'", - '\0': "contain null bytes", -}; -export function assertIsValidFieldName(key) { - let match; - if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { - throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); - } -}; - -// checks if all field names in an object are valid -export function assertHasValidFieldNames(doc){ - if (doc && typeof doc === "object") { - JSON.stringify(doc, (key, value) => { - assertIsValidFieldName(key); - return value; - }); - } -}; diff --git a/packages/minimongo/wrap_transform.js b/packages/minimongo/wrap_transform.js deleted file mode 100644 index 797fd3241c..0000000000 --- a/packages/minimongo/wrap_transform.js +++ /dev/null @@ -1,46 +0,0 @@ -// Wrap a transform function to return objects that have the _id field -// of the untransformed document. This ensures that subsystems such as -// the observe-sequence package that call `observe` can keep track of -// the documents identities. -// -// - Require that it returns objects -// - If the return value has an _id field, verify that it matches the -// original _id field -// - If the return value doesn't have an _id field, add it back. -LocalCollection.wrapTransform = function (transform) { - if (! transform) - return null; - - // No need to doubly-wrap transforms. - if (transform.__wrappedTransform__) - return transform; - - var wrapped = function (doc) { - if (!doc.hasOwnProperty('_id')) { - // XXX do we ever have a transform on the oplog's collection? because that - // collection has no _id. - throw new Error("can only transform documents with _id"); - } - - var id = doc._id; - // XXX consider making tracker a weak dependency and checking Package.tracker here - var transformed = Tracker.nonreactive(function () { - return transform(doc); - }); - - if (!isPlainObject(transformed)) { - throw new Error("transform must return object"); - } - - if (transformed.hasOwnProperty('_id')) { - if (!EJSON.equals(transformed._id, id)) { - throw new Error("transformed document can't have different _id"); - } - } else { - transformed._id = id; - } - return transformed; - }; - wrapped.__wrappedTransform__ = true; - return wrapped; -}; diff --git a/packages/minimongo/wrap_transform_tests.js b/packages/minimongo/wrap_transform_tests.js deleted file mode 100644 index 3c1f84196a..0000000000 --- a/packages/minimongo/wrap_transform_tests.js +++ /dev/null @@ -1,58 +0,0 @@ -Tinytest.add("minimongo - wrapTransform", function (test) { - var wrap = LocalCollection.wrapTransform; - - // Transforming no function gives falsey. - test.isFalse(wrap(undefined)); - test.isFalse(wrap(null)); - - // It's OK if you don't change the ID. - var validTransform = function (doc) { - delete doc.x; - doc.y = 42; - doc.z = function () { return 43; }; - return doc; - }; - var transformed = wrap(validTransform)({_id: "asdf", x: 54}); - test.equal(Object.keys(transformed), ['_id', 'y', 'z']); - test.equal(transformed.y, 42); - test.equal(transformed.z(), 43); - - // Ensure that ObjectIDs work (even if the _ids in question are not ===-equal) - var oid1 = new MongoID.ObjectID(); - var oid2 = new MongoID.ObjectID(oid1.toHexString()); - test.equal(wrap(function () {return {_id: oid2};})({_id: oid1}), - {_id: oid2}); - - // transform functions must return objects - var invalidObjects = [ - "asdf", new MongoID.ObjectID(), false, null, true, - 27, [123], /adsf/, new Date, function () {}, undefined - ]; - invalidObjects.forEach(function (invalidObject) { - var wrapped = wrap(function () { return invalidObject; }); - test.throws(function () { - wrapped({_id: "asdf"}); - }); - }, /transform must return object/); - - // transform functions may not change _ids - var wrapped = wrap(function (doc) { doc._id = 'x'; return doc; }); - test.throws(function () { - wrapped({_id: 'y'}); - }, /can't have different _id/); - - // transform functions may remove _ids - test.equal({_id: 'a', x: 2}, - wrap(function (d) {delete d._id; return d;})({_id: 'a', x: 2})); - - // test that wrapped transform functions are nonreactive - var unwrapped = function (doc) { - test.isFalse(Tracker.active); - return doc; - }; - var handle = Tracker.autorun(function () { - test.isTrue(Tracker.active); - wrap(unwrapped)({_id: "xxx"}); - }); - handle.stop(); -}); From 7cf9c2116ccf7fc81c79ee4d77069147b38c471c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Tue, 11 Jul 2017 21:58:16 +0200 Subject: [PATCH 04/28] Separated Matcher. --- packages/minimongo/common.js | 30 + packages/minimongo/matcher.js | 1259 +++++++++++++++++++++++++++++ packages/minimongo/minimongo.js | 1336 +------------------------------ packages/minimongo/package.js | 3 + 4 files changed, 1317 insertions(+), 1311 deletions(-) create mode 100644 packages/minimongo/common.js create mode 100644 packages/minimongo/matcher.js diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js new file mode 100644 index 0000000000..b471cda7b3 --- /dev/null +++ b/packages/minimongo/common.js @@ -0,0 +1,30 @@ +import {LocalCollection} from './local_collection.js'; + +export function isIndexable (obj) { + return Array.isArray(obj) || LocalCollection._isPlainObject(obj); +} + +export function isNumericKey (s) { + return /^[0-9]+$/.test(s); +} + +// Returns true if this is an object with at least one key and all keys begin +// with $. Unless inconsistentOK is set, throws if some keys begin with $ and +// others don't. +export function isOperatorObject (valueSelector, inconsistentOK) { + if (!LocalCollection._isPlainObject(valueSelector)) + return false; + + var theseAreOperators = undefined; + Object.keys(valueSelector).forEach(function (selKey) { + var thisIsOperator = selKey.substr(0, 1) === '$'; + if (theseAreOperators === undefined) { + theseAreOperators = thisIsOperator; + } else if (theseAreOperators !== thisIsOperator) { + if (!inconsistentOK) + throw new Error(`Inconsistent operator: ${JSON.stringify(valueSelector)}`); + theseAreOperators = false; + } + }); + return !!theseAreOperators; // {} has no operators +} diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js new file mode 100644 index 0000000000..05746fc776 --- /dev/null +++ b/packages/minimongo/matcher.js @@ -0,0 +1,1259 @@ +import {LocalCollection} from './local_collection.js'; +import { + isIndexable, + isNumericKey, + isOperatorObject, +} from './common.js'; + +// The minimongo selector compiler! + +// Terminology: +// - a 'selector' is the EJSON object representing a selector +// - a 'matcher' is its compiled form (whether a full Minimongo.Matcher +// object or one of the component lambdas that matches parts of it) +// - a 'result object' is an object with a 'result' field and maybe +// distance and arrayIndices. +// - a 'branched value' is an object with a 'value' field and maybe +// 'dontIterate' and 'arrayIndices'. +// - a 'document' is a top-level object that can be stored in a collection. +// - a 'lookup function' is a function that takes in a document and returns +// an array of 'branched values'. +// - a 'branched matcher' maps from an array of branched values to a result +// object. +// - an 'element matcher' maps from a single value to a bool. + +// Main entry point. +// var matcher = new Minimongo.Matcher({a: {$gt: 5}}); +// if (matcher.documentMatches({a: 7})) ... +export class Matcher { + constructor (selector, isUpdate) { + // A set (object mapping string -> *) of all of the document paths looked + // at by the selector. Also includes the empty string if it may look at any + // path (eg, $where). + this._paths = {}; + // Set to true if compilation finds a $near. + this._hasGeoQuery = false; + // Set to true if compilation finds a $where. + this._hasWhere = false; + // Set to false if compilation finds anything other than a simple equality + // or one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used + // with scalars as operands. + this._isSimple = true; + // Set to a dummy document which always matches this Matcher. Or set to null + // if such document is too hard to find. + this._matchingDocument = undefined; + // A clone of the original selector. It may just be a function if the user + // passed in a function; otherwise is definitely an object (eg, IDs are + // translated into {_id: ID} first. Used by canBecomeTrueByModifier and + // Sorter._useWithMatcher. + this._selector = null; + this._docMatcher = this._compileSelector(selector); + // Set to true if selection is done for an update operation + // Default is false + // Used for $near array update (issue #3599) + this._isUpdate = isUpdate; + } + + documentMatches (doc) { + if (doc !== Object(doc)) + throw Error('documentMatches needs a document'); + + return this._docMatcher(doc); + } + + hasGeoQuery () { + return this._hasGeoQuery; + } + + hasWhere () { + return this._hasWhere; + } + + isSimple () { + return this._isSimple; + } + + // Given a selector, return a function that takes one argument, a + // document. It returns a result object. + _compileSelector (selector) { + // you can pass a literal function instead of a selector + if (selector instanceof Function) { + this._isSimple = false; + this._selector = selector; + this._recordPathUsed(''); + return doc => ({result: !!selector.call(doc)}); + } + + // shorthand -- scalars match _id + if (LocalCollection._selectorIsId(selector)) { + this._selector = {_id: selector}; + this._recordPathUsed('_id'); + return doc => ({result: EJSON.equals(doc._id, selector)}); + } + + // protect against dangerous selectors. falsey and {_id: falsey} are both + // likely programmer error, and not what you want, particularly for + // destructive operations. + if (!selector || (selector.hasOwnProperty('_id') && !selector._id)) { + this._isSimple = false; + return nothingMatcher; + } + + // Top level can't be an array or true or binary. + if (Array.isArray(selector) || + EJSON.isBinary(selector) || + typeof selector === 'boolean') + throw new Error(`Invalid selector: ${selector}`); + + this._selector = EJSON.clone(selector); + + return compileDocumentSelector(selector, this, {isRoot: true}); + } + + // Returns a list of key paths the given selector is looking for. It includes + // the empty string if there is a $where. + _getPaths () { + return Object.keys(this._paths); + } + + _recordPathUsed (path) { + this._paths[path] = true; + } +} + +// helpers used by compiled selector code +LocalCollection._f = { + // XXX for _all and _in, consider building 'inquery' at compile time.. + + _type: function (v) { + if (typeof v === "number") + return 1; + if (typeof v === "string") + return 2; + if (typeof v === "boolean") + return 8; + if (Array.isArray(v)) + return 4; + if (v === null) + return 10; + if (v instanceof RegExp) + // note that typeof(/x/) === "object" + return 11; + if (typeof v === "function") + return 13; + if (v instanceof Date) + return 9; + if (EJSON.isBinary(v)) + return 5; + if (v instanceof MongoID.ObjectID) + return 7; + return 3; // object + + // XXX support some/all of these: + // 14, symbol + // 15, javascript code with scope + // 16, 18: 32-bit/64-bit integer + // 17, timestamp + // 255, minkey + // 127, maxkey + }, + + // deep equality test: use for literal document and array matches + _equal: function (a, b) { + return EJSON.equals(a, b, {keyOrderSensitive: true}); + }, + + // maps a type code to a value that can be used to sort values of + // different types + _typeorder: function (t) { + // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types + // XXX what is the correct sort position for Javascript code? + // ('100' in the matrix below) + // XXX minkey/maxkey + return [-1, // (not a type) + 1, // number + 2, // string + 3, // object + 4, // array + 5, // binary + -1, // deprecated + 6, // ObjectID + 7, // bool + 8, // Date + 0, // null + 9, // RegExp + -1, // deprecated + 100, // JS code + 2, // deprecated (symbol) + 100, // JS code + 1, // 32-bit int + 8, // Mongo timestamp + 1 // 64-bit int + ][t]; + }, + + // compare two values of unknown type according to BSON ordering + // semantics. (as an extension, consider 'undefined' to be less than + // any other value.) return negative if a is less, positive if b is + // less, or 0 if equal + _cmp: function (a, b) { + if (a === undefined) + return b === undefined ? 0 : -1; + if (b === undefined) + return 1; + var ta = LocalCollection._f._type(a); + var tb = LocalCollection._f._type(b); + var oa = LocalCollection._f._typeorder(ta); + var ob = LocalCollection._f._typeorder(tb); + if (oa !== ob) + return oa < ob ? -1 : 1; + if (ta !== tb) + // XXX need to implement this if we implement Symbol or integers, or + // Timestamp + throw Error("Missing type coercion logic in _cmp"); + if (ta === 7) { // ObjectID + // Convert to string. + ta = tb = 2; + a = a.toHexString(); + b = b.toHexString(); + } + if (ta === 9) { // Date + // Convert to millis. + ta = tb = 1; + a = a.getTime(); + b = b.getTime(); + } + + if (ta === 1) // double + return a - b; + if (tb === 2) // string + return a < b ? -1 : (a === b ? 0 : 1); + if (ta === 3) { // Object + // this could be much more efficient in the expected case ... + var to_array = function (obj) { + var ret = []; + for (var key in obj) { + ret.push(key); + ret.push(obj[key]); + } + return ret; + }; + return LocalCollection._f._cmp(to_array(a), to_array(b)); + } + if (ta === 4) { // Array + for (var i = 0; ; i++) { + if (i === a.length) + return (i === b.length) ? 0 : -1; + if (i === b.length) + return 1; + var s = LocalCollection._f._cmp(a[i], b[i]); + if (s !== 0) + return s; + } + } + if (ta === 5) { // binary + // Surprisingly, a small binary blob is always less than a large one in + // Mongo. + if (a.length !== b.length) + return a.length - b.length; + for (i = 0; i < a.length; i++) { + if (a[i] < b[i]) + return -1; + if (a[i] > b[i]) + return 1; + } + return 0; + } + if (ta === 8) { // boolean + if (a) return b ? 0 : 1; + return b ? -1 : 0; + } + if (ta === 10) // null + return 0; + if (ta === 11) // regexp + throw Error("Sorting not supported on regular expression"); // XXX + // 13: javascript code + // 14: symbol + // 15: javascript code with scope + // 16: 32-bit integer + // 17: timestamp + // 18: 64-bit integer + // 255: minkey + // 127: maxkey + if (ta === 13) // javascript code + throw Error("Sorting not supported on Javascript code"); // XXX + throw Error("Unknown type to sort"); + } +}; + +// Each element selector contains: +// - compileElementSelector, a function with args: +// - operand - the "right hand side" of the operator +// - valueSelector - the "context" for the operator (so that $regex can find +// $options) +// - matcher - the Matcher this is going into (so that $elemMatch can compile +// more things) +// returning a function mapping a single value to bool. +// - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from +// being called +// - dontIncludeLeafArrays, a bool which causes an argument to be passed to +// expandArraysInBranches if it is called +const ELEMENT_OPERATORS = { + $lt: makeInequality(function (cmpValue) { + return cmpValue < 0; + }), + $gt: makeInequality(function (cmpValue) { + return cmpValue > 0; + }), + $lte: makeInequality(function (cmpValue) { + return cmpValue <= 0; + }), + $gte: makeInequality(function (cmpValue) { + return cmpValue >= 0; + }), + $mod: { + compileElementSelector: function (operand) { + if (!(Array.isArray(operand) && operand.length === 2 + && typeof(operand[0]) === 'number' + && typeof(operand[1]) === 'number')) { + throw Error("argument to $mod must be an array of two numbers"); + } + // XXX could require to be ints or round or something + var divisor = operand[0]; + var remainder = operand[1]; + return function (value) { + return typeof value === 'number' && value % divisor === remainder; + }; + } + }, + $in: { + compileElementSelector: function (operand) { + if (!Array.isArray(operand)) + throw Error("$in needs an array"); + + var elementMatchers = []; + operand.forEach(function (option) { + if (option instanceof RegExp) + elementMatchers.push(regexpElementMatcher(option)); + else if (isOperatorObject(option)) + throw Error("cannot nest $ under $in"); + else + elementMatchers.push(equalityElementMatcher(option)); + }); + + return function (value) { + // Allow {a: {$in: [null]}} to match when 'a' does not exist. + if (value === undefined) + value = null; + return elementMatchers.some(function (e) { + return e(value); + }); + }; + } + }, + $size: { + // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we + // don't want to consider the element [5,5] in the leaf array [[5,5]] as a + // possible value. + dontExpandLeafArrays: true, + compileElementSelector: function (operand) { + if (typeof operand === 'string') { + // Don't ask me why, but by experimentation, this seems to be what Mongo + // does. + operand = 0; + } else if (typeof operand !== 'number') { + throw Error("$size needs a number"); + } + return function (value) { + return Array.isArray(value) && value.length === operand; + }; + } + }, + $type: { + // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should + // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: + // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but + // should *not* include it itself. + dontIncludeLeafArrays: true, + compileElementSelector: function (operand) { + if (typeof operand !== 'number') + throw Error("$type needs a number"); + return function (value) { + return value !== undefined + && LocalCollection._f._type(value) === operand; + }; + } + }, + $bitsAllSet: { + compileElementSelector: function (operand) { + var op = getOperandBitmask(operand, '$bitsAllSet') + return function (value) { + var bitmask = getValueBitmask(value, op.length) + return bitmask && op.every(function (byte, idx) { + return ((bitmask[idx] & byte) == byte) + }) + } + } + }, + $bitsAnySet: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAnySet') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.some(function (byte, idx) { + return ((~bitmask[idx] & byte) !== byte) + }) + } + } + }, + $bitsAllClear: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAllClear') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.every(function (byte, idx) { + return !(bitmask[idx] & byte) + }) + } + } + }, + $bitsAnyClear: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAnyClear') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.some(function (byte, idx) { + return ((bitmask[idx] & byte) !== byte) + }) + } + } + }, + $regex: { + compileElementSelector: function (operand, valueSelector) { + if (!(typeof operand === 'string' || operand instanceof RegExp)) + throw Error("$regex has to be a string or RegExp"); + + var regexp; + if (valueSelector.$options !== undefined) { + // Options passed in $options (even the empty string) always overrides + // options in the RegExp object itself. (See also + // Mongo.Collection._rewriteSelector.) + + // Be clear that we only support the JS-supported options, not extended + // ones (eg, Mongo supports x and s). Ideally we would implement x and s + // by transforming the regexp, but not today... + if (/[^gim]/.test(valueSelector.$options)) + throw new Error("Only the i, m, and g regexp options are supported"); + + var regexSource = operand instanceof RegExp ? operand.source : operand; + regexp = new RegExp(regexSource, valueSelector.$options); + } else if (operand instanceof RegExp) { + regexp = operand; + } else { + regexp = new RegExp(operand); + } + return regexpElementMatcher(regexp); + } + }, + $elemMatch: { + dontExpandLeafArrays: true, + compileElementSelector: function (operand, valueSelector, matcher) { + if (!LocalCollection._isPlainObject(operand)) + throw Error("$elemMatch need an object"); + + var subMatcher, isDocMatcher; + if (isOperatorObject(Object.keys(operand) + .filter(function (key) { return !Object.keys(LOGICAL_OPERATORS).includes(key); }) + .reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { + subMatcher = compileValueSelector(operand, matcher); + isDocMatcher = false; + } else { + // This is NOT the same as compileValueSelector(operand), and not just + // because of the slightly different calling convention. + // {$elemMatch: {x: 3}} means "an element has a field x:3", not + // "consists only of a field x:3". Also, regexps and sub-$ are allowed. + subMatcher = compileDocumentSelector(operand, matcher, + {inElemMatch: true}); + isDocMatcher = true; + } + + return function (value) { + if (!Array.isArray(value)) + return false; + for (var i = 0; i < value.length; ++i) { + var arrayElement = value[i]; + var arg; + if (isDocMatcher) { + // We can only match {$elemMatch: {b: 3}} against objects. + // (We can also match against arrays, if there's numeric indices, + // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) + if (!isIndexable(arrayElement)) + return false; + arg = arrayElement; + } else { + // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches + // {a: [8]} but not {a: [[8]]} + arg = [{value: arrayElement, dontIterate: true}]; + } + // XXX support $near in $elemMatch by propagating $distance? + if (subMatcher(arg).result) + return i; // specially understood to mean "use as arrayIndices" + } + return false; + }; + } + } +}; + +// Operators that appear at the top level of a document selector. +const LOGICAL_OPERATORS = { + $and: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + return andDocumentMatchers(matchers); + }, + + $or: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + + // Special case: if there is only one matcher, use it directly, *preserving* + // any arrayIndices it returns. + if (matchers.length === 1) + return matchers[0]; + + return function (doc) { + var result = matchers.some(function (f) { + return f(doc).result; + }); + // $or does NOT set arrayIndices when it has multiple + // sub-expressions. (Tested against MongoDB.) + return {result: result}; + }; + }, + + $nor: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + return function (doc) { + var result = matchers.every(function (f) { + return !f(doc).result; + }); + // Never set arrayIndices, because we only match if nothing in particular + // 'matched' (and because this is consistent with MongoDB). + return {result: result}; + }; + }, + + $where: function (selectorValue, matcher) { + // Record that *any* path may be used. + matcher._recordPathUsed(''); + matcher._hasWhere = true; + if (!(selectorValue instanceof Function)) { + // XXX MongoDB seems to have more complex logic to decide where or or not + // to add 'return'; not sure exactly what it is. + selectorValue = Function('obj', `return ${selectorValue}`); + } + return function (doc) { + // We make the document available as both `this` and `obj`. + // XXX not sure what we should do if this throws + return {result: selectorValue.call(doc, doc)}; + }; + }, + + // This is just used as a comment in the query (in MongoDB, it also ends up in + // query logs); it has no effect on the actual selection. + $comment: function () { + return function () { + return {result: true}; + }; + } +}; + +// Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a +// document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as +// "match each branched value independently and combine with +// convertElementMatcherToBranchedMatcher". +var VALUE_OPERATORS = { + $eq: function (operand) { + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(operand)); + }, + $not: function (operand, valueSelector, matcher) { + return invertBranchedMatcher(compileValueSelector(operand, matcher)); + }, + $ne: function (operand) { + return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( + equalityElementMatcher(operand))); + }, + $nin: function (operand) { + return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( + ELEMENT_OPERATORS.$in.compileElementSelector(operand))); + }, + $exists: function (operand) { + var exists = convertElementMatcherToBranchedMatcher(function (value) { + return value !== undefined; + }); + return operand ? exists : invertBranchedMatcher(exists); + }, + // $options just provides options for $regex; its logic is inside $regex + $options: function (operand, valueSelector) { + if (!valueSelector.hasOwnProperty('$regex')) + throw Error("$options needs a $regex"); + return everythingMatcher; + }, + // $maxDistance is basically an argument to $near + $maxDistance: function (operand, valueSelector) { + if (!valueSelector.$near) + throw Error("$maxDistance needs a $near"); + return everythingMatcher; + }, + $all: function (operand, valueSelector, matcher) { + if (!Array.isArray(operand)) + throw Error("$all requires array"); + // Not sure why, but this seems to be what MongoDB does. + if (operand.length === 0) + return nothingMatcher; + + var branchedMatchers = []; + operand.forEach(function (criterion) { + // XXX handle $all/$elemMatch combination + if (isOperatorObject(criterion)) + throw Error("no $ expressions in $all"); + // This is always a regexp or equality selector. + branchedMatchers.push(compileValueSelector(criterion, matcher)); + }); + // andBranchedMatchers does NOT require all selectors to return true on the + // SAME branch. + return andBranchedMatchers(branchedMatchers); + }, + $near: function (operand, valueSelector, matcher, isRoot) { + if (!isRoot) + throw Error("$near can't be inside another $ operator"); + matcher._hasGeoQuery = true; + + // There are two kinds of geodata in MongoDB: legacy coordinate pairs and + // GeoJSON. They use different distance metrics, too. GeoJSON queries are + // marked with a $geometry property, though legacy coordinates can be + // matched using $geometry. + + var maxDistance, point, distance; + if (LocalCollection._isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { + // GeoJSON "2dsphere" mode. + maxDistance = operand.$maxDistance; + point = operand.$geometry; + distance = function (value) { + // XXX: for now, we don't calculate the actual distance between, say, + // polygon and circle. If people care about this use-case it will get + // a priority. + if (!value) + return null; + if(!value.type) + return GeoJSON.pointDistance(point, + { type: "Point", coordinates: pointToArray(value) }); + if (value.type === "Point") { + return GeoJSON.pointDistance(point, value); + } else { + return GeoJSON.geometryWithinRadius(value, point, maxDistance) + ? 0 : maxDistance + 1; + } + }; + } else { + maxDistance = valueSelector.$maxDistance; + if (!isIndexable(operand)) + throw Error("$near argument must be coordinate pair or GeoJSON"); + point = pointToArray(operand); + distance = function (value) { + if (!isIndexable(value)) + return null; + return distanceCoordinatePairs(point, value); + }; + } + + return function (branchedValues) { + // There might be multiple points in the document that match the given + // field. Only one of them needs to be within $maxDistance, but we need to + // evaluate all of them and use the nearest one for the implicit sort + // specifier. (That's why we can't just use ELEMENT_OPERATORS here.) + // + // Note: This differs from MongoDB's implementation, where a document will + // actually show up *multiple times* in the result set, with one entry for + // each within-$maxDistance branching point. + branchedValues = expandArraysInBranches(branchedValues); + var result = {result: false}; + branchedValues.every(function (branch) { + // if operation is an update, don't skip branches, just return the first one (#3599) + if (!matcher._isUpdate){ + if (!(typeof branch.value === "object")){ + return true; + } + var curDistance = distance(branch.value); + // Skip branches that aren't real points or are too far away. + if (curDistance === null || curDistance > maxDistance) + return true; + // Skip anything that's a tie. + if (result.distance !== undefined && result.distance <= curDistance) + return true; + } + result.result = true; + result.distance = curDistance; + if (!branch.arrayIndices) + delete result.arrayIndices; + else + result.arrayIndices = branch.arrayIndices; + if (matcher._isUpdate) + return false; + return true; + }); + return result; + }; + } +}; + +// NB: We are cheating and using this function to implement 'AND' for both +// 'document matchers' and 'branched matchers'. They both return result objects +// but the argument is different: for the former it's a whole doc, whereas for +// the latter it's an array of 'branched values'. +function andSomeMatchers (subMatchers) { + if (subMatchers.length === 0) + return everythingMatcher; + if (subMatchers.length === 1) + return subMatchers[0]; + + return function (docOrBranches) { + var ret = {}; + ret.result = subMatchers.every(function (f) { + var subResult = f(docOrBranches); + // Copy a 'distance' number out of the first sub-matcher that has + // one. Yes, this means that if there are multiple $near fields in a + // query, something arbitrary happens; this appears to be consistent with + // Mongo. + if (subResult.result && subResult.distance !== undefined + && ret.distance === undefined) { + ret.distance = subResult.distance; + } + // Similarly, propagate arrayIndices from sub-matchers... but to match + // MongoDB behavior, this time the *last* sub-matcher with arrayIndices + // wins. + if (subResult.result && subResult.arrayIndices) { + ret.arrayIndices = subResult.arrayIndices; + } + return subResult.result; + }); + + // If we didn't actually match, forget any extra metadata we came up with. + if (!ret.result) { + delete ret.distance; + delete ret.arrayIndices; + } + return ret; + }; +} + +const andDocumentMatchers = andSomeMatchers; +const andBranchedMatchers = andSomeMatchers; + +function compileArrayOfDocumentSelectors (selectors, matcher, inElemMatch) { + if (!Array.isArray(selectors) || selectors.length === 0) + throw Error('$and/$or/$nor must be nonempty array'); + return selectors.map(function (subSelector) { + if (!LocalCollection._isPlainObject(subSelector)) + throw Error('$or/$and/$nor entries need to be full objects'); + return compileDocumentSelector( + subSelector, matcher, {inElemMatch: inElemMatch}); + }); +} + +// Takes in a selector that could match a full document (eg, the original +// selector). Returns a function mapping document->result object. +// +// matcher is the Matcher object we are compiling. +// +// If this is the root document selector (ie, not wrapped in $and or the like), +// then isRoot is true. (This is used by $near.) +function compileDocumentSelector (docSelector, matcher, options = {}) { + let docMatchers = []; + Object.keys(docSelector).forEach(function (key) { + let subSelector = docSelector[key]; + if (key.substr(0, 1) === '$') { + // Outer operators are either logical operators (they recurse back into + // this function), or $where. + if (!LOGICAL_OPERATORS.hasOwnProperty(key)) + throw new Error(`Unrecognized logical operator: ${key}`); + matcher._isSimple = false; + docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, + options.inElemMatch)); + } else { + // Record this path, but only if we aren't in an elemMatcher, since in an + // elemMatch this is a path inside an object in an array, not in the doc + // root. + if (!options.inElemMatch) + matcher._recordPathUsed(key); + let lookUpByIndex = makeLookupFunction(key); + let valueMatcher = + compileValueSelector(subSelector, matcher, options.isRoot); + docMatchers.push(function (doc) { + let branchValues = lookUpByIndex(doc); + return valueMatcher(branchValues); + }); + } + }); + + return andDocumentMatchers(docMatchers); +} + +// Takes in a selector that could match a key-indexed value in a document; eg, +// {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to +// indicate equality). Returns a branched matcher: a function mapping +// [branched value]->result object. +function compileValueSelector (valueSelector, matcher, isRoot) { + if (valueSelector instanceof RegExp) { + matcher._isSimple = false; + return convertElementMatcherToBranchedMatcher( + regexpElementMatcher(valueSelector)); + } else if (isOperatorObject(valueSelector)) { + return operatorBranchedMatcher(valueSelector, matcher, isRoot); + } else { + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(valueSelector)); + } +} + +// Given an element matcher (which evaluates a single value), returns a branched +// value (which evaluates the element matcher on all the branches and returns a +// more structured return value possibly including arrayIndices). +function convertElementMatcherToBranchedMatcher (elementMatcher, options) { + options = options || {}; + return function (branches) { + var expanded = branches; + if (!options.dontExpandLeafArrays) { + expanded = expandArraysInBranches( + branches, options.dontIncludeLeafArrays); + } + var ret = {}; + ret.result = expanded.some(function (element) { + var matched = elementMatcher(element.value); + + // Special case for $elemMatch: it means "true, and use this as an array + // index if I didn't already have one". + if (typeof matched === 'number') { + // XXX This code dates from when we only stored a single array index + // (for the outermost array). Should we be also including deeper array + // indices from the $elemMatch match? + if (!element.arrayIndices) + element.arrayIndices = [matched]; + matched = true; + } + + // If some element matched, and it's tagged with array indices, include + // those indices in our result object. + if (matched && element.arrayIndices) + ret.arrayIndices = element.arrayIndices; + + return matched; + }); + return ret; + }; +} + +// Helpers for $near. +function distanceCoordinatePairs (a, b) { + a = pointToArray(a); + b = pointToArray(b); + var x = a[0] - b[0]; + var y = a[1] - b[1]; + if (Number.isNaN(x) || Number.isNaN(y)) + return null; + return Math.sqrt(x * x + y * y); +} + +// Takes something that is not an operator object and returns an element matcher +// for equality with that thing. +function equalityElementMatcher (elementSelector) { + if (isOperatorObject(elementSelector)) + throw Error("Can't create equalityValueSelector for operator object"); + + // Special-case: null and undefined are equal (if you got undefined in there + // somewhere, or if you got it due to some branch being non-existent in the + // weird special case), even though they aren't with EJSON.equals. + if (elementSelector == null) { // undefined or null + return function (value) { + return value == null; // undefined or null + }; + } + + return function (value) { + return LocalCollection._f._equal(elementSelector, value); + }; +} + +function everythingMatcher (docOrBranchedValues) { + return {result: true}; +} + +function expandArraysInBranches (branches, skipTheArrays) { + var branchesOut = []; + branches.forEach(function (branch) { + var thisIsArray = Array.isArray(branch.value); + // We include the branch itself, *UNLESS* we it's an array that we're going + // to iterate and we're told to skip arrays. (That's right, we include some + // arrays even skipTheArrays is true: these are arrays that were found via + // explicit numerical indices.) + if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { + branchesOut.push({ + value: branch.value, + arrayIndices: branch.arrayIndices + }); + } + if (thisIsArray && !branch.dontIterate) { + branch.value.forEach(function (leaf, i) { + branchesOut.push({ + value: leaf, + arrayIndices: (branch.arrayIndices || []).concat(i) + }); + }); + } + }); + return branchesOut; +} + +// Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear. +function getOperandBitmask (operand, selector) { + // numeric bitmask + // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. + // Otherwise, $bitsAllSet will return an error. + if (Number.isInteger(operand) && operand >= 0) { + return new Uint8Array(new Int32Array([operand]).buffer) + } + // bindata bitmask + // You can also use an arbitrarily large BinData instance as a bitmask. + else if (EJSON.isBinary(operand)) { + return new Uint8Array(operand.buffer) + } + // position list + // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. + else if (Array.isArray(operand) && operand.every(function (e) { + return Number.isInteger(e) && e >= 0 + })) { + var buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1) + var view = new Uint8Array(buffer) + operand.forEach(function (x) { + view[x >> 3] |= (1 << (x & 0x7)) + }) + return view + } + // bad operand + else { + throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`) + } +} + +function getValueBitmask (value, length) { + // The field value must be either numerical or a BinData instance. Otherwise, $bits... will not match the current document. + // numerical + if (Number.isSafeInteger(value)) { + // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer + // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. + var buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); + var view = new Uint32Array(buffer, 0, 2) + view[0] = (value % ((1 << 16) * (1 << 16))) | 0 + view[1] = (value / ((1 << 16) * (1 << 16))) | 0 + // sign extension + if (value < 0) { + view = new Uint8Array(buffer, 2) + view.forEach(function (byte, idx) { + view[idx] = 0xff + }) + } + return new Uint8Array(buffer) + } + // bindata + else if (EJSON.isBinary(value)) { + return new Uint8Array(value.buffer) + } + // no match + return false +} + +// Returns a branched matcher that matches iff the given matcher does not. +// Note that this implicitly "deMorganizes" the wrapped function. ie, it +// means that ALL branch values need to fail to match innerBranchedMatcher. +function invertBranchedMatcher (branchedMatcher) { + return function (branchValues) { + var invertMe = branchedMatcher(branchValues); + // We explicitly choose to strip arrayIndices here: it doesn't make sense to + // say "update the array element that does not match something", at least + // in mongo-land. + return {result: !invertMe.result}; + }; +} + +// Helper for $lt/$gt/$lte/$gte. +function makeInequality (cmpValueComparator) { + return { + compileElementSelector: function (operand) { + // Arrays never compare false with non-arrays for any inequality. + // XXX This was behavior we observed in pre-release MongoDB 2.5, but + // it seems to have been reverted. + // See https://jira.mongodb.org/browse/SERVER-11444 + if (Array.isArray(operand)) { + return function () { + return false; + }; + } + + // Special case: consider undefined and null the same (so true with + // $gte/$lte). + if (operand === undefined) + operand = null; + + var operandType = LocalCollection._f._type(operand); + + return function (value) { + if (value === undefined) + value = null; + // Comparisons are never true among things of different type (except + // null vs undefined). + if (LocalCollection._f._type(value) !== operandType) + return false; + return cmpValueComparator(LocalCollection._f._cmp(value, operand)); + }; + } + }; +} + +// makeLookupFunction(key) returns a lookup function. +// +// A lookup function takes in a document and returns an array of matching +// branches. If no arrays are found while looking up the key, this array will +// have exactly one branches (possibly 'undefined', if some segment of the key +// was not found). +// +// If arrays are found in the middle, this can have more than one element, since +// we 'branch'. When we 'branch', if there are more key segments to look up, +// then we only pursue branches that are plain objects (not arrays or scalars). +// This means we can actually end up with no branches! +// +// We do *NOT* branch on arrays that are found at the end (ie, at the last +// dotted member of the key). We just return that array; if you want to +// effectively 'branch' over the array's values, post-process the lookup +// function with expandArraysInBranches. +// +// Each branch is an object with keys: +// - value: the value at the branch +// - dontIterate: an optional bool; if true, it means that 'value' is an array +// that expandArraysInBranches should NOT expand. This specifically happens +// when there is a numeric index in the key, and ensures the +// perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT +// match {a: [[5]]}. +// - arrayIndices: if any array indexing was done during lookup (either due to +// explicit numeric indices or implicit branching), this will be an array of +// the array indices used, from outermost to innermost; it is falsey or +// absent if no array index is used. If an explicit numeric index is used, +// the index will be followed in arrayIndices by the string 'x'. +// +// Note: arrayIndices is used for two purposes. First, it is used to +// implement the '$' modifier feature, which only ever looks at its first +// element. +// +// Second, it is used for sort key generation, which needs to be able to tell +// the difference between different paths. Moreover, it needs to +// differentiate between explicit and implicit branching, which is why +// there's the somewhat hacky 'x' entry: this means that explicit and +// implicit array lookups will have different full arrayIndices paths. (That +// code only requires that different paths have different arrayIndices; it +// doesn't actually 'parse' arrayIndices. As an alternative, arrayIndices +// could contain objects with flags like 'implicit', but I think that only +// makes the code surrounding them more complex.) +// +// (By the way, this field ends up getting passed around a lot without +// cloning, so never mutate any arrayIndices field/var in this package!) +// +// +// At the top level, you may only pass in a plain object or array. +// +// See the test 'minimongo - lookup' for some examples of what lookup functions +// return. +function makeLookupFunction (key, options) { + options = options || {}; + var parts = key.split('.'); + var firstPart = parts.length ? parts[0] : ''; + var firstPartIsNumeric = isNumericKey(firstPart); + var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); + var lookupRest; + if (parts.length > 1) { + lookupRest = makeLookupFunction(parts.slice(1).join('.')); + } + + var omitUnnecessaryFields = function (retVal) { + if (!retVal.dontIterate) + delete retVal.dontIterate; + if (retVal.arrayIndices && !retVal.arrayIndices.length) + delete retVal.arrayIndices; + return retVal; + }; + + // Doc will always be a plain object or an array. + // apply an explicit numeric index, an array. + return function (doc, arrayIndices) { + if (!arrayIndices) + arrayIndices = []; + + if (Array.isArray(doc)) { + // If we're being asked to do an invalid lookup into an array (non-integer + // or out-of-bounds), return no results (which is different from returning + // a single undefined result, in that `null` equality checks won't match). + if (!(firstPartIsNumeric && firstPart < doc.length)) + return []; + + // Remember that we used this array index. Include an 'x' to indicate that + // the previous index came from being considered as an explicit array + // index (not branching). + arrayIndices = arrayIndices.concat(+firstPart, 'x'); + } + + // Do our first lookup. + var firstLevel = doc[firstPart]; + + // If there is no deeper to dig, return what we found. + // + // If what we found is an array, most value selectors will choose to treat + // the elements of the array as matchable values in their own right, but + // that's done outside of the lookup function. (Exceptions to this are $size + // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a: + // [[1, 2]]}.) + // + // That said, if we just did an *explicit* array lookup (on doc) to find + // firstLevel, and firstLevel is an array too, we do NOT want value + // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}. + // So in that case, we mark the return value as 'don't iterate'. + if (!lookupRest) { + return [omitUnnecessaryFields({ + value: firstLevel, + dontIterate: Array.isArray(doc) && Array.isArray(firstLevel), + arrayIndices: arrayIndices})]; + } + + // We need to dig deeper. But if we can't, because what we've found is not + // an array or plain object, we're done. If we just did a numeric index into + // an array, we return nothing here (this is a change in Mongo 2.5 from + // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise, + // return a single `undefined` (which can, for example, match via equality + // with `null`). + if (!isIndexable(firstLevel)) { + if (Array.isArray(doc)) + return []; + return [omitUnnecessaryFields({value: undefined, + arrayIndices: arrayIndices})]; + } + + var result = []; + var appendToResult = function (more) { + Array.prototype.push.apply(result, more); + }; + + // Dig deeper: look up the rest of the parts on whatever we've found. + // (lookupRest is smart enough to not try to do invalid lookups into + // firstLevel if it's an array.) + appendToResult(lookupRest(firstLevel, arrayIndices)); + + // If we found an array, then in *addition* to potentially treating the next + // part as a literal integer lookup, we should also 'branch': try to look up + // the rest of the parts on each array element in parallel. + // + // In this case, we *only* dig deeper into array elements that are plain + // objects. (Recall that we only got this far if we have further to dig.) + // This makes sense: we certainly don't dig deeper into non-indexable + // objects. And it would be weird to dig into an array: it's simpler to have + // a rule that explicit integer indexes only apply to an outer array, not to + // an array you find after a branching search. + // + // In the special case of a numeric part in a *sort selector* (not a query + // selector), we skip the branching: we ONLY allow the numeric part to mean + // 'look up this index' in that case, not 'also look up this index in all + // the elements of the array'. + if (Array.isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { + firstLevel.forEach(function (branch, arrayIndex) { + if (LocalCollection._isPlainObject(branch)) { + appendToResult(lookupRest( + branch, + arrayIndices.concat(arrayIndex))); + } + }); + } + + return result; + }; +} + +MinimongoTest.makeLookupFunction = makeLookupFunction; + +function nothingMatcher (docOrBranchedValues) { + return {result: false}; +} + +// Takes an operator object (an object with $ keys) and returns a branched +// matcher for it. +function operatorBranchedMatcher (valueSelector, matcher, isRoot) { + // Each valueSelector works separately on the various branches. So one + // operator can match one branch and another can match another branch. This + // is OK. + + var operatorMatchers = []; + Object.keys(valueSelector).forEach(function (operator) { + var operand = valueSelector[operator]; + var simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && + typeof operand === 'number'; + var simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); + var simpleInclusion = ['$in', '$nin'].includes(operator) && + Array.isArray(operand) && !operand.some(function (x) { return x === Object(x); }); + + if (! (simpleRange || simpleInclusion || simpleEquality)) { + matcher._isSimple = false; + } + + if (VALUE_OPERATORS.hasOwnProperty(operator)) { + operatorMatchers.push( + VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); + } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { + var options = ELEMENT_OPERATORS[operator]; + operatorMatchers.push( + convertElementMatcherToBranchedMatcher( + options.compileElementSelector( + operand, valueSelector, matcher), + options)); + } else { + throw new Error("Unrecognized operator: " + operator); + } + }); + + return andBranchedMatchers(operatorMatchers); +} + +// Makes sure we get 2 elements array and assume the first one to be x and +// the second one to y no matter what user passes. +// In case user passes { lon: x, lat: y } returns [x, y] +function pointToArray (point) { + return Array.isArray(point) ? point.slice() : [point.x, point.y]; +} + +// Takes a RegExp object and returns an element matcher. +function regexpElementMatcher (regexp) { + return function (value) { + if (value instanceof RegExp) { + return value.toString() === regexp.toString(); + } + // Regexps only work against strings. + if (typeof value !== 'string') + return false; + + // Reset regexp's state to avoid inconsistent matching for objects with the + // same value on consecutive calls of regexp.test. This happens only if the + // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for + // which we should *not* change the lastIndex but MongoDB doesn't support + // either of these flags. + regexp.lastIndex = 0; + + return regexp.test(value); + }; +} diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 027de20b11..1470a57366 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1,3 +1,10 @@ +import {Matcher} from './matcher'; +import { + isIndexable, + isNumericKey, + isOperatorObject, +} from './common.js'; + // Make sure field names do not contain Mongo restricted // characters ('.', '$', '\0'). // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names @@ -60,7 +67,7 @@ LocalCollection = function (name) { self.paused = false; }; -Minimongo = {}; +Minimongo = {Matcher}; // Object exported only for unit testing. // Use it to export private functions to test in Tinytest. @@ -1187,7 +1194,7 @@ LocalCollection.wrapTransform = function (transform) { return transform(doc); }); - if (!isPlainObject(transformed)) { + if (!LocalCollection._isPlainObject(transformed)) { throw new Error("transform must return object"); } @@ -1204,1308 +1211,14 @@ LocalCollection.wrapTransform = function (transform) { return wrapped; }; -// Like _.isArray, but doesn't regard polyfilled Uint8Arrays on old browsers as -// arrays. -// XXX maybe this should be EJSON.isArray -isArray = function (x) { - return Array.isArray(x) && !EJSON.isBinary(x); -}; - // XXX maybe this should be EJSON.isObject, though EJSON doesn't know about // RegExp // XXX note that _type(undefined) === 3!!!! -isPlainObject = LocalCollection._isPlainObject = function (x) { +LocalCollection._isPlainObject = function (x) { return x && LocalCollection._f._type(x) === 3; }; -isIndexable = function (x) { - return isArray(x) || isPlainObject(x); -}; - -// Returns true if this is an object with at least one key and all keys begin -// with $. Unless inconsistentOK is set, throws if some keys begin with $ and -// others don't. -isOperatorObject = function (valueSelector, inconsistentOK) { - if (!isPlainObject(valueSelector)) - return false; - - var theseAreOperators = undefined; - Object.keys(valueSelector).forEach(function (selKey) { - var thisIsOperator = selKey.substr(0, 1) === '$'; - if (theseAreOperators === undefined) { - theseAreOperators = thisIsOperator; - } else if (theseAreOperators !== thisIsOperator) { - if (!inconsistentOK) - throw new Error("Inconsistent operator: " + - JSON.stringify(valueSelector)); - theseAreOperators = false; - } - }); - return !!theseAreOperators; // {} has no operators -}; - - -// string can be converted to integer -isNumericKey = function (s) { - return /^[0-9]+$/.test(s); -}; - -// The minimongo selector compiler! - -// Terminology: -// - a "selector" is the EJSON object representing a selector -// - a "matcher" is its compiled form (whether a full Minimongo.Matcher -// object or one of the component lambdas that matches parts of it) -// - a "result object" is an object with a "result" field and maybe -// distance and arrayIndices. -// - a "branched value" is an object with a "value" field and maybe -// "dontIterate" and "arrayIndices". -// - a "document" is a top-level object that can be stored in a collection. -// - a "lookup function" is a function that takes in a document and returns -// an array of "branched values". -// - a "branched matcher" maps from an array of branched values to a result -// object. -// - an "element matcher" maps from a single value to a bool. - -// Main entry point. -// var matcher = new Minimongo.Matcher({a: {$gt: 5}}); -// if (matcher.documentMatches({a: 7})) ... -Minimongo.Matcher = function (selector, isUpdate = false) { - var self = this; - // A set (object mapping string -> *) of all of the document paths looked - // at by the selector. Also includes the empty string if it may look at any - // path (eg, $where). - self._paths = {}; - // Set to true if compilation finds a $near. - self._hasGeoQuery = false; - // Set to true if compilation finds a $where. - self._hasWhere = false; - // Set to false if compilation finds anything other than a simple equality or - // one or more of '$gt', '$gte', '$lt', '$lte', '$ne', '$in', '$nin' used with - // scalars as operands. - self._isSimple = true; - // Set to a dummy document which always matches this Matcher. Or set to null - // if such document is too hard to find. - self._matchingDocument = undefined; - // A clone of the original selector. It may just be a function if the user - // passed in a function; otherwise is definitely an object (eg, IDs are - // translated into {_id: ID} first. Used by canBecomeTrueByModifier and - // Sorter._useWithMatcher. - self._selector = null; - self._docMatcher = self._compileSelector(selector); - // Set to true if selection is done for an update operation - // Default is false - // Used for $near array update (issue #3599) - self._isUpdate = isUpdate; -}; - -Object.assign(Minimongo.Matcher.prototype, { - documentMatches: function (doc) { - if (!doc || typeof doc !== "object") { - throw Error("documentMatches needs a document"); - } - return this._docMatcher(doc); - }, - hasGeoQuery: function () { - return this._hasGeoQuery; - }, - hasWhere: function () { - return this._hasWhere; - }, - isSimple: function () { - return this._isSimple; - }, - - // Given a selector, return a function that takes one argument, a - // document. It returns a result object. - _compileSelector: function (selector) { - var self = this; - // you can pass a literal function instead of a selector - if (selector instanceof Function) { - self._isSimple = false; - self._selector = selector; - self._recordPathUsed(''); - return function (doc) { - return {result: !!selector.call(doc)}; - }; - } - - // shorthand -- scalars match _id - if (LocalCollection._selectorIsId(selector)) { - self._selector = {_id: selector}; - self._recordPathUsed('_id'); - return function (doc) { - return {result: EJSON.equals(doc._id, selector)}; - }; - } - - // protect against dangerous selectors. falsey and {_id: falsey} are both - // likely programmer error, and not what you want, particularly for - // destructive operations. - if (!selector || (('_id' in selector) && !selector._id)) { - self._isSimple = false; - return nothingMatcher; - } - - // Top level can't be an array or true or binary. - if (typeof(selector) === 'boolean' || isArray(selector) || - EJSON.isBinary(selector)) - throw new Error("Invalid selector: " + selector); - - self._selector = EJSON.clone(selector); - return compileDocumentSelector(selector, self, {isRoot: true}); - }, - _recordPathUsed: function (path) { - this._paths[path] = true; - }, - // Returns a list of key paths the given selector is looking for. It includes - // the empty string if there is a $where. - _getPaths: function () { - return Object.keys(this._paths); - } -}); - - -// Takes in a selector that could match a full document (eg, the original -// selector). Returns a function mapping document->result object. -// -// matcher is the Matcher object we are compiling. -// -// If this is the root document selector (ie, not wrapped in $and or the like), -// then isRoot is true. (This is used by $near.) -var compileDocumentSelector = function (docSelector, matcher, options) { - options = options || {}; - var docMatchers = []; - Object.keys(docSelector).forEach(function (key) { - var subSelector = docSelector[key]; - if (key.substr(0, 1) === '$') { - // Outer operators are either logical operators (they recurse back into - // this function), or $where. - if (!LOGICAL_OPERATORS.hasOwnProperty(key)) - throw new Error("Unrecognized logical operator: " + key); - matcher._isSimple = false; - docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, - options.inElemMatch)); - } else { - // Record this path, but only if we aren't in an elemMatcher, since in an - // elemMatch this is a path inside an object in an array, not in the doc - // root. - if (!options.inElemMatch) - matcher._recordPathUsed(key); - var lookUpByIndex = makeLookupFunction(key); - var valueMatcher = - compileValueSelector(subSelector, matcher, options.isRoot); - docMatchers.push(function (doc) { - var branchValues = lookUpByIndex(doc); - return valueMatcher(branchValues); - }); - } - }); - - return andDocumentMatchers(docMatchers); -}; - -// Takes in a selector that could match a key-indexed value in a document; eg, -// {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to -// indicate equality). Returns a branched matcher: a function mapping -// [branched value]->result object. -var compileValueSelector = function (valueSelector, matcher, isRoot) { - if (valueSelector instanceof RegExp) { - matcher._isSimple = false; - return convertElementMatcherToBranchedMatcher( - regexpElementMatcher(valueSelector)); - } else if (isOperatorObject(valueSelector)) { - return operatorBranchedMatcher(valueSelector, matcher, isRoot); - } else { - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(valueSelector)); - } -}; - -// Given an element matcher (which evaluates a single value), returns a branched -// value (which evaluates the element matcher on all the branches and returns a -// more structured return value possibly including arrayIndices). -var convertElementMatcherToBranchedMatcher = function ( - elementMatcher, options) { - options = options || {}; - return function (branches) { - var expanded = branches; - if (!options.dontExpandLeafArrays) { - expanded = expandArraysInBranches( - branches, options.dontIncludeLeafArrays); - } - var ret = {}; - ret.result = expanded.some(function (element) { - var matched = elementMatcher(element.value); - - // Special case for $elemMatch: it means "true, and use this as an array - // index if I didn't already have one". - if (typeof matched === 'number') { - // XXX This code dates from when we only stored a single array index - // (for the outermost array). Should we be also including deeper array - // indices from the $elemMatch match? - if (!element.arrayIndices) - element.arrayIndices = [matched]; - matched = true; - } - - // If some element matched, and it's tagged with array indices, include - // those indices in our result object. - if (matched && element.arrayIndices) - ret.arrayIndices = element.arrayIndices; - - return matched; - }); - return ret; - }; -}; - -// Takes a RegExp object and returns an element matcher. -regexpElementMatcher = function (regexp) { - return function (value) { - if (value instanceof RegExp) { - return value.toString() === regexp.toString(); - } - // Regexps only work against strings. - if (typeof value !== 'string') - return false; - - // Reset regexp's state to avoid inconsistent matching for objects with the - // same value on consecutive calls of regexp.test. This happens only if the - // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for - // which we should *not* change the lastIndex but MongoDB doesn't support - // either of these flags. - regexp.lastIndex = 0; - - return regexp.test(value); - }; -}; - -// Takes something that is not an operator object and returns an element matcher -// for equality with that thing. -equalityElementMatcher = function (elementSelector) { - if (isOperatorObject(elementSelector)) - throw Error("Can't create equalityValueSelector for operator object"); - - // Special-case: null and undefined are equal (if you got undefined in there - // somewhere, or if you got it due to some branch being non-existent in the - // weird special case), even though they aren't with EJSON.equals. - if (elementSelector == null) { // undefined or null - return function (value) { - return value == null; // undefined or null - }; - } - - return function (value) { - return LocalCollection._f._equal(elementSelector, value); - }; -}; - -// Takes an operator object (an object with $ keys) and returns a branched -// matcher for it. -var operatorBranchedMatcher = function (valueSelector, matcher, isRoot) { - // Each valueSelector works separately on the various branches. So one - // operator can match one branch and another can match another branch. This - // is OK. - - var operatorMatchers = []; - Object.keys(valueSelector).forEach(function (operator) { - var operand = valueSelector[operator]; - var simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && - typeof operand === 'number'; - var simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); - var simpleInclusion = ['$in', '$nin'].includes(operator) && - Array.isArray(operand) && !operand.some(function (x) { return x === Object(x); }); - - if (! (simpleRange || simpleInclusion || simpleEquality)) { - matcher._isSimple = false; - } - - if (VALUE_OPERATORS.hasOwnProperty(operator)) { - operatorMatchers.push( - VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); - } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { - var options = ELEMENT_OPERATORS[operator]; - operatorMatchers.push( - convertElementMatcherToBranchedMatcher( - options.compileElementSelector( - operand, valueSelector, matcher), - options)); - } else { - throw new Error("Unrecognized operator: " + operator); - } - }); - - return andBranchedMatchers(operatorMatchers); -}; - -var compileArrayOfDocumentSelectors = function ( - selectors, matcher, inElemMatch) { - if (!isArray(selectors) || selectors.length === 0) - throw Error("$and/$or/$nor must be nonempty array"); - return selectors.map(function (subSelector) { - if (!isPlainObject(subSelector)) - throw Error("$or/$and/$nor entries need to be full objects"); - return compileDocumentSelector( - subSelector, matcher, {inElemMatch: inElemMatch}); - }); -}; - -// Operators that appear at the top level of a document selector. -var LOGICAL_OPERATORS = { - $and: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - return andDocumentMatchers(matchers); - }, - - $or: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - - // Special case: if there is only one matcher, use it directly, *preserving* - // any arrayIndices it returns. - if (matchers.length === 1) - return matchers[0]; - - return function (doc) { - var result = matchers.some(function (f) { - return f(doc).result; - }); - // $or does NOT set arrayIndices when it has multiple - // sub-expressions. (Tested against MongoDB.) - return {result: result}; - }; - }, - - $nor: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - return function (doc) { - var result = matchers.every(function (f) { - return !f(doc).result; - }); - // Never set arrayIndices, because we only match if nothing in particular - // "matched" (and because this is consistent with MongoDB). - return {result: result}; - }; - }, - - $where: function (selectorValue, matcher) { - // Record that *any* path may be used. - matcher._recordPathUsed(''); - matcher._hasWhere = true; - if (!(selectorValue instanceof Function)) { - // XXX MongoDB seems to have more complex logic to decide where or or not - // to add "return"; not sure exactly what it is. - selectorValue = Function("obj", "return " + selectorValue); - } - return function (doc) { - // We make the document available as both `this` and `obj`. - // XXX not sure what we should do if this throws - return {result: selectorValue.call(doc, doc)}; - }; - }, - - // This is just used as a comment in the query (in MongoDB, it also ends up in - // query logs); it has no effect on the actual selection. - $comment: function () { - return function () { - return {result: true}; - }; - } -}; - -// Returns a branched matcher that matches iff the given matcher does not. -// Note that this implicitly "deMorganizes" the wrapped function. ie, it -// means that ALL branch values need to fail to match innerBranchedMatcher. -var invertBranchedMatcher = function (branchedMatcher) { - return function (branchValues) { - var invertMe = branchedMatcher(branchValues); - // We explicitly choose to strip arrayIndices here: it doesn't make sense to - // say "update the array element that does not match something", at least - // in mongo-land. - return {result: !invertMe.result}; - }; -}; - -// Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a -// document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as -// "match each branched value independently and combine with -// convertElementMatcherToBranchedMatcher". -var VALUE_OPERATORS = { - $eq: function (operand) { - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(operand)); - }, - $not: function (operand, valueSelector, matcher) { - return invertBranchedMatcher(compileValueSelector(operand, matcher)); - }, - $ne: function (operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( - equalityElementMatcher(operand))); - }, - $nin: function (operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( - ELEMENT_OPERATORS.$in.compileElementSelector(operand))); - }, - $exists: function (operand) { - var exists = convertElementMatcherToBranchedMatcher(function (value) { - return value !== undefined; - }); - return operand ? exists : invertBranchedMatcher(exists); - }, - // $options just provides options for $regex; its logic is inside $regex - $options: function (operand, valueSelector) { - if (!valueSelector.hasOwnProperty('$regex')) - throw Error("$options needs a $regex"); - return everythingMatcher; - }, - // $maxDistance is basically an argument to $near - $maxDistance: function (operand, valueSelector) { - if (!valueSelector.$near) - throw Error("$maxDistance needs a $near"); - return everythingMatcher; - }, - $all: function (operand, valueSelector, matcher) { - if (!isArray(operand)) - throw Error("$all requires array"); - // Not sure why, but this seems to be what MongoDB does. - if (operand.length === 0) - return nothingMatcher; - - var branchedMatchers = []; - operand.forEach(function (criterion) { - // XXX handle $all/$elemMatch combination - if (isOperatorObject(criterion)) - throw Error("no $ expressions in $all"); - // This is always a regexp or equality selector. - branchedMatchers.push(compileValueSelector(criterion, matcher)); - }); - // andBranchedMatchers does NOT require all selectors to return true on the - // SAME branch. - return andBranchedMatchers(branchedMatchers); - }, - $near: function (operand, valueSelector, matcher, isRoot) { - if (!isRoot) - throw Error("$near can't be inside another $ operator"); - matcher._hasGeoQuery = true; - - // There are two kinds of geodata in MongoDB: legacy coordinate pairs and - // GeoJSON. They use different distance metrics, too. GeoJSON queries are - // marked with a $geometry property, though legacy coordinates can be - // matched using $geometry. - - var maxDistance, point, distance; - if (isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { - // GeoJSON "2dsphere" mode. - maxDistance = operand.$maxDistance; - point = operand.$geometry; - distance = function (value) { - // XXX: for now, we don't calculate the actual distance between, say, - // polygon and circle. If people care about this use-case it will get - // a priority. - if (!value) - return null; - if(!value.type) - return GeoJSON.pointDistance(point, - { type: "Point", coordinates: pointToArray(value) }); - if (value.type === "Point") { - return GeoJSON.pointDistance(point, value); - } else { - return GeoJSON.geometryWithinRadius(value, point, maxDistance) - ? 0 : maxDistance + 1; - } - }; - } else { - maxDistance = valueSelector.$maxDistance; - if (!isArray(operand) && !isPlainObject(operand)) - throw Error("$near argument must be coordinate pair or GeoJSON"); - point = pointToArray(operand); - distance = function (value) { - if (!isArray(value) && !isPlainObject(value)) - return null; - return distanceCoordinatePairs(point, value); - }; - } - - return function (branchedValues) { - // There might be multiple points in the document that match the given - // field. Only one of them needs to be within $maxDistance, but we need to - // evaluate all of them and use the nearest one for the implicit sort - // specifier. (That's why we can't just use ELEMENT_OPERATORS here.) - // - // Note: This differs from MongoDB's implementation, where a document will - // actually show up *multiple times* in the result set, with one entry for - // each within-$maxDistance branching point. - branchedValues = expandArraysInBranches(branchedValues); - var result = {result: false}; - branchedValues.every(function (branch) { - // if operation is an update, don't skip branches, just return the first one (#3599) - if (!matcher._isUpdate){ - if (!(typeof branch.value === "object")){ - return true; - } - var curDistance = distance(branch.value); - // Skip branches that aren't real points or are too far away. - if (curDistance === null || curDistance > maxDistance) - return true; - // Skip anything that's a tie. - if (result.distance !== undefined && result.distance <= curDistance) - return true; - } - result.result = true; - result.distance = curDistance; - if (!branch.arrayIndices) - delete result.arrayIndices; - else - result.arrayIndices = branch.arrayIndices; - if (matcher._isUpdate) - return false; - return true; - }); - return result; - }; - } -}; - -// Helpers for $near. -var distanceCoordinatePairs = function (a, b) { - a = pointToArray(a); - b = pointToArray(b); - var x = a[0] - b[0]; - var y = a[1] - b[1]; - if (Number.isNaN(x) || Number.isNaN(y)) - return null; - return Math.sqrt(x * x + y * y); -}; -// Makes sure we get 2 elements array and assume the first one to be x and -// the second one to y no matter what user passes. -// In case user passes { lon: x, lat: y } returns [x, y] -var pointToArray = function (point) { - return Array.isArray(point) ? point.slice() : [point.x, point.y]; -}; - -// Helper for $lt/$gt/$lte/$gte. -var makeInequality = function (cmpValueComparator) { - return { - compileElementSelector: function (operand) { - // Arrays never compare false with non-arrays for any inequality. - // XXX This was behavior we observed in pre-release MongoDB 2.5, but - // it seems to have been reverted. - // See https://jira.mongodb.org/browse/SERVER-11444 - if (isArray(operand)) { - return function () { - return false; - }; - } - - // Special case: consider undefined and null the same (so true with - // $gte/$lte). - if (operand === undefined) - operand = null; - - var operandType = LocalCollection._f._type(operand); - - return function (value) { - if (value === undefined) - value = null; - // Comparisons are never true among things of different type (except - // null vs undefined). - if (LocalCollection._f._type(value) !== operandType) - return false; - return cmpValueComparator(LocalCollection._f._cmp(value, operand)); - }; - } - }; -}; - -// Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear. -var getOperandBitmask = function(operand, selector) { - // numeric bitmask - // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. - // Otherwise, $bitsAllSet will return an error. - if (Number.isInteger(operand) && operand >= 0) { - return new Uint8Array(new Int32Array([operand]).buffer) - } - // bindata bitmask - // You can also use an arbitrarily large BinData instance as a bitmask. - else if (EJSON.isBinary(operand)) { - return new Uint8Array(operand.buffer) - } - // position list - // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. - else if (isArray(operand) && operand.every(function (e) { - return Number.isInteger(e) && e >= 0 - })) { - var buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1) - var view = new Uint8Array(buffer) - operand.forEach(function (x) { - view[x >> 3] |= (1 << (x & 0x7)) - }) - return view - } - // bad operand - else { - throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`) - } -} -var getValueBitmask = function (value, length) { - // The field value must be either numerical or a BinData instance. Otherwise, $bits... will not match the current document. - // numerical - if (Number.isSafeInteger(value)) { - // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer - // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. - var buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); - var view = new Uint32Array(buffer, 0, 2) - view[0] = (value % ((1 << 16) * (1 << 16))) | 0 - view[1] = (value / ((1 << 16) * (1 << 16))) | 0 - // sign extension - if (value < 0) { - view = new Uint8Array(buffer, 2) - view.forEach(function (byte, idx) { - view[idx] = 0xff - }) - } - return new Uint8Array(buffer) - } - // bindata - else if (EJSON.isBinary(value)) { - return new Uint8Array(value.buffer) - } - // no match - return false -} - -// Each element selector contains: -// - compileElementSelector, a function with args: -// - operand - the "right hand side" of the operator -// - valueSelector - the "context" for the operator (so that $regex can find -// $options) -// - matcher - the Matcher this is going into (so that $elemMatch can compile -// more things) -// returning a function mapping a single value to bool. -// - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from -// being called -// - dontIncludeLeafArrays, a bool which causes an argument to be passed to -// expandArraysInBranches if it is called -ELEMENT_OPERATORS = { - $lt: makeInequality(function (cmpValue) { - return cmpValue < 0; - }), - $gt: makeInequality(function (cmpValue) { - return cmpValue > 0; - }), - $lte: makeInequality(function (cmpValue) { - return cmpValue <= 0; - }), - $gte: makeInequality(function (cmpValue) { - return cmpValue >= 0; - }), - $mod: { - compileElementSelector: function (operand) { - if (!(isArray(operand) && operand.length === 2 - && typeof(operand[0]) === 'number' - && typeof(operand[1]) === 'number')) { - throw Error("argument to $mod must be an array of two numbers"); - } - // XXX could require to be ints or round or something - var divisor = operand[0]; - var remainder = operand[1]; - return function (value) { - return typeof value === 'number' && value % divisor === remainder; - }; - } - }, - $in: { - compileElementSelector: function (operand) { - if (!isArray(operand)) - throw Error("$in needs an array"); - - var elementMatchers = []; - operand.forEach(function (option) { - if (option instanceof RegExp) - elementMatchers.push(regexpElementMatcher(option)); - else if (isOperatorObject(option)) - throw Error("cannot nest $ under $in"); - else - elementMatchers.push(equalityElementMatcher(option)); - }); - - return function (value) { - // Allow {a: {$in: [null]}} to match when 'a' does not exist. - if (value === undefined) - value = null; - return elementMatchers.some(function (e) { - return e(value); - }); - }; - } - }, - $size: { - // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we - // don't want to consider the element [5,5] in the leaf array [[5,5]] as a - // possible value. - dontExpandLeafArrays: true, - compileElementSelector: function (operand) { - if (typeof operand === 'string') { - // Don't ask me why, but by experimentation, this seems to be what Mongo - // does. - operand = 0; - } else if (typeof operand !== 'number') { - throw Error("$size needs a number"); - } - return function (value) { - return isArray(value) && value.length === operand; - }; - } - }, - $type: { - // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should - // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: - // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but - // should *not* include it itself. - dontIncludeLeafArrays: true, - compileElementSelector: function (operand) { - if (typeof operand !== 'number') - throw Error("$type needs a number"); - return function (value) { - return value !== undefined - && LocalCollection._f._type(value) === operand; - }; - } - }, - $bitsAllSet: { - compileElementSelector: function (operand) { - var op = getOperandBitmask(operand, '$bitsAllSet') - return function (value) { - var bitmask = getValueBitmask(value, op.length) - return bitmask && op.every(function (byte, idx) { - return ((bitmask[idx] & byte) == byte) - }) - } - } - }, - $bitsAnySet: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAnySet') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.some(function (byte, idx) { - return ((~bitmask[idx] & byte) !== byte) - }) - } - } - }, - $bitsAllClear: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAllClear') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.every(function (byte, idx) { - return !(bitmask[idx] & byte) - }) - } - } - }, - $bitsAnyClear: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAnyClear') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.some(function (byte, idx) { - return ((bitmask[idx] & byte) !== byte) - }) - } - } - }, - $regex: { - compileElementSelector: function (operand, valueSelector) { - if (!(typeof operand === 'string' || operand instanceof RegExp)) - throw Error("$regex has to be a string or RegExp"); - - var regexp; - if (valueSelector.$options !== undefined) { - // Options passed in $options (even the empty string) always overrides - // options in the RegExp object itself. (See also - // Mongo.Collection._rewriteSelector.) - - // Be clear that we only support the JS-supported options, not extended - // ones (eg, Mongo supports x and s). Ideally we would implement x and s - // by transforming the regexp, but not today... - if (/[^gim]/.test(valueSelector.$options)) - throw new Error("Only the i, m, and g regexp options are supported"); - - var regexSource = operand instanceof RegExp ? operand.source : operand; - regexp = new RegExp(regexSource, valueSelector.$options); - } else if (operand instanceof RegExp) { - regexp = operand; - } else { - regexp = new RegExp(operand); - } - return regexpElementMatcher(regexp); - } - }, - $elemMatch: { - dontExpandLeafArrays: true, - compileElementSelector: function (operand, valueSelector, matcher) { - if (!isPlainObject(operand)) - throw Error("$elemMatch need an object"); - - var subMatcher, isDocMatcher; - if (isOperatorObject(Object.keys(operand) - .filter(function (key) { return !Object.keys(LOGICAL_OPERATORS).includes(key); }) - .reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { - subMatcher = compileValueSelector(operand, matcher); - isDocMatcher = false; - } else { - // This is NOT the same as compileValueSelector(operand), and not just - // because of the slightly different calling convention. - // {$elemMatch: {x: 3}} means "an element has a field x:3", not - // "consists only of a field x:3". Also, regexps and sub-$ are allowed. - subMatcher = compileDocumentSelector(operand, matcher, - {inElemMatch: true}); - isDocMatcher = true; - } - - return function (value) { - if (!isArray(value)) - return false; - for (var i = 0; i < value.length; ++i) { - var arrayElement = value[i]; - var arg; - if (isDocMatcher) { - // We can only match {$elemMatch: {b: 3}} against objects. - // (We can also match against arrays, if there's numeric indices, - // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) - if (!isPlainObject(arrayElement) && !isArray(arrayElement)) - return false; - arg = arrayElement; - } else { - // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches - // {a: [8]} but not {a: [[8]]} - arg = [{value: arrayElement, dontIterate: true}]; - } - // XXX support $near in $elemMatch by propagating $distance? - if (subMatcher(arg).result) - return i; // specially understood to mean "use as arrayIndices" - } - return false; - }; - } - } -}; - -// makeLookupFunction(key) returns a lookup function. -// -// A lookup function takes in a document and returns an array of matching -// branches. If no arrays are found while looking up the key, this array will -// have exactly one branches (possibly 'undefined', if some segment of the key -// was not found). -// -// If arrays are found in the middle, this can have more than one element, since -// we "branch". When we "branch", if there are more key segments to look up, -// then we only pursue branches that are plain objects (not arrays or scalars). -// This means we can actually end up with no branches! -// -// We do *NOT* branch on arrays that are found at the end (ie, at the last -// dotted member of the key). We just return that array; if you want to -// effectively "branch" over the array's values, post-process the lookup -// function with expandArraysInBranches. -// -// Each branch is an object with keys: -// - value: the value at the branch -// - dontIterate: an optional bool; if true, it means that 'value' is an array -// that expandArraysInBranches should NOT expand. This specifically happens -// when there is a numeric index in the key, and ensures the -// perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT -// match {a: [[5]]}. -// - arrayIndices: if any array indexing was done during lookup (either due to -// explicit numeric indices or implicit branching), this will be an array of -// the array indices used, from outermost to innermost; it is falsey or -// absent if no array index is used. If an explicit numeric index is used, -// the index will be followed in arrayIndices by the string 'x'. -// -// Note: arrayIndices is used for two purposes. First, it is used to -// implement the '$' modifier feature, which only ever looks at its first -// element. -// -// Second, it is used for sort key generation, which needs to be able to tell -// the difference between different paths. Moreover, it needs to -// differentiate between explicit and implicit branching, which is why -// there's the somewhat hacky 'x' entry: this means that explicit and -// implicit array lookups will have different full arrayIndices paths. (That -// code only requires that different paths have different arrayIndices; it -// doesn't actually "parse" arrayIndices. As an alternative, arrayIndices -// could contain objects with flags like "implicit", but I think that only -// makes the code surrounding them more complex.) -// -// (By the way, this field ends up getting passed around a lot without -// cloning, so never mutate any arrayIndices field/var in this package!) -// -// -// At the top level, you may only pass in a plain object or array. -// -// See the test 'minimongo - lookup' for some examples of what lookup functions -// return. -makeLookupFunction = function (key, options) { - options = options || {}; - var parts = key.split('.'); - var firstPart = parts.length ? parts[0] : ''; - var firstPartIsNumeric = isNumericKey(firstPart); - var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); - var lookupRest; - if (parts.length > 1) { - lookupRest = makeLookupFunction(parts.slice(1).join('.')); - } - - var omitUnnecessaryFields = function (retVal) { - if (!retVal.dontIterate) - delete retVal.dontIterate; - if (retVal.arrayIndices && !retVal.arrayIndices.length) - delete retVal.arrayIndices; - return retVal; - }; - - // Doc will always be a plain object or an array. - // apply an explicit numeric index, an array. - return function (doc, arrayIndices) { - if (!arrayIndices) - arrayIndices = []; - - if (isArray(doc)) { - // If we're being asked to do an invalid lookup into an array (non-integer - // or out-of-bounds), return no results (which is different from returning - // a single undefined result, in that `null` equality checks won't match). - if (!(firstPartIsNumeric && firstPart < doc.length)) - return []; - - // Remember that we used this array index. Include an 'x' to indicate that - // the previous index came from being considered as an explicit array - // index (not branching). - arrayIndices = arrayIndices.concat(+firstPart, 'x'); - } - - // Do our first lookup. - var firstLevel = doc[firstPart]; - - // If there is no deeper to dig, return what we found. - // - // If what we found is an array, most value selectors will choose to treat - // the elements of the array as matchable values in their own right, but - // that's done outside of the lookup function. (Exceptions to this are $size - // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a: - // [[1, 2]]}.) - // - // That said, if we just did an *explicit* array lookup (on doc) to find - // firstLevel, and firstLevel is an array too, we do NOT want value - // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}. - // So in that case, we mark the return value as "don't iterate". - if (!lookupRest) { - return [omitUnnecessaryFields({ - value: firstLevel, - dontIterate: isArray(doc) && isArray(firstLevel), - arrayIndices: arrayIndices})]; - } - - // We need to dig deeper. But if we can't, because what we've found is not - // an array or plain object, we're done. If we just did a numeric index into - // an array, we return nothing here (this is a change in Mongo 2.5 from - // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise, - // return a single `undefined` (which can, for example, match via equality - // with `null`). - if (!isIndexable(firstLevel)) { - if (isArray(doc)) - return []; - return [omitUnnecessaryFields({value: undefined, - arrayIndices: arrayIndices})]; - } - - var result = []; - var appendToResult = function (more) { - Array.prototype.push.apply(result, more); - }; - - // Dig deeper: look up the rest of the parts on whatever we've found. - // (lookupRest is smart enough to not try to do invalid lookups into - // firstLevel if it's an array.) - appendToResult(lookupRest(firstLevel, arrayIndices)); - - // If we found an array, then in *addition* to potentially treating the next - // part as a literal integer lookup, we should also "branch": try to look up - // the rest of the parts on each array element in parallel. - // - // In this case, we *only* dig deeper into array elements that are plain - // objects. (Recall that we only got this far if we have further to dig.) - // This makes sense: we certainly don't dig deeper into non-indexable - // objects. And it would be weird to dig into an array: it's simpler to have - // a rule that explicit integer indexes only apply to an outer array, not to - // an array you find after a branching search. - // - // In the special case of a numeric part in a *sort selector* (not a query - // selector), we skip the branching: we ONLY allow the numeric part to mean - // "look up this index" in that case, not "also look up this index in all - // the elements of the array". - if (isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { - firstLevel.forEach(function (branch, arrayIndex) { - if (isPlainObject(branch)) { - appendToResult(lookupRest( - branch, - arrayIndices.concat(arrayIndex))); - } - }); - } - - return result; - }; -}; -MinimongoTest.makeLookupFunction = makeLookupFunction; - -expandArraysInBranches = function (branches, skipTheArrays) { - var branchesOut = []; - branches.forEach(function (branch) { - var thisIsArray = isArray(branch.value); - // We include the branch itself, *UNLESS* we it's an array that we're going - // to iterate and we're told to skip arrays. (That's right, we include some - // arrays even skipTheArrays is true: these are arrays that were found via - // explicit numerical indices.) - if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { - branchesOut.push({ - value: branch.value, - arrayIndices: branch.arrayIndices - }); - } - if (thisIsArray && !branch.dontIterate) { - branch.value.forEach(function (leaf, i) { - branchesOut.push({ - value: leaf, - arrayIndices: (branch.arrayIndices || []).concat(i) - }); - }); - } - }); - return branchesOut; -}; - -var nothingMatcher = function (docOrBranchedValues) { - return {result: false}; -}; - -var everythingMatcher = function (docOrBranchedValues) { - return {result: true}; -}; - - -// NB: We are cheating and using this function to implement "AND" for both -// "document matchers" and "branched matchers". They both return result objects -// but the argument is different: for the former it's a whole doc, whereas for -// the latter it's an array of "branched values". -var andSomeMatchers = function (subMatchers) { - if (subMatchers.length === 0) - return everythingMatcher; - if (subMatchers.length === 1) - return subMatchers[0]; - - return function (docOrBranches) { - var ret = {}; - ret.result = subMatchers.every(function (f) { - var subResult = f(docOrBranches); - // Copy a 'distance' number out of the first sub-matcher that has - // one. Yes, this means that if there are multiple $near fields in a - // query, something arbitrary happens; this appears to be consistent with - // Mongo. - if (subResult.result && subResult.distance !== undefined - && ret.distance === undefined) { - ret.distance = subResult.distance; - } - // Similarly, propagate arrayIndices from sub-matchers... but to match - // MongoDB behavior, this time the *last* sub-matcher with arrayIndices - // wins. - if (subResult.result && subResult.arrayIndices) { - ret.arrayIndices = subResult.arrayIndices; - } - return subResult.result; - }); - - // If we didn't actually match, forget any extra metadata we came up with. - if (!ret.result) { - delete ret.distance; - delete ret.arrayIndices; - } - return ret; - }; -}; - -var andDocumentMatchers = andSomeMatchers; -var andBranchedMatchers = andSomeMatchers; - - -// helpers used by compiled selector code -LocalCollection._f = { - // XXX for _all and _in, consider building 'inquery' at compile time.. - - _type: function (v) { - if (typeof v === "number") - return 1; - if (typeof v === "string") - return 2; - if (typeof v === "boolean") - return 8; - if (isArray(v)) - return 4; - if (v === null) - return 10; - if (v instanceof RegExp) - // note that typeof(/x/) === "object" - return 11; - if (typeof v === "function") - return 13; - if (v instanceof Date) - return 9; - if (EJSON.isBinary(v)) - return 5; - if (v instanceof MongoID.ObjectID) - return 7; - return 3; // object - - // XXX support some/all of these: - // 14, symbol - // 15, javascript code with scope - // 16, 18: 32-bit/64-bit integer - // 17, timestamp - // 255, minkey - // 127, maxkey - }, - - // deep equality test: use for literal document and array matches - _equal: function (a, b) { - return EJSON.equals(a, b, {keyOrderSensitive: true}); - }, - - // maps a type code to a value that can be used to sort values of - // different types - _typeorder: function (t) { - // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types - // XXX what is the correct sort position for Javascript code? - // ('100' in the matrix below) - // XXX minkey/maxkey - return [-1, // (not a type) - 1, // number - 2, // string - 3, // object - 4, // array - 5, // binary - -1, // deprecated - 6, // ObjectID - 7, // bool - 8, // Date - 0, // null - 9, // RegExp - -1, // deprecated - 100, // JS code - 2, // deprecated (symbol) - 100, // JS code - 1, // 32-bit int - 8, // Mongo timestamp - 1 // 64-bit int - ][t]; - }, - - // compare two values of unknown type according to BSON ordering - // semantics. (as an extension, consider 'undefined' to be less than - // any other value.) return negative if a is less, positive if b is - // less, or 0 if equal - _cmp: function (a, b) { - if (a === undefined) - return b === undefined ? 0 : -1; - if (b === undefined) - return 1; - var ta = LocalCollection._f._type(a); - var tb = LocalCollection._f._type(b); - var oa = LocalCollection._f._typeorder(ta); - var ob = LocalCollection._f._typeorder(tb); - if (oa !== ob) - return oa < ob ? -1 : 1; - if (ta !== tb) - // XXX need to implement this if we implement Symbol or integers, or - // Timestamp - throw Error("Missing type coercion logic in _cmp"); - if (ta === 7) { // ObjectID - // Convert to string. - ta = tb = 2; - a = a.toHexString(); - b = b.toHexString(); - } - if (ta === 9) { // Date - // Convert to millis. - ta = tb = 1; - a = a.getTime(); - b = b.getTime(); - } - - if (ta === 1) // double - return a - b; - if (tb === 2) // string - return a < b ? -1 : (a === b ? 0 : 1); - if (ta === 3) { // Object - // this could be much more efficient in the expected case ... - var to_array = function (obj) { - var ret = []; - for (var key in obj) { - ret.push(key); - ret.push(obj[key]); - } - return ret; - }; - return LocalCollection._f._cmp(to_array(a), to_array(b)); - } - if (ta === 4) { // Array - for (var i = 0; ; i++) { - if (i === a.length) - return (i === b.length) ? 0 : -1; - if (i === b.length) - return 1; - var s = LocalCollection._f._cmp(a[i], b[i]); - if (s !== 0) - return s; - } - } - if (ta === 5) { // binary - // Surprisingly, a small binary blob is always less than a large one in - // Mongo. - if (a.length !== b.length) - return a.length - b.length; - for (i = 0; i < a.length; i++) { - if (a[i] < b[i]) - return -1; - if (a[i] > b[i]) - return 1; - } - return 0; - } - if (ta === 8) { // boolean - if (a) return b ? 0 : 1; - return b ? -1 : 0; - } - if (ta === 10) // null - return 0; - if (ta === 11) // regexp - throw Error("Sorting not supported on regular expression"); // XXX - // 13: javascript code - // 14: symbol - // 15: javascript code with scope - // 16: 32-bit integer - // 17: timestamp - // 18: 64-bit integer - // 255: minkey - // 127: maxkey - if (ta === 13) // javascript code - throw Error("Sorting not supported on Javascript code"); // XXX - throw Error("Unknown type to sort"); - } -}; - -const objectOnlyHasDollarKeys = (object) => { +function objectOnlyHasDollarKeys (object) { const keys = Object.keys(object); return keys.length > 0 && keys.every(key => key.charAt(0) === '$'); }; @@ -2947,7 +1660,7 @@ Object.assign(Minimongo.Sorter.prototype, { // (functions (a,b)->(negative or positive or zero)), returns a single // comparator which uses each comparator in order and returns the first // non-zero value. -var composeComparators = function (comparatorArray) { +function composeComparators (comparatorArray) { return function (a, b) { for (var i = 0; i < comparatorArray.length; ++i) { var compare = comparatorArray[i](a, b); @@ -2956,7 +1669,8 @@ var composeComparators = function (comparatorArray) { } return 0; }; -}; +} + // Knows how to compile a fields projection to a predicate function. // @returns - Function: a closure that filters out an object according to the // fields projection rules: @@ -3012,7 +1726,7 @@ LocalCollection._compileProjection = function (fields) { // - tree - Object - tree representation of keys involved in projection // (exception for '_id' as it is a special case handled separately) // - including - Boolean - "take only certain fields" type of projection -projectionDetails = function (fields) { +function projectionDetails (fields) { // Find the non-_id keys (_id is handled specially because it is included unless // explicitly excluded). Sort the keys, so that our code to detect overlaps // like 'foo' and 'foo.bar' can assume that 'foo' comes first. @@ -3073,7 +1787,7 @@ projectionDetails = function (fields) { tree: projectionRulesTree, including: including }; -}; +} // paths - Array: list of mongo style paths // newLeafFn - Function: of form function(path) should return a scalar value to @@ -3084,7 +1798,7 @@ projectionDetails = function (fields) { // conflict resolution. // initial tree - Optional Object: starting tree. // @returns - Object: tree represented as a set of nested objects -pathsToTree = function (paths, newLeafFn, conflictFn, tree) { +function pathsToTree (paths, newLeafFn, conflictFn, tree) { tree = tree || {}; paths.forEach(function (keyPath) { var treePos = tree; @@ -3117,7 +1831,7 @@ pathsToTree = function (paths, newLeafFn, conflictFn, tree) { }); return tree; -}; +} LocalCollection._checkSupportedProjection = function (fields) { if (fields !== Object(fields) || Array.isArray(fields)) @@ -3148,7 +1862,7 @@ LocalCollection._checkSupportedProjection = function (fields) { // out when to set the fields in $setOnInsert, if present. LocalCollection._modify = function (doc, mod, options) { options = options || {}; - if (!isPlainObject(mod)) + if (!LocalCollection._isPlainObject(mod)) throw MinimongoError("Modifier must be an object"); // Make sure the caller can't mutate our data structures. @@ -3238,7 +1952,7 @@ LocalCollection._modify = function (doc, mod, options) { // // if options.arrayIndices is set, use its first element for the (first) '$' in // the path. -var findModTarget = function (doc, keyparts, options) { +function findModTarget (doc, keyparts, options) { options = options || {}; var usedArrayIndex = false; for (var i = 0; i < keyparts.length; i++) { @@ -3304,9 +2018,9 @@ var findModTarget = function (doc, keyparts, options) { } // notreached -}; +} -var NO_CREATE_MODIFIERS = { +const NO_CREATE_MODIFIERS = { $unset: true, $pop: true, $rename: true, @@ -3314,7 +2028,7 @@ var NO_CREATE_MODIFIERS = { $pullAll: true }; -var MODIFIERS = { +const MODIFIERS = { $currentDate: function (target, field, arg) { if (typeof arg === "object" && arg.hasOwnProperty("$type")) { if (arg.$type !== "date") { @@ -3634,6 +2348,7 @@ var MODIFIERS = { throw MinimongoError("$bit is not supported", { field }); } }; + // ordered: bool. // old_results and new_results: collections of documents. // if ordered, they are arrays. @@ -3647,8 +2362,7 @@ LocalCollection._diffQueryUnorderedChanges = function (oldResults, newResults, o }; -LocalCollection._diffQueryOrderedChanges = - function (oldResults, newResults, observer, options) { +LocalCollection._diffQueryOrderedChanges = function (oldResults, newResults, observer, options) { return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options); }; diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 012714325a..9d784276c9 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -24,6 +24,9 @@ Package.onUse(api => { api.addFiles('minimongo.js'); api.addFiles('minimongo_server.js', 'server'); + + // api.mainModule('client_main.js', 'client'); + // api.mainModule('server_main.js', 'server'); }); Package.onTest(api => { From 083e3a5f7cb6f01b0636b79c9ea7fae20c53d72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Tue, 11 Jul 2017 22:29:15 +0200 Subject: [PATCH 05/28] Separated LocalCollection. --- packages/minimongo/cursor.js | 3 + packages/minimongo/local_collection.js | 1745 +++++++++++++++++++++ packages/minimongo/minimongo.js | 1987 ++---------------------- packages/minimongo/observe_handle.js | 2 + 4 files changed, 1872 insertions(+), 1865 deletions(-) create mode 100644 packages/minimongo/cursor.js create mode 100644 packages/minimongo/local_collection.js create mode 100644 packages/minimongo/observe_handle.js diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js new file mode 100644 index 0000000000..62a2836c34 --- /dev/null +++ b/packages/minimongo/cursor.js @@ -0,0 +1,3 @@ +export class Cursor { + +} diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js new file mode 100644 index 0000000000..5ebcee16a7 --- /dev/null +++ b/packages/minimongo/local_collection.js @@ -0,0 +1,1745 @@ +import {Cursor} from './cursor.js'; +import {ObserveHandle} from './observe_handle.js'; + +export class LocalCollection { + static Cursor = Cursor; + + static ObserveHandle = ObserveHandle; + + // XXX maybe move these into another ObserveHelpers package or something + + // _CachingChangeObserver is an object which receives observeChanges callbacks + // and keeps a cache of the current cursor state up to date in self.docs. Users + // of this class should read the docs field but not modify it. You should pass + // the "applyChange" field as the callbacks to the underlying observeChanges + // call. Optionally, you can specify your own observeChanges callbacks which are + // invoked immediately before the docs field is updated; this object is made + // available as `this` to those callbacks. + static _CachingChangeObserver = class _CachingChangeObserver { + constructor (options) { + var self = this; + options = options || {}; + + var orderedFromCallbacks = options.callbacks && + LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); + if (options.hasOwnProperty('ordered')) { + self.ordered = options.ordered; + if (options.callbacks && options.ordered !== orderedFromCallbacks) + throw Error("ordered option doesn't match callbacks"); + } else if (options.callbacks) { + self.ordered = orderedFromCallbacks; + } else { + throw Error("must provide ordered or callbacks"); + } + var callbacks = options.callbacks || {}; + + if (self.ordered) { + self.docs = new OrderedDict(MongoID.idStringify); + self.applyChange = { + addedBefore: function (id, fields, before) { + var doc = EJSON.clone(fields); + doc._id = id; + callbacks.addedBefore && callbacks.addedBefore.call( + self, id, fields, before); + // This line triggers if we provide added with movedBefore. + callbacks.added && callbacks.added.call(self, id, fields); + // XXX could `before` be a falsy ID? Technically + // idStringify seems to allow for them -- though + // OrderedDict won't call stringify on a falsy arg. + self.docs.putBefore(id, doc, before || null); + }, + movedBefore: function (id, before) { + var doc = self.docs.get(id); + callbacks.movedBefore && callbacks.movedBefore.call(self, id, before); + self.docs.moveBefore(id, before || null); + } + }; + } else { + self.docs = new LocalCollection._IdMap; + self.applyChange = { + added: function (id, fields) { + var doc = EJSON.clone(fields); + callbacks.added && callbacks.added.call(self, id, fields); + doc._id = id; + self.docs.set(id, doc); + } + }; + } + + // The methods in _IdMap and OrderedDict used by these callbacks are + // identical. + self.applyChange.changed = function (id, fields) { + var doc = self.docs.get(id); + if (!doc) + throw new Error("Unknown id for changed: " + id); + callbacks.changed && callbacks.changed.call( + self, id, EJSON.clone(fields)); + DiffSequence.applyChanges(doc, fields); + }; + self.applyChange.removed = function (id) { + callbacks.removed && callbacks.removed.call(self, id); + self.docs.remove(id); + }; + } + }; + + static _IdMap = class _IdMap extends IdMap { + constructor () { + super(MongoID.idStringify, MongoID.idParse); + } + }; + + // Wrap a transform function to return objects that have the _id field + // of the untransformed document. This ensures that subsystems such as + // the observe-sequence package that call `observe` can keep track of + // the documents identities. + // + // - Require that it returns objects + // - If the return value has an _id field, verify that it matches the + // original _id field + // - If the return value doesn't have an _id field, add it back. + static wrapTransform = transform => { + if (! transform) + return null; + + // No need to doubly-wrap transforms. + if (transform.__wrappedTransform__) + return transform; + + var wrapped = function (doc) { + if (!doc.hasOwnProperty('_id')) { + // XXX do we ever have a transform on the oplog's collection? because that + // collection has no _id. + throw new Error("can only transform documents with _id"); + } + + var id = doc._id; + // XXX consider making tracker a weak dependency and checking Package.tracker here + var transformed = Tracker.nonreactive(function () { + return transform(doc); + }); + + if (!LocalCollection._isPlainObject(transformed)) { + throw new Error("transform must return object"); + } + + if (transformed.hasOwnProperty('_id')) { + if (!EJSON.equals(transformed._id, id)) { + throw new Error("transformed document can't have different _id"); + } + } else { + transformed._id = id; + } + return transformed; + }; + wrapped.__wrappedTransform__ = true; + return wrapped; + }; + + // XXX the sorted-query logic below is laughably inefficient. we'll + // need to come up with a better datastructure for this. + // + // XXX the logic for observing with a skip or a limit is even more + // laughably inefficient. we recompute the whole results every time! + + // This binary search puts a value between any equal values, and the first + // lesser value. + static _binarySearch = (cmp, array, value) => { + var first = 0, rangeLength = array.length; + + while (rangeLength > 0) { + var halfRange = Math.floor(rangeLength/2); + if (cmp(value, array[first + halfRange]) >= 0) { + first += halfRange + 1; + rangeLength -= halfRange + 1; + } else { + rangeLength = halfRange; + } + } + return first; + }; + + static _checkSupportedProjection = fields => { + if (fields !== Object(fields) || Array.isArray(fields)) + throw MinimongoError("fields option must be an object"); + + Object.keys(fields).forEach(function (keyPath) { + var val = fields[keyPath]; + if (keyPath.split('.').includes('$')) + throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); + if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) + throw MinimongoError("Minimongo doesn't support operators in projections yet."); + if (![1, 0, true, false].includes(val)) + throw MinimongoError("Projection values should be one of 1, 0, true, or false"); + }); + }; + + // Knows how to compile a fields projection to a predicate function. + // @returns - Function: a closure that filters out an object according to the + // fields projection rules: + // @param obj - Object: MongoDB-styled document + // @returns - Object: a document with the fields filtered out + // according to projection rules. Doesn't retain subfields + // of passed argument. + static _compileProjection = fields => { + LocalCollection._checkSupportedProjection(fields); + + var _idProjection = fields._id === undefined ? true : fields._id; + var details = projectionDetails(fields); + + // returns transformed doc according to ruleTree + var transform = function (doc, ruleTree) { + // Special case for "sets" + if (Array.isArray(doc)) + return doc.map(function (subdoc) { return transform(subdoc, ruleTree); }); + + var res = details.including ? {} : EJSON.clone(doc); + Object.keys(ruleTree).forEach(function (key) { + var rule = ruleTree[key]; + if (!doc.hasOwnProperty(key)) + return; + if (rule === Object(rule)) { + // For sub-objects/subsets we branch + if (doc[key] === Object(doc[key])) + res[key] = transform(doc[key], rule); + // Otherwise we don't even touch this subfield + } else if (details.including) + res[key] = EJSON.clone(doc[key]); + else + delete res[key]; + }); + + return res; + }; + + return function (obj) { + var res = transform(obj, details.tree); + + if (_idProjection && obj.hasOwnProperty('_id')) + res._id = obj._id; + if (!_idProjection && res.hasOwnProperty('_id')) + delete res._id; + return res; + }; + }; + + static _diffObjects = (left, right, callbacks) => { + return DiffSequence.diffObjects(left, right, callbacks); + }; + + // ordered: bool. + // old_results and new_results: collections of documents. + // if ordered, they are arrays. + // if unordered, they are IdMaps + static _diffQueryChanges = (ordered, oldResults, newResults, observer, options) => { + return DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options); + }; + + static _diffQueryOrderedChanges = (oldResults, newResults, observer, options) => { + return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options); + }; + + static _diffQueryUnorderedChanges = (oldResults, newResults, observer, options) => { + return DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options); + }; + + static _findInOrderedResults = (query, doc) => { + if (!query.ordered) + throw new Error("Can't call _findInOrderedResults on unordered query"); + for (var i = 0; i < query.results.length; i++) + if (query.results[i] === doc) + return i; + throw Error("object missing from query"); + }; + + // If this is a selector which explicitly constrains the match by ID to a finite + // number of documents, returns a list of their IDs. Otherwise returns + // null. Note that the selector may have other restrictions so it may not even + // match those document! We care about $in and $and since those are generated + // access-controlled update and remove. + static _idsMatchedBySelector = selector => { + // Is the selector just an ID? + if (LocalCollection._selectorIsId(selector)) + return [selector]; + if (!selector) + return null; + + // Do we have an _id clause? + if (selector.hasOwnProperty('_id')) { + // Is the _id clause just an ID? + if (LocalCollection._selectorIsId(selector._id)) + return [selector._id]; + // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? + if (selector._id && selector._id.$in + && Array.isArray(selector._id.$in) + && selector._id.$in.length + && selector._id.$in.every(LocalCollection._selectorIsId)) { + return selector._id.$in; + } + return null; + } + + // If this is a top-level $and, and any of the clauses constrain their + // documents, then the whole selector is constrained by any one clause's + // constraint. (Well, by their intersection, but that seems unlikely.) + if (selector.$and && Array.isArray(selector.$and)) { + for (var i = 0; i < selector.$and.length; ++i) { + var subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); + if (subIds) + return subIds; + } + } + + return null; + }; + + static _insertInResults = (query, doc) => { + var fields = EJSON.clone(doc); + delete fields._id; + if (query.ordered) { + if (!query.sorter) { + query.addedBefore(doc._id, query.projectionFn(fields), null); + query.results.push(doc); + } else { + var i = LocalCollection._insertInSortedList( + query.sorter.getComparator({distances: query.distances}), + query.results, doc); + var next = query.results[i+1]; + if (next) + next = next._id; + else + next = null; + query.addedBefore(doc._id, query.projectionFn(fields), next); + } + query.added(doc._id, query.projectionFn(fields)); + } else { + query.added(doc._id, query.projectionFn(fields)); + query.results.set(doc._id, doc); + } + }; + + static _insertInSortedList = (cmp, array, value) => { + if (array.length === 0) { + array.push(value); + return 0; + } + + var idx = LocalCollection._binarySearch(cmp, array, value); + array.splice(idx, 0, value); + return idx; + }; + + // XXX maybe this should be EJSON.isObject, though EJSON doesn't know about + // RegExp + // XXX note that _type(undefined) === 3!!!! + static _isPlainObject = x => { + return x && LocalCollection._f._type(x) === 3; + }; + + // XXX need a strategy for passing the binding of $ into this + // function, from the compiled selector + // + // maybe just {key.up.to.just.before.dollarsign: array_index} + // + // XXX atomicity: if one modification fails, do we roll back the whole + // change? + // + // options: + // - isInsert is set when _modify is being called to compute the document to + // insert as part of an upsert operation. We use this primarily to figure + // out when to set the fields in $setOnInsert, if present. + static _modify = (doc, mod, options) => { + options = options || {}; + if (!LocalCollection._isPlainObject(mod)) + throw MinimongoError("Modifier must be an object"); + + // Make sure the caller can't mutate our data structures. + mod = EJSON.clone(mod); + + var isModifier = isOperatorObject(mod); + + var newDoc; + + if (!isModifier) { + if (mod._id && !EJSON.equals(doc._id, mod._id)) + throw MinimongoError("Cannot change the _id of a document"); + + // replace the whole document + assertHasValidFieldNames(mod); + newDoc = mod; + } else { + // apply modifiers to the doc. + newDoc = EJSON.clone(doc); + + Object.keys(mod).forEach(function (op) { + var operand = mod[op]; + var modFunc = MODIFIERS[op]; + // Treat $setOnInsert as $set if this is an insert. + if (options.isInsert && op === '$setOnInsert') + modFunc = MODIFIERS['$set']; + if (!modFunc) + throw MinimongoError("Invalid modifier specified " + op); + Object.keys(operand).forEach(function (keypath) { + var arg = operand[keypath]; + if (keypath === '') { + throw MinimongoError("An empty update path is not valid."); + } + + if (keypath === '_id' && op !== '$setOnInsert') { + throw MinimongoError("Mod on _id not allowed"); + } + + var keyparts = keypath.split('.'); + + if (!keyparts.every(Boolean)) { + throw MinimongoError( + "The update path '" + keypath + + "' contains an empty field name, which is not allowed."); + } + + var noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); + var forbidArray = (op === "$rename"); + var target = findModTarget(newDoc, keyparts, { + noCreate: NO_CREATE_MODIFIERS[op], + forbidArray: (op === "$rename"), + arrayIndices: options.arrayIndices + }); + var field = keyparts.pop(); + modFunc(target, field, arg, keypath, newDoc); + }); + }); + } + + // move new document into place. + Object.keys(doc).forEach(function (k) { + // Note: this used to be for (var k in doc) however, this does not + // work right in Opera. Deleting from a doc while iterating over it + // would sometimes cause opera to skip some keys. + if (k !== '_id') + delete doc[k]; + }); + Object.keys(newDoc).forEach(function (k) { + doc[k] = newDoc[k]; + }); + }; + + static _observeFromObserveChanges = (cursor, observeCallbacks) => { + var transform = cursor.getTransform() || function (doc) {return doc;}; + var suppressed = !!observeCallbacks._suppress_initial; + + var observeChangesCallbacks; + if (LocalCollection._observeCallbacksAreOrdered(observeCallbacks)) { + // The "_no_indices" option sets all index arguments to -1 and skips the + // linear scans required to generate them. This lets observers that don't + // need absolute indices benefit from the other features of this API -- + // relative order, transforms, and applyChanges -- without the speed hit. + var indices = !observeCallbacks._no_indices; + observeChangesCallbacks = { + addedBefore: function (id, fields, before) { + var self = this; + if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) + return; + var doc = transform(Object.assign(fields, {_id: id})); + if (observeCallbacks.addedAt) { + var index = indices + ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; + observeCallbacks.addedAt(doc, index, before); + } else { + observeCallbacks.added(doc); + } + }, + changed: function (id, fields) { + var self = this; + if (!(observeCallbacks.changedAt || observeCallbacks.changed)) + return; + var doc = EJSON.clone(self.docs.get(id)); + if (!doc) + throw new Error("Unknown id for changed: " + id); + var oldDoc = transform(EJSON.clone(doc)); + DiffSequence.applyChanges(doc, fields); + doc = transform(doc); + if (observeCallbacks.changedAt) { + var index = indices ? self.docs.indexOf(id) : -1; + observeCallbacks.changedAt(doc, oldDoc, index); + } else { + observeCallbacks.changed(doc, oldDoc); + } + }, + movedBefore: function (id, before) { + var self = this; + if (!observeCallbacks.movedTo) + return; + var from = indices ? self.docs.indexOf(id) : -1; + + var to = indices + ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; + // When not moving backwards, adjust for the fact that removing the + // document slides everything back one slot. + if (to > from) + --to; + observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))), + from, to, before || null); + }, + removed: function (id) { + var self = this; + if (!(observeCallbacks.removedAt || observeCallbacks.removed)) + return; + // technically maybe there should be an EJSON.clone here, but it's about + // to be removed from self.docs! + var doc = transform(self.docs.get(id)); + if (observeCallbacks.removedAt) { + var index = indices ? self.docs.indexOf(id) : -1; + observeCallbacks.removedAt(doc, index); + } else { + observeCallbacks.removed(doc); + } + } + }; + } else { + observeChangesCallbacks = { + added: function (id, fields) { + if (!suppressed && observeCallbacks.added) { + var doc = Object.assign(fields, {_id: id}); + observeCallbacks.added(transform(doc)); + } + }, + changed: function (id, fields) { + var self = this; + if (observeCallbacks.changed) { + var oldDoc = self.docs.get(id); + var doc = EJSON.clone(oldDoc); + DiffSequence.applyChanges(doc, fields); + observeCallbacks.changed(transform(doc), + transform(EJSON.clone(oldDoc))); + } + }, + removed: function (id) { + var self = this; + if (observeCallbacks.removed) { + observeCallbacks.removed(transform(self.docs.get(id))); + } + } + }; + } + + var changeObserver = new LocalCollection._CachingChangeObserver( + {callbacks: observeChangesCallbacks}); + var handle = cursor.observeChanges(changeObserver.applyChange); + suppressed = false; + + return handle; + }; + + static _observeCallbacksAreOrdered = callbacks => { + if (callbacks.addedAt && callbacks.added) + throw new Error("Please specify only one of added() and addedAt()"); + if (callbacks.changedAt && callbacks.changed) + throw new Error("Please specify only one of changed() and changedAt()"); + if (callbacks.removed && callbacks.removedAt) + throw new Error("Please specify only one of removed() and removedAt()"); + + return !!(callbacks.addedAt || callbacks.movedTo || callbacks.changedAt + || callbacks.removedAt); + }; + + static _observeChangesCallbacksAreOrdered = callbacks => { + if (callbacks.added && callbacks.addedBefore) + throw new Error("Please specify only one of added() and addedBefore()"); + return !!(callbacks.addedBefore || callbacks.movedBefore); + }; + + // When performing an upsert, the incoming selector object can be re-used as + // the upsert modifier object, as long as Mongo query and projection + // operators (prefixed with a $ character) are removed from the newly + // created modifier object. This function attempts to strip all $ based Mongo + // operators when creating the upsert modifier object. + // NOTE: There is a known issue here in that some Mongo $ based opeartors + // should not actually be stripped. + // See https://github.com/meteor/meteor/issues/8806. + static _removeDollarOperators = selector => { + let cleansed = {}; + Object.keys(selector).forEach((key) => { + const value = selector[key]; + if (key.charAt(0) !== '$' && !objectOnlyHasDollarKeys(value)) { + if (value !== null + && value.constructor + && Object.getPrototypeOf(value) === Object.prototype) { + cleansed[key] = LocalCollection._removeDollarOperators(value); + } else { + cleansed[key] = value; + } + } + }); + return cleansed; + }; + + static _removeFromResults = (query, doc) => { + if (query.ordered) { + var i = LocalCollection._findInOrderedResults(query, doc); + query.removed(doc._id); + query.results.splice(i, 1); + } else { + var id = doc._id; // in case callback mutates doc + query.removed(doc._id); + query.results.remove(id); + } + }; + + // Is this selector just shorthand for lookup by _id? + static _selectorIsId = selector => { + return (typeof selector === "string") || + (typeof selector === "number") || + selector instanceof MongoID.ObjectID; + }; + + // Is the selector just lookup by _id (shorthand or not)? + static _selectorIsIdPerhapsAsObject = selector => { + return LocalCollection._selectorIsId(selector) || + (selector && typeof selector === "object" && + selector._id && LocalCollection._selectorIsId(selector._id) && + Object.keys(selector).length === 1); + }; + + static _updateInResults = (query, doc, old_doc) => { + if (!EJSON.equals(doc._id, old_doc._id)) + throw new Error("Can't change a doc's _id while updating"); + var projectionFn = query.projectionFn; + var changedFields = DiffSequence.makeChangedFields( + projectionFn(doc), projectionFn(old_doc)); + + if (!query.ordered) { + if (Object.keys(changedFields).length) { + query.changed(doc._id, changedFields); + query.results.set(doc._id, doc); + } + return; + } + + var orig_idx = LocalCollection._findInOrderedResults(query, doc); + + if (Object.keys(changedFields).length) + query.changed(doc._id, changedFields); + if (!query.sorter) + return; + + // just take it out and put it back in again, and see if the index + // changes + query.results.splice(orig_idx, 1); + var new_idx = LocalCollection._insertInSortedList( + query.sorter.getComparator({distances: query.distances}), + query.results, doc); + if (orig_idx !== new_idx) { + var next = query.results[new_idx+1]; + if (next) + next = next._id; + else + next = null; + query.movedBefore && query.movedBefore(doc._id, next); + } + }; + + constructor (name) { + this.name = name; + // _id -> document (also containing id) + this._docs = new LocalCollection._IdMap; + + this._observeQueue = new Meteor._SynchronousQueue(); + + this.next_qid = 1; // live query id generator + + // qid -> live query object. keys: + // ordered: bool. ordered queries have addedBefore/movedBefore callbacks. + // results: array (ordered) or object (unordered) of current results + // (aliased with this._docs!) + // resultsSnapshot: snapshot of results. null if not paused. + // cursor: Cursor object for the query. + // selector, sorter, (callbacks): functions + this.queries = {}; + + // null if not saving originals; an IdMap from id to original document value if + // saving originals. See comments before saveOriginals(). + this._savedOriginals = null; + + // True when observers are paused and we should not send callbacks. + this.paused = false; + } + + // options may include sort, skip, limit, reactive + // sort may be any of these forms: + // {a: 1, b: -1} + // [["a", "asc"], ["b", "desc"]] + // ["a", ["b", "desc"]] + // (in the first form you're beholden to key enumeration order in + // your javascript VM) + // + // reactive: if given, and false, don't register with Tracker (default + // is true) + // + // XXX possibly should support retrieving a subset of fields? and + // have it be a hint (ignored on the client, when not copying the + // doc?) + // + // XXX sort does not yet support subkeys ('a.b') .. fix that! + // XXX add one more sort form: "key" + // XXX tests + find (selector, options) { + // default syntax for everything is to omit the selector argument. + // but if selector is explicitly passed in as false or undefined, we + // want a selector that matches nothing. + if (arguments.length === 0) + selector = {}; + + return new LocalCollection.Cursor(this, selector, options); + } + + findOne (selector, options) { + if (arguments.length === 0) + selector = {}; + + // NOTE: by setting limit 1 here, we end up using very inefficient + // code that recomputes the whole query on each update. The upside is + // that when you reactively depend on a findOne you only get + // invalidated when the found object changes, not any object in the + // collection. Most findOne will be by id, which has a fast path, so + // this might not be a big deal. In most cases, invalidation causes + // the called to re-query anyway, so this should be a net performance + // improvement. + options = options || {}; + options.limit = 1; + + return this.find(selector, options).fetch()[0]; + } + + // XXX possibly enforce that 'undefined' does not appear (we assume + // this in our handling of null and $exists) + insert (doc, callback) { + var self = this; + doc = EJSON.clone(doc); + + assertHasValidFieldNames(doc); + + if (!doc.hasOwnProperty('_id')) { + // if you really want to use ObjectIDs, set this global. + // Mongo.Collection specifies its own ids and does not use this code. + doc._id = LocalCollection._useOID ? new MongoID.ObjectID() + : Random.id(); + } + var id = doc._id; + + if (self._docs.has(id)) + throw MinimongoError("Duplicate _id '" + id + "'"); + + self._saveOriginal(id, undefined); + self._docs.set(id, doc); + + var queriesToRecompute = []; + // trigger live queries that match + for (var qid in self.queries) { + var query = self.queries[qid]; + if (query.dirty) continue; + var matchResult = query.matcher.documentMatches(doc); + if (matchResult.result) { + if (query.distances && matchResult.distance !== undefined) + query.distances.set(id, matchResult.distance); + if (query.cursor.skip || query.cursor.limit) + queriesToRecompute.push(qid); + else + LocalCollection._insertInResults(query, doc); + } + } + + queriesToRecompute.forEach(function (qid) { + if (self.queries[qid]) + self._recomputeResults(self.queries[qid]); + }); + self._observeQueue.drain(); + + // Defer because the caller likely doesn't expect the callback to be run + // immediately. + if (callback) + Meteor.defer(function () { + callback(null, id); + }); + return id; + } + + // Pause the observers. No callbacks from observers will fire until + // 'resumeObservers' is called. + pauseObservers () { + // No-op if already paused. + if (this.paused) + return; + + // Set the 'paused' flag such that new observer messages don't fire. + this.paused = true; + + // Take a snapshot of the query results for each query. + for (var qid in this.queries) { + var query = this.queries[qid]; + + query.resultsSnapshot = EJSON.clone(query.results); + } + } + + remove (selector, callback) { + var self = this; + + // Easy special case: if we're not calling observeChanges callbacks and we're + // not saving originals and we got asked to remove everything, then just empty + // everything directly. + if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) { + var result = self._docs.size(); + self._docs.clear(); + Object.keys(self.queries).forEach(function (qid) { + var query = self.queries[qid]; + if (query.ordered) { + query.results = []; + } else { + query.results.clear(); + } + }); + if (callback) { + Meteor.defer(function () { + callback(null, result); + }); + } + return result; + } + + var matcher = new Minimongo.Matcher(selector); + var remove = []; + self._eachPossiblyMatchingDoc(selector, function (doc, id) { + if (matcher.documentMatches(doc).result) + remove.push(id); + }); + + var queriesToRecompute = []; + var queryRemove = []; + for (var i = 0; i < remove.length; i++) { + var removeId = remove[i]; + var removeDoc = self._docs.get(removeId); + Object.keys(self.queries).forEach(function (qid) { + var query = self.queries[qid]; + if (query.dirty) return; + + if (query.matcher.documentMatches(removeDoc).result) { + if (query.cursor.skip || query.cursor.limit) + queriesToRecompute.push(qid); + else + queryRemove.push({qid: qid, doc: removeDoc}); + } + }); + self._saveOriginal(removeId, removeDoc); + self._docs.remove(removeId); + } + + // run live query callbacks _after_ we've removed the documents. + queryRemove.forEach(function (remove) { + var query = self.queries[remove.qid]; + if (query) { + query.distances && query.distances.remove(remove.doc._id); + LocalCollection._removeFromResults(query, remove.doc); + } + }); + queriesToRecompute.forEach(function (qid) { + var query = self.queries[qid]; + if (query) + self._recomputeResults(query); + }); + self._observeQueue.drain(); + result = remove.length; + if (callback) + Meteor.defer(function () { + callback(null, result); + }); + return result; + } + + // Resume the observers. Observers immediately receive change + // notifications to bring them to the current state of the + // database. Note that this is not just replaying all the changes that + // happened during the pause, it is a smarter 'coalesced' diff. + resumeObservers () { + var self = this; + // No-op if not paused. + if (!this.paused) + return; + + // Unset the 'paused' flag. Make sure to do this first, otherwise + // observer methods won't actually fire when we trigger them. + this.paused = false; + + for (var qid in this.queries) { + var query = self.queries[qid]; + if (query.dirty) { + query.dirty = false; + // re-compute results will perform `LocalCollection._diffQueryChanges` automatically. + self._recomputeResults(query, query.resultsSnapshot); + } else { + // Diff the current results against the snapshot and send to observers. + // pass the query object for its observer callbacks. + LocalCollection._diffQueryChanges( + query.ordered, query.resultsSnapshot, query.results, query, + {projectionFn: query.projectionFn}); + } + query.resultsSnapshot = null; + } + self._observeQueue.drain(); + } + + retrieveOriginals () { + var self = this; + if (!self._savedOriginals) + throw new Error("Called retrieveOriginals without saveOriginals"); + + var originals = self._savedOriginals; + self._savedOriginals = null; + return originals; + } + + // To track what documents are affected by a piece of code, call saveOriginals() + // before it and retrieveOriginals() after it. retrieveOriginals returns an + // object whose keys are the ids of the documents that were affected since the + // call to saveOriginals(), and the values are equal to the document's contents + // at the time of saveOriginals. (In the case of an inserted document, undefined + // is the value.) You must alternate between calls to saveOriginals() and + // retrieveOriginals(). + saveOriginals () { + var self = this; + if (self._savedOriginals) + throw new Error("Called saveOriginals twice without retrieveOriginals"); + self._savedOriginals = new LocalCollection._IdMap; + } + + // XXX atomicity: if multi is true, and one modification fails, do + // we rollback the whole operation, or what? + update (selector, mod, options, callback) { + var self = this; + if (! callback && options instanceof Function) { + callback = options; + options = null; + } + if (!options) options = {}; + + var matcher = new Minimongo.Matcher(selector, true); + + // Save the original results of any query that we might need to + // _recomputeResults on, because _modifyAndNotify will mutate the objects in + // it. (We don't need to save the original results of paused queries because + // they already have a resultsSnapshot and we won't be diffing in + // _recomputeResults.) + var qidToOriginalResults = {}; + // We should only clone each document once, even if it appears in multiple queries + var docMap = new LocalCollection._IdMap; + var idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector); + + Object.keys(self.queries).forEach(function (qid) { + var query = self.queries[qid]; + if ((query.cursor.skip || query.cursor.limit) && ! self.paused) { + // Catch the case of a reactive `count()` on a cursor with skip + // or limit, which registers an unordered observe. This is a + // pretty rare case, so we just clone the entire result set with + // no optimizations for documents that appear in these result + // sets and other queries. + if (query.results instanceof LocalCollection._IdMap) { + qidToOriginalResults[qid] = query.results.clone(); + return; + } + + if (!(query.results instanceof Array)) { + throw new Error("Assertion failed: query.results not an array"); + } + + // Clones a document to be stored in `qidToOriginalResults` + // because it may be modified before the new and old result sets + // are diffed. But if we know exactly which document IDs we're + // going to modify, then we only need to clone those. + var memoizedCloneIfNeeded = function(doc) { + if (docMap.has(doc._id)) { + return docMap.get(doc._id); + } else { + var docToMemoize; + + if (idsMatchedBySelector && !idsMatchedBySelector.some(function(id) { + return EJSON.equals(id, doc._id); + })) { + docToMemoize = doc; + } else { + docToMemoize = EJSON.clone(doc); + } + + docMap.set(doc._id, docToMemoize); + return docToMemoize; + } + }; + + qidToOriginalResults[qid] = query.results.map(memoizedCloneIfNeeded); + } + }); + var recomputeQids = {}; + + var updateCount = 0; + + self._eachPossiblyMatchingDoc(selector, function (doc, id) { + var queryResult = matcher.documentMatches(doc); + if (queryResult.result) { + // XXX Should we save the original even if mod ends up being a no-op? + self._saveOriginal(id, doc); + self._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices); + ++updateCount; + if (!options.multi) + return false; // break + } + return true; + }); + + Object.keys(recomputeQids).forEach(function (qid) { + var query = self.queries[qid]; + if (query) + self._recomputeResults(query, qidToOriginalResults[qid]); + }); + self._observeQueue.drain(); + + // If we are doing an upsert, and we didn't modify any documents yet, then + // it's time to do an insert. Figure out what document we are inserting, and + // generate an id for it. + var insertedId; + if (updateCount === 0 && options.upsert) { + + let selectorModifier = LocalCollection._selectorIsId(selector) + ? { _id: selector } + : selector; + + selectorModifier = LocalCollection._removeDollarOperators(selectorModifier); + + const newDoc = {}; + if (selectorModifier._id) { + newDoc._id = selectorModifier._id; + delete selectorModifier._id; + } + + // This double _modify call is made to help work around an issue where collection + // upserts won't work properly, with nested properties (see issue #8631). + LocalCollection._modify(newDoc, {$set: selectorModifier}); + LocalCollection._modify(newDoc, mod, {isInsert: true}); + + if (! newDoc._id && options.insertedId) + newDoc._id = options.insertedId; + insertedId = self.insert(newDoc); + updateCount = 1; + } + + // Return the number of affected documents, or in the upsert case, an object + // containing the number of affected docs and the id of the doc that was + // inserted, if any. + var result; + if (options._returnObject) { + result = { + numberAffected: updateCount + }; + if (insertedId !== undefined) + result.insertedId = insertedId; + } else { + result = updateCount; + } + + if (callback) + Meteor.defer(function () { + callback(null, result); + }); + return result; + } + + // A convenience wrapper on update. LocalCollection.upsert(sel, mod) is + // equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject: + // true }). + upsert (selector, mod, options, callback) { + var self = this; + if (! callback && typeof options === "function") { + callback = options; + options = {}; + } + return self.update(selector, mod, Object.assign({}, options, { + upsert: true, + _returnObject: true + }), callback); + } + + // Iterates over a subset of documents that could match selector; calls + // f(doc, id) on each of them. Specifically, if selector specifies + // specific _id's, it only looks at those. doc is *not* cloned: it is the + // same object that is in _docs. + _eachPossiblyMatchingDoc (selector, f) { + var self = this; + var specificIds = LocalCollection._idsMatchedBySelector(selector); + if (specificIds) { + for (var i = 0; i < specificIds.length; ++i) { + var id = specificIds[i]; + var doc = self._docs.get(id); + if (doc) { + var breakIfFalse = f(doc, id); + if (breakIfFalse === false) + break; + } + } + } else { + self._docs.forEach(f); + } + } + + _modifyAndNotify (doc, mod, recomputeQids, arrayIndices) { + var self = this; + + var matched_before = {}; + for (var qid in self.queries) { + var query = self.queries[qid]; + if (query.dirty) continue; + + if (query.ordered) { + matched_before[qid] = query.matcher.documentMatches(doc).result; + } else { + // Because we don't support skip or limit (yet) in unordered queries, we + // can just do a direct lookup. + matched_before[qid] = query.results.has(doc._id); + } + } + + var old_doc = EJSON.clone(doc); + + LocalCollection._modify(doc, mod, {arrayIndices: arrayIndices}); + + for (qid in self.queries) { + query = self.queries[qid]; + if (query.dirty) continue; + + var before = matched_before[qid]; + var afterMatch = query.matcher.documentMatches(doc); + var after = afterMatch.result; + if (after && query.distances && afterMatch.distance !== undefined) + query.distances.set(doc._id, afterMatch.distance); + + if (query.cursor.skip || query.cursor.limit) { + // We need to recompute any query where the doc may have been in the + // cursor's window either before or after the update. (Note that if skip + // or limit is set, "before" and "after" being true do not necessarily + // mean that the document is in the cursor's output after skip/limit is + // applied... but if they are false, then the document definitely is NOT + // in the output. So it's safe to skip recompute if neither before or + // after are true.) + if (before || after) + recomputeQids[qid] = true; + } else if (before && !after) { + LocalCollection._removeFromResults(query, doc); + } else if (!before && after) { + LocalCollection._insertInResults(query, doc); + } else if (before && after) { + LocalCollection._updateInResults(query, doc, old_doc); + } + } + } + + // Recomputes the results of a query and runs observe callbacks for the + // difference between the previous results and the current results (unless + // paused). Used for skip/limit queries. + // + // When this is used by insert or remove, it can just use query.results for the + // old results (and there's no need to pass in oldResults), because these + // operations don't mutate the documents in the collection. Update needs to pass + // in an oldResults which was deep-copied before the modifier was applied. + // + // oldResults is guaranteed to be ignored if the query is not paused. + _recomputeResults (query, oldResults) { + var self = this; + if (self.paused) { + // There's no reason to recompute the results now as we're still paused. + // By flagging the query as "dirty", the recompute will be performed + // when resumeObservers is called. + query.dirty = true; + return; + } + + if (! self.paused && ! oldResults) + oldResults = query.results; + if (query.distances) + query.distances.clear(); + query.results = query.cursor._getRawObjects({ + ordered: query.ordered, distances: query.distances}); + + if (! self.paused) { + LocalCollection._diffQueryChanges( + query.ordered, oldResults, query.results, query, + { projectionFn: query.projectionFn }); + } + } + + _saveOriginal (id, doc) { + var self = this; + // Are we even trying to save originals? + if (!self._savedOriginals) + return; + // Have we previously mutated the original (and so 'doc' is not actually + // original)? (Note the 'has' check rather than truth: we store undefined + // here for inserted docs!) + if (self._savedOriginals.has(id)) + return; + self._savedOriginals.set(id, EJSON.clone(doc)); + } +} + +const MODIFIERS = { + $currentDate: function (target, field, arg) { + if (typeof arg === "object" && arg.hasOwnProperty("$type")) { + if (arg.$type !== "date") { + throw MinimongoError( + "Minimongo does currently only support the date type " + + "in $currentDate modifiers", + { field }); + } + } else if (arg !== true) { + throw MinimongoError("Invalid $currentDate modifier", { field }); + } + target[field] = new Date(); + }, + $min: function (target, field, arg) { + if (typeof arg !== "number") { + throw MinimongoError("Modifier $min allowed for numbers only", { field }); + } + if (field in target) { + if (typeof target[field] !== "number") { + throw MinimongoError( + "Cannot apply $min modifier to non-number", { field }); + } + if (target[field] > arg) { + target[field] = arg; + } + } else { + target[field] = arg; + } + }, + $max: function (target, field, arg) { + if (typeof arg !== "number") { + throw MinimongoError("Modifier $max allowed for numbers only", { field }); + } + if (field in target) { + if (typeof target[field] !== "number") { + throw MinimongoError( + "Cannot apply $max modifier to non-number", { field }); + } + if (target[field] < arg) { + target[field] = arg; + } + } else { + target[field] = arg; + } + }, + $inc: function (target, field, arg) { + if (typeof arg !== "number") + throw MinimongoError("Modifier $inc allowed for numbers only", { field }); + if (field in target) { + if (typeof target[field] !== "number") + throw MinimongoError( + "Cannot apply $inc modifier to non-number", { field }); + target[field] += arg; + } else { + target[field] = arg; + } + }, + $set: function (target, field, arg) { + if (target !== Object(target)) { // not an array or an object + var e = MinimongoError( + "Cannot set property on non-object field", { field }); + e.setPropertyError = true; + throw e; + } + if (target === null) { + var e = MinimongoError("Cannot set property on null", { field }); + e.setPropertyError = true; + throw e; + } + assertHasValidFieldNames(arg); + target[field] = arg; + }, + $setOnInsert: function (target, field, arg) { + // converted to `$set` in `_modify` + }, + $unset: function (target, field, arg) { + if (target !== undefined) { + if (target instanceof Array) { + if (field in target) + target[field] = null; + } else + delete target[field]; + } + }, + $push: function (target, field, arg) { + if (target[field] === undefined) + target[field] = []; + if (!(target[field] instanceof Array)) + throw MinimongoError( + "Cannot apply $push modifier to non-array", { field }); + + if (!(arg && arg.$each)) { + // Simple mode: not $each + assertHasValidFieldNames(arg); + target[field].push(arg); + return; + } + + // Fancy mode: $each (and maybe $slice and $sort and $position) + var toPush = arg.$each; + if (!(toPush instanceof Array)) + throw MinimongoError("$each must be an array", { field }); + assertHasValidFieldNames(toPush); + + // Parse $position + var position = undefined; + if ('$position' in arg) { + if (typeof arg.$position !== "number") + throw MinimongoError("$position must be a numeric value", { field }); + // XXX should check to make sure integer + if (arg.$position < 0) + throw MinimongoError( + "$position in $push must be zero or positive", { field }); + position = arg.$position; + } + + // Parse $slice. + var slice = undefined; + if ('$slice' in arg) { + if (typeof arg.$slice !== "number") + throw MinimongoError("$slice must be a numeric value", { field }); + // XXX should check to make sure integer + slice = arg.$slice; + } + + // Parse $sort. + var sortFunction = undefined; + if (arg.$sort) { + if (slice === undefined) + throw MinimongoError("$sort requires $slice to be present", { field }); + // XXX this allows us to use a $sort whose value is an array, but that's + // actually an extension of the Node driver, so it won't work + // server-side. Could be confusing! + // XXX is it correct that we don't do geo-stuff here? + sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); + for (var i = 0; i < toPush.length; i++) { + if (LocalCollection._f._type(toPush[i]) !== 3) { + throw MinimongoError("$push like modifiers using $sort " + + "require all elements to be objects", { field }); + } + } + } + + // Actually push. + if (position === undefined) { + for (var j = 0; j < toPush.length; j++) + target[field].push(toPush[j]); + } else { + var spliceArguments = [position, 0]; + for (var j = 0; j < toPush.length; j++) + spliceArguments.push(toPush[j]); + Array.prototype.splice.apply(target[field], spliceArguments); + } + + // Actually sort. + if (sortFunction) + target[field].sort(sortFunction); + + // Actually slice. + if (slice !== undefined) { + if (slice === 0) + target[field] = []; // differs from Array.slice! + else if (slice < 0) + target[field] = target[field].slice(slice); + else + target[field] = target[field].slice(0, slice); + } + }, + $pushAll: function (target, field, arg) { + if (!(typeof arg === "object" && arg instanceof Array)) + throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); + assertHasValidFieldNames(arg); + var x = target[field]; + if (x === undefined) + target[field] = arg; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $pushAll modifier to non-array", { field }); + else { + for (var i = 0; i < arg.length; i++) + x.push(arg[i]); + } + }, + $addToSet: function (target, field, arg) { + var isEach = false; + if (typeof arg === "object") { + //check if first key is '$each' + const keys = Object.keys(arg); + if (keys[0] === "$each"){ + isEach = true; + } + } + var values = isEach ? arg["$each"] : [arg]; + assertHasValidFieldNames(values); + var x = target[field]; + if (x === undefined) + target[field] = values; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $addToSet modifier to non-array", { field }); + else { + values.forEach(function (value) { + for (var i = 0; i < x.length; i++) + if (LocalCollection._f._equal(value, x[i])) + return; + x.push(value); + }); + } + }, + $pop: function (target, field, arg) { + if (target === undefined) + return; + var x = target[field]; + if (x === undefined) + return; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $pop modifier to non-array", { field }); + else { + if (typeof arg === 'number' && arg < 0) + x.splice(0, 1); + else + x.pop(); + } + }, + $pull: function (target, field, arg) { + if (target === undefined) + return; + var x = target[field]; + if (x === undefined) + return; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $pull/pullAll modifier to non-array", { field }); + else { + var out = []; + if (arg != null && typeof arg === "object" && !(arg instanceof Array)) { + // XXX would be much nicer to compile this once, rather than + // for each document we modify.. but usually we're not + // modifying that many documents, so we'll let it slide for + // now + + // XXX Minimongo.Matcher isn't up for the job, because we need + // to permit stuff like {$pull: {a: {$gt: 4}}}.. something + // like {$gt: 4} is not normally a complete selector. + // same issue as $elemMatch possibly? + var matcher = new Minimongo.Matcher(arg); + for (var i = 0; i < x.length; i++) + if (!matcher.documentMatches(x[i]).result) + out.push(x[i]); + } else { + for (var i = 0; i < x.length; i++) + if (!LocalCollection._f._equal(x[i], arg)) + out.push(x[i]); + } + target[field] = out; + } + }, + $pullAll: function (target, field, arg) { + if (!(typeof arg === "object" && arg instanceof Array)) + throw MinimongoError( + "Modifier $pushAll/pullAll allowed for arrays only", { field }); + if (target === undefined) + return; + var x = target[field]; + if (x === undefined) + return; + else if (!(x instanceof Array)) + throw MinimongoError( + "Cannot apply $pull/pullAll modifier to non-array", { field }); + else { + var out = []; + for (var i = 0; i < x.length; i++) { + var exclude = false; + for (var j = 0; j < arg.length; j++) { + if (LocalCollection._f._equal(x[i], arg[j])) { + exclude = true; + break; + } + } + if (!exclude) + out.push(x[i]); + } + target[field] = out; + } + }, + $rename: function (target, field, arg, keypath, doc) { + if (keypath === arg) + // no idea why mongo has this restriction.. + throw MinimongoError("$rename source must differ from target", { field }); + if (target === null) + throw MinimongoError("$rename source field invalid", { field }); + if (typeof arg !== "string") + throw MinimongoError("$rename target must be a string", { field }); + if (arg.indexOf('\0') > -1) { + // Null bytes are not allowed in Mongo field names + // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names + throw MinimongoError( + "The 'to' field for $rename cannot contain an embedded null byte", + { field }); + } + if (target === undefined) + return; + var v = target[field]; + delete target[field]; + + var keyparts = arg.split('.'); + var target2 = findModTarget(doc, keyparts, {forbidArray: true}); + if (target2 === null) + throw MinimongoError("$rename target field invalid", { field }); + var field2 = keyparts.pop(); + target2[field2] = v; + }, + $bit: function (target, field, arg) { + // XXX mongo only supports $bit on integers, and we only support + // native javascript numbers (doubles) so far, so we can't support $bit + throw MinimongoError("$bit is not supported", { field }); + } +}; + +const NO_CREATE_MODIFIERS = { + $unset: true, + $pop: true, + $rename: true, + $pull: true, + $pullAll: true +}; + +// Make sure field names do not contain Mongo restricted +// characters ('.', '$', '\0'). +// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names +const invalidCharMsg = { + '.': "contain '.'", + '$': "start with '$'", + '\0': "contain null bytes", +}; + +// checks if all field names in an object are valid +function assertHasValidFieldNames (doc){ + if (doc && typeof doc === "object") { + JSON.stringify(doc, (key, value) => { + assertIsValidFieldName(key); + return value; + }); + } +} + +function assertIsValidFieldName (key) { + let match; + if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { + throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); + } +} + +// for a.b.c.2.d.e, keyparts should be ['a', 'b', 'c', '2', 'd', 'e'], +// and then you would operate on the 'e' property of the returned +// object. +// +// if options.noCreate is falsey, creates intermediate levels of +// structure as necessary, like mkdir -p (and raises an exception if +// that would mean giving a non-numeric property to an array.) if +// options.noCreate is true, return undefined instead. +// +// may modify the last element of keyparts to signal to the caller that it needs +// to use a different value to index into the returned object (for example, +// ['a', '01'] -> ['a', 1]). +// +// if forbidArray is true, return null if the keypath goes through an array. +// +// if options.arrayIndices is set, use its first element for the (first) '$' in +// the path. +function findModTarget (doc, keyparts, options) { + options = options || {}; + var usedArrayIndex = false; + for (var i = 0; i < keyparts.length; i++) { + var last = (i === keyparts.length - 1); + var keypart = keyparts[i]; + var indexable = isIndexable(doc); + if (!indexable) { + if (options.noCreate) + return undefined; + var e = MinimongoError( + "cannot use the part '" + keypart + "' to traverse " + doc); + e.setPropertyError = true; + throw e; + } + if (doc instanceof Array) { + if (options.forbidArray) + return null; + if (keypart === '$') { + if (usedArrayIndex) + throw MinimongoError("Too many positional (i.e. '$') elements"); + if (!options.arrayIndices || !options.arrayIndices.length) { + throw MinimongoError("The positional operator did not find the " + + "match needed from the query"); + } + keypart = options.arrayIndices[0]; + usedArrayIndex = true; + } else if (isNumericKey(keypart)) { + keypart = parseInt(keypart); + } else { + if (options.noCreate) + return undefined; + throw MinimongoError( + "can't append to array using string field name [" + + keypart + "]"); + } + if (last) + // handle 'a.01' + keyparts[i] = keypart; + if (options.noCreate && keypart >= doc.length) + return undefined; + while (doc.length < keypart) + doc.push(null); + if (!last) { + if (doc.length === keypart) + doc.push({}); + else if (typeof doc[keypart] !== "object") + throw MinimongoError("can't modify field '" + keyparts[i + 1] + + "' of list value " + JSON.stringify(doc[keypart])); + } + } else { + assertIsValidFieldName(keypart); + if (!(keypart in doc)) { + if (options.noCreate) + return undefined; + if (!last) + doc[keypart] = {}; + } + } + + if (last) + return doc; + doc = doc[keypart]; + } + + // notreached +} + +function objectOnlyHasDollarKeys (object) { + const keys = Object.keys(object); + return keys.length > 0 && keys.every(key => key.charAt(0) === '$'); +} + +// paths - Array: list of mongo style paths +// newLeafFn - Function: of form function(path) should return a scalar value to +// put into list created for that path +// conflictFn - Function: of form function(node, path, fullPath) is called +// when building a tree path for 'fullPath' node on +// 'path' was already a leaf with a value. Must return a +// conflict resolution. +// initial tree - Optional Object: starting tree. +// @returns - Object: tree represented as a set of nested objects +function pathsToTree (paths, newLeafFn, conflictFn, tree) { + tree = tree || {}; + paths.forEach(function (keyPath) { + var treePos = tree; + var pathArr = keyPath.split('.'); + + // use .every just for iteration with break + var success = pathArr.slice(0, -1).every(function (key, idx) { + if (!treePos.hasOwnProperty(key)) + treePos[key] = {}; + else if (treePos[key] !== Object(treePos[key])) { + treePos[key] = conflictFn(treePos[key], + pathArr.slice(0, idx + 1).join('.'), + keyPath); + // break out of loop if we are failing for this path + if (treePos[key] !== Object(treePos[key])) + return false; + } + + treePos = treePos[key]; + return true; + }); + + if (success) { + var lastKey = pathArr[pathArr.length - 1]; + if (!treePos.hasOwnProperty(lastKey)) + treePos[lastKey] = newLeafFn(keyPath); + else + treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); + } + }); + + return tree; +} + +// Traverses the keys of passed projection and constructs a tree where all +// leaves are either all True or all False +// @returns Object: +// - tree - Object - tree representation of keys involved in projection +// (exception for '_id' as it is a special case handled separately) +// - including - Boolean - "take only certain fields" type of projection +function projectionDetails (fields) { + // Find the non-_id keys (_id is handled specially because it is included unless + // explicitly excluded). Sort the keys, so that our code to detect overlaps + // like 'foo' and 'foo.bar' can assume that 'foo' comes first. + var fieldsKeys = Object.keys(fields).sort(); + + // If _id is the only field in the projection, do not remove it, since it is + // required to determine if this is an exclusion or exclusion. Also keep an + // inclusive _id, since inclusive _id follows the normal rules about mixing + // inclusive and exclusive fields. If _id is not the only field in the + // projection and is exclusive, remove it so it can be handled later by a + // special case, since exclusive _id is always allowed. + if (fieldsKeys.length > 0 && + !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && + !(fieldsKeys.includes('_id') && fields['_id'])) + fieldsKeys = fieldsKeys.filter(function (key) { return key !== '_id'; }); + + var including = null; // Unknown + + fieldsKeys.forEach(function (keyPath) { + var rule = !!fields[keyPath]; + if (including === null) + including = rule; + if (including !== rule) + // This error message is copied from MongoDB shell + throw MinimongoError("You cannot currently mix including and excluding fields."); + }); + + + var projectionRulesTree = pathsToTree( + fieldsKeys, + function (path) { return including; }, + function (node, path, fullPath) { + // Check passed projection fields' keys: If you have two rules such as + // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If + // that happens, there is a probability you are doing something wrong, + // framework should notify you about such mistake earlier on cursor + // compilation step than later during runtime. Note, that real mongo + // doesn't do anything about it and the later rule appears in projection + // project, more priority it takes. + // + // Example, assume following in mongo shell: + // > db.coll.insert({ a: { b: 23, c: 44 } }) + // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) + // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } } + // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) + // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } + // + // Note, how second time the return set of keys is different. + + var currentPath = fullPath; + var anotherPath = path; + throw MinimongoError("both " + currentPath + " and " + anotherPath + + " found in fields option, using both of them may trigger " + + "unexpected behavior. Did you mean to use only one of them?"); + }); + + return { + tree: projectionRulesTree, + including: including + }; +} diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 1470a57366..081b13596e 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -1,35 +1,11 @@ -import {Matcher} from './matcher'; +import {LocalCollection} from './local_collection.js'; +import {Matcher} from './matcher.js'; import { isIndexable, isNumericKey, isOperatorObject, } from './common.js'; -// Make sure field names do not contain Mongo restricted -// characters ('.', '$', '\0'). -// https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -const invalidCharMsg = { - '.': "contain '.'", - '$': "start with '$'", - '\0': "contain null bytes", -}; -function assertIsValidFieldName(key) { - let match; - if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { - throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); - } -}; - -// checks if all field names in an object are valid -function assertHasValidFieldNames(doc){ - if (doc && typeof doc === "object") { - JSON.stringify(doc, (key, value) => { - assertIsValidFieldName(key); - return value; - }); - } -}; - // XXX type checking on selectors (graceful error if malformed) @@ -40,34 +16,7 @@ function assertHasValidFieldNames(doc){ // ObserveHandle: the return value of a live query. -LocalCollection = function (name) { - var self = this; - self.name = name; - // _id -> document (also containing id) - self._docs = new LocalCollection._IdMap; - - self._observeQueue = new Meteor._SynchronousQueue(); - - self.next_qid = 1; // live query id generator - - // qid -> live query object. keys: - // ordered: bool. ordered queries have addedBefore/movedBefore callbacks. - // results: array (ordered) or object (unordered) of current results - // (aliased with self._docs!) - // resultsSnapshot: snapshot of results. null if not paused. - // cursor: Cursor object for the query. - // selector, sorter, (callbacks): functions - self.queries = {}; - - // null if not saving originals; an IdMap from id to original document value if - // saving originals. See comments before saveOriginals(). - self._savedOriginals = null; - - // True when observers are paused and we should not send callbacks. - self.paused = false; -}; - -Minimongo = {Matcher}; +Minimongo = {LocalCollection, Matcher}; // Object exported only for unit testing. // Use it to export private functions to test in Tinytest. @@ -83,35 +32,6 @@ MinimongoError = function (message, options={}) { return e; }; - -// options may include sort, skip, limit, reactive -// sort may be any of these forms: -// {a: 1, b: -1} -// [["a", "asc"], ["b", "desc"]] -// ["a", ["b", "desc"]] -// (in the first form you're beholden to key enumeration order in -// your javascript VM) -// -// reactive: if given, and false, don't register with Tracker (default -// is true) -// -// XXX possibly should support retrieving a subset of fields? and -// have it be a hint (ignored on the client, when not copying the -// doc?) -// -// XXX sort does not yet support subkeys ('a.b') .. fix that! -// XXX add one more sort form: "key" -// XXX tests -LocalCollection.prototype.find = function (selector, options) { - // default syntax for everything is to omit the selector argument. - // but if selector is explicitly passed in as false or undefined, we - // want a selector that matches nothing. - if (arguments.length === 0) - selector = {}; - - return new LocalCollection.Cursor(this, selector, options); -}; - // don't call this ctor directly. use LocalCollection.find(). LocalCollection.Cursor = function (collection, selector, options) { @@ -156,24 +76,6 @@ LocalCollection.Cursor = function (collection, selector, options) { LocalCollection.Cursor.prototype.rewind = function () { }; -LocalCollection.prototype.findOne = function (selector, options) { - if (arguments.length === 0) - selector = {}; - - // NOTE: by setting limit 1 here, we end up using very inefficient - // code that recomputes the whole query on each update. The upside is - // that when you reactively depend on a findOne you only get - // invalidated when the found object changes, not any object in the - // collection. Most findOne will be by id, which has a fast path, so - // this might not be a big deal. In most cases, invalidation causes - // the called to re-query anyway, so this should be a net performance - // improvement. - options = options || {}; - options.limit = 1; - - return this.find(selector, options).fetch()[0]; -}; - /** * @callback IterationCallback * @param {Object} doc @@ -287,27 +189,6 @@ LocalCollection.Cursor.prototype._getCollectionName = function () { return self.collection.name; }; -LocalCollection._observeChangesCallbacksAreOrdered = function (callbacks) { - if (callbacks.added && callbacks.addedBefore) - throw new Error("Please specify only one of added() and addedBefore()"); - return !!(callbacks.addedBefore || callbacks.movedBefore); -}; - -LocalCollection._observeCallbacksAreOrdered = function (callbacks) { - if (callbacks.addedAt && callbacks.added) - throw new Error("Please specify only one of added() and addedAt()"); - if (callbacks.changedAt && callbacks.changed) - throw new Error("Please specify only one of changed() and changedAt()"); - if (callbacks.removed && callbacks.removedAt) - throw new Error("Please specify only one of removed() and removedAt()"); - - return !!(callbacks.addedAt || callbacks.movedTo || callbacks.changedAt - || callbacks.removedAt); -}; - -// the handle that comes back from observe. -LocalCollection.ObserveHandle = function () {}; - // options to contain: // * callbacks for observe(): // - addedAt (document, atIndex) @@ -329,134 +210,132 @@ LocalCollection.ObserveHandle = function () {}; // XXX maybe callbacks should take a list of objects, to expose transactions? // XXX maybe support field limiting (to limit what you're notified on) -Object.assign(LocalCollection.Cursor.prototype, { - /** - * @summary Watch a query. Receive callbacks as the result set changes. - * @locus Anywhere - * @memberOf Mongo.Cursor - * @instance - * @param {Object} callbacks Functions to call to deliver the result set as it changes - */ - observe: function (options) { - var self = this; - return LocalCollection._observeFromObserveChanges(self, options); - }, +/** + * @summary Watch a query. Receive callbacks as the result set changes. + * @locus Anywhere + * @memberOf Mongo.Cursor + * @instance + * @param {Object} callbacks Functions to call to deliver the result set as it changes + */ +LocalCollection.Cursor.prototype.observe = function (options) { + var self = this; + return LocalCollection._observeFromObserveChanges(self, options); +}; - /** - * @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 - * @memberOf Mongo.Cursor - * @instance - * @param {Object} callbacks Functions to call to deliver the result set as it changes - */ - observeChanges: function (options) { - var self = this; +/** + * @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 + * @memberOf Mongo.Cursor + * @instance + * @param {Object} callbacks Functions to call to deliver the result set as it changes + */ +LocalCollection.Cursor.prototype.observeChanges = function (options) { + var self = this; - var ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); + var ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); - // there are several places that assume you aren't combining skip/limit with - // unordered observe. eg, update's EJSON.clone, and the "there are several" - // comment in _modifyAndNotify - // XXX allow skip/limit with unordered observe - if (!options._allow_unordered && !ordered && (self.skip || self.limit)) - throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); + // there are several places that assume you aren't combining skip/limit with + // unordered observe. eg, update's EJSON.clone, and the "there are several" + // comment in _modifyAndNotify + // XXX allow skip/limit with unordered observe + if (!options._allow_unordered && !ordered && (self.skip || self.limit)) + throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); - if (self.fields && (self.fields._id === 0 || self.fields._id === false)) - throw Error("You may not observe a cursor with {fields: {_id: 0}}"); + if (self.fields && (self.fields._id === 0 || self.fields._id === false)) + throw Error("You may not observe a cursor with {fields: {_id: 0}}"); - var query = { - dirty: false, - matcher: self.matcher, // not fast pathed - sorter: ordered && self.sorter, - distances: ( - self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), - resultsSnapshot: null, - ordered: ordered, - cursor: self, - projectionFn: self._projectionFn - }; - var qid; + var query = { + dirty: false, + matcher: self.matcher, // not fast pathed + sorter: ordered && self.sorter, + distances: ( + self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), + resultsSnapshot: null, + ordered: ordered, + cursor: self, + projectionFn: self._projectionFn + }; + var qid; - // Non-reactive queries call added[Before] and then never call anything - // else. - if (self.reactive) { - qid = self.collection.next_qid++; - self.collection.queries[qid] = query; - } - query.results = self._getRawObjects({ - ordered: ordered, distances: query.distances}); - if (self.collection.paused) - query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); - - // wrap callbacks we were passed. callbacks only fire when not paused and - // are never undefined - // Filters out blacklisted fields according to cursor's projection. - // XXX wrong place for this? - - // furthermore, callbacks enqueue until the operation we're working on is - // done. - var wrapCallback = function (f) { - if (!f) - return function () {}; - return function (/*args*/) { - var context = this; - var args = arguments; - - if (self.collection.paused) - return; - - self.collection._observeQueue.queueTask(function () { - f.apply(context, args); - }); - }; - }; - query.added = wrapCallback(options.added); - query.changed = wrapCallback(options.changed); - query.removed = wrapCallback(options.removed); - if (ordered) { - query.addedBefore = wrapCallback(options.addedBefore); - query.movedBefore = wrapCallback(options.movedBefore); - } - - if (!options._suppress_initial && !self.collection.paused) { - var results = query.results._map || query.results; - Object.keys(results).forEach(function (key) { - var doc = results[key]; - var fields = EJSON.clone(doc); - - delete fields._id; - if (ordered) - query.addedBefore(doc._id, self._projectionFn(fields), null); - query.added(doc._id, self._projectionFn(fields)); - }); - } - - var handle = new LocalCollection.ObserveHandle; - Object.assign(handle, { - collection: self.collection, - stop: function () { - if (self.reactive) - delete self.collection.queries[qid]; - } - }); - - if (self.reactive && Tracker.active) { - // XXX in many cases, the same observe will be recreated when - // the current autorun is rerun. we could save work by - // letting it linger across rerun and potentially get - // repurposed if the same observe is performed, using logic - // similar to that of Meteor.subscribe. - Tracker.onInvalidate(function () { - handle.stop(); - }); - } - // run the observe callbacks resulting from the initial contents - // before we leave the observe. - self.collection._observeQueue.drain(); - - return handle; + // Non-reactive queries call added[Before] and then never call anything + // else. + if (self.reactive) { + qid = self.collection.next_qid++; + self.collection.queries[qid] = query; } -}); + query.results = self._getRawObjects({ + ordered: ordered, distances: query.distances}); + if (self.collection.paused) + query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); + + // wrap callbacks we were passed. callbacks only fire when not paused and + // are never undefined + // Filters out blacklisted fields according to cursor's projection. + // XXX wrong place for this? + + // furthermore, callbacks enqueue until the operation we're working on is + // done. + var wrapCallback = function (f) { + if (!f) + return function () {}; + return function (/*args*/) { + var context = this; + var args = arguments; + + if (self.collection.paused) + return; + + self.collection._observeQueue.queueTask(function () { + f.apply(context, args); + }); + }; + }; + query.added = wrapCallback(options.added); + query.changed = wrapCallback(options.changed); + query.removed = wrapCallback(options.removed); + if (ordered) { + query.addedBefore = wrapCallback(options.addedBefore); + query.movedBefore = wrapCallback(options.movedBefore); + } + + if (!options._suppress_initial && !self.collection.paused) { + var results = query.results._map || query.results; + Object.keys(results).forEach(function (key) { + var doc = results[key]; + var fields = EJSON.clone(doc); + + delete fields._id; + if (ordered) + query.addedBefore(doc._id, self._projectionFn(fields), null); + query.added(doc._id, self._projectionFn(fields)); + }); + } + + var handle = new LocalCollection.ObserveHandle; + Object.assign(handle, { + collection: self.collection, + stop: function () { + if (self.reactive) + delete self.collection.queries[qid]; + } + }); + + if (self.reactive && Tracker.active) { + // XXX in many cases, the same observe will be recreated when + // the current autorun is rerun. we could save work by + // letting it linger across rerun and potentially get + // repurposed if the same observe is performed, using logic + // similar to that of Meteor.subscribe. + Tracker.onInvalidate(function () { + handle.stop(); + }); + } + // run the observe callbacks resulting from the initial contents + // before we leave the observe. + self.collection._observeQueue.drain(); + + return handle; +}; // Returns a collection of matching objects, but doesn't deep copy them. // @@ -570,684 +449,6 @@ LocalCollection.Cursor.prototype._depend = function (changers, _allow_unordered) } }; -// XXX possibly enforce that 'undefined' does not appear (we assume -// this in our handling of null and $exists) -LocalCollection.prototype.insert = function (doc, callback) { - var self = this; - doc = EJSON.clone(doc); - - assertHasValidFieldNames(doc); - - if (!doc.hasOwnProperty('_id')) { - // if you really want to use ObjectIDs, set this global. - // Mongo.Collection specifies its own ids and does not use this code. - doc._id = LocalCollection._useOID ? new MongoID.ObjectID() - : Random.id(); - } - var id = doc._id; - - if (self._docs.has(id)) - throw MinimongoError("Duplicate _id '" + id + "'"); - - self._saveOriginal(id, undefined); - self._docs.set(id, doc); - - var queriesToRecompute = []; - // trigger live queries that match - for (var qid in self.queries) { - var query = self.queries[qid]; - if (query.dirty) continue; - var matchResult = query.matcher.documentMatches(doc); - if (matchResult.result) { - if (query.distances && matchResult.distance !== undefined) - query.distances.set(id, matchResult.distance); - if (query.cursor.skip || query.cursor.limit) - queriesToRecompute.push(qid); - else - LocalCollection._insertInResults(query, doc); - } - } - - queriesToRecompute.forEach(function (qid) { - if (self.queries[qid]) - self._recomputeResults(self.queries[qid]); - }); - self._observeQueue.drain(); - - // Defer because the caller likely doesn't expect the callback to be run - // immediately. - if (callback) - Meteor.defer(function () { - callback(null, id); - }); - return id; -}; - -// Iterates over a subset of documents that could match selector; calls -// f(doc, id) on each of them. Specifically, if selector specifies -// specific _id's, it only looks at those. doc is *not* cloned: it is the -// same object that is in _docs. -LocalCollection.prototype._eachPossiblyMatchingDoc = function (selector, f) { - var self = this; - var specificIds = LocalCollection._idsMatchedBySelector(selector); - if (specificIds) { - for (var i = 0; i < specificIds.length; ++i) { - var id = specificIds[i]; - var doc = self._docs.get(id); - if (doc) { - var breakIfFalse = f(doc, id); - if (breakIfFalse === false) - break; - } - } - } else { - self._docs.forEach(f); - } -}; - -LocalCollection.prototype.remove = function (selector, callback) { - var self = this; - - // Easy special case: if we're not calling observeChanges callbacks and we're - // not saving originals and we got asked to remove everything, then just empty - // everything directly. - if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) { - var result = self._docs.size(); - self._docs.clear(); - Object.keys(self.queries).forEach(function (qid) { - var query = self.queries[qid]; - if (query.ordered) { - query.results = []; - } else { - query.results.clear(); - } - }); - if (callback) { - Meteor.defer(function () { - callback(null, result); - }); - } - return result; - } - - var matcher = new Minimongo.Matcher(selector); - var remove = []; - self._eachPossiblyMatchingDoc(selector, function (doc, id) { - if (matcher.documentMatches(doc).result) - remove.push(id); - }); - - var queriesToRecompute = []; - var queryRemove = []; - for (var i = 0; i < remove.length; i++) { - var removeId = remove[i]; - var removeDoc = self._docs.get(removeId); - Object.keys(self.queries).forEach(function (qid) { - var query = self.queries[qid]; - if (query.dirty) return; - - if (query.matcher.documentMatches(removeDoc).result) { - if (query.cursor.skip || query.cursor.limit) - queriesToRecompute.push(qid); - else - queryRemove.push({qid: qid, doc: removeDoc}); - } - }); - self._saveOriginal(removeId, removeDoc); - self._docs.remove(removeId); - } - - // run live query callbacks _after_ we've removed the documents. - queryRemove.forEach(function (remove) { - var query = self.queries[remove.qid]; - if (query) { - query.distances && query.distances.remove(remove.doc._id); - LocalCollection._removeFromResults(query, remove.doc); - } - }); - queriesToRecompute.forEach(function (qid) { - var query = self.queries[qid]; - if (query) - self._recomputeResults(query); - }); - self._observeQueue.drain(); - result = remove.length; - if (callback) - Meteor.defer(function () { - callback(null, result); - }); - return result; -}; - -// XXX atomicity: if multi is true, and one modification fails, do -// we rollback the whole operation, or what? -LocalCollection.prototype.update = function (selector, mod, options, callback) { - var self = this; - if (! callback && options instanceof Function) { - callback = options; - options = null; - } - if (!options) options = {}; - - var matcher = new Minimongo.Matcher(selector, true); - - // Save the original results of any query that we might need to - // _recomputeResults on, because _modifyAndNotify will mutate the objects in - // it. (We don't need to save the original results of paused queries because - // they already have a resultsSnapshot and we won't be diffing in - // _recomputeResults.) - var qidToOriginalResults = {}; - // We should only clone each document once, even if it appears in multiple queries - var docMap = new LocalCollection._IdMap; - var idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector); - - Object.keys(self.queries).forEach(function (qid) { - var query = self.queries[qid]; - if ((query.cursor.skip || query.cursor.limit) && ! self.paused) { - // Catch the case of a reactive `count()` on a cursor with skip - // or limit, which registers an unordered observe. This is a - // pretty rare case, so we just clone the entire result set with - // no optimizations for documents that appear in these result - // sets and other queries. - if (query.results instanceof LocalCollection._IdMap) { - qidToOriginalResults[qid] = query.results.clone(); - return; - } - - if (!(query.results instanceof Array)) { - throw new Error("Assertion failed: query.results not an array"); - } - - // Clones a document to be stored in `qidToOriginalResults` - // because it may be modified before the new and old result sets - // are diffed. But if we know exactly which document IDs we're - // going to modify, then we only need to clone those. - var memoizedCloneIfNeeded = function(doc) { - if (docMap.has(doc._id)) { - return docMap.get(doc._id); - } else { - var docToMemoize; - - if (idsMatchedBySelector && !idsMatchedBySelector.some(function(id) { - return EJSON.equals(id, doc._id); - })) { - docToMemoize = doc; - } else { - docToMemoize = EJSON.clone(doc); - } - - docMap.set(doc._id, docToMemoize); - return docToMemoize; - } - }; - - qidToOriginalResults[qid] = query.results.map(memoizedCloneIfNeeded); - } - }); - var recomputeQids = {}; - - var updateCount = 0; - - self._eachPossiblyMatchingDoc(selector, function (doc, id) { - var queryResult = matcher.documentMatches(doc); - if (queryResult.result) { - // XXX Should we save the original even if mod ends up being a no-op? - self._saveOriginal(id, doc); - self._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices); - ++updateCount; - if (!options.multi) - return false; // break - } - return true; - }); - - Object.keys(recomputeQids).forEach(function (qid) { - var query = self.queries[qid]; - if (query) - self._recomputeResults(query, qidToOriginalResults[qid]); - }); - self._observeQueue.drain(); - - // If we are doing an upsert, and we didn't modify any documents yet, then - // it's time to do an insert. Figure out what document we are inserting, and - // generate an id for it. - var insertedId; - if (updateCount === 0 && options.upsert) { - - let selectorModifier = LocalCollection._selectorIsId(selector) - ? { _id: selector } - : selector; - - selectorModifier = LocalCollection._removeDollarOperators(selectorModifier); - - const newDoc = {}; - if (selectorModifier._id) { - newDoc._id = selectorModifier._id; - delete selectorModifier._id; - } - - // This double _modify call is made to help work around an issue where collection - // upserts won't work properly, with nested properties (see issue #8631). - LocalCollection._modify(newDoc, {$set: selectorModifier}); - LocalCollection._modify(newDoc, mod, {isInsert: true}); - - if (! newDoc._id && options.insertedId) - newDoc._id = options.insertedId; - insertedId = self.insert(newDoc); - updateCount = 1; - } - - // Return the number of affected documents, or in the upsert case, an object - // containing the number of affected docs and the id of the doc that was - // inserted, if any. - var result; - if (options._returnObject) { - result = { - numberAffected: updateCount - }; - if (insertedId !== undefined) - result.insertedId = insertedId; - } else { - result = updateCount; - } - - if (callback) - Meteor.defer(function () { - callback(null, result); - }); - return result; -}; - -// A convenience wrapper on update. LocalCollection.upsert(sel, mod) is -// equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject: -// true }). -LocalCollection.prototype.upsert = function (selector, mod, options, callback) { - var self = this; - if (! callback && typeof options === "function") { - callback = options; - options = {}; - } - return self.update(selector, mod, Object.assign({}, options, { - upsert: true, - _returnObject: true - }), callback); -}; - -LocalCollection.prototype._modifyAndNotify = function ( - doc, mod, recomputeQids, arrayIndices) { - var self = this; - - var matched_before = {}; - for (var qid in self.queries) { - var query = self.queries[qid]; - if (query.dirty) continue; - - if (query.ordered) { - matched_before[qid] = query.matcher.documentMatches(doc).result; - } else { - // Because we don't support skip or limit (yet) in unordered queries, we - // can just do a direct lookup. - matched_before[qid] = query.results.has(doc._id); - } - } - - var old_doc = EJSON.clone(doc); - - LocalCollection._modify(doc, mod, {arrayIndices: arrayIndices}); - - for (qid in self.queries) { - query = self.queries[qid]; - if (query.dirty) continue; - - var before = matched_before[qid]; - var afterMatch = query.matcher.documentMatches(doc); - var after = afterMatch.result; - if (after && query.distances && afterMatch.distance !== undefined) - query.distances.set(doc._id, afterMatch.distance); - - if (query.cursor.skip || query.cursor.limit) { - // We need to recompute any query where the doc may have been in the - // cursor's window either before or after the update. (Note that if skip - // or limit is set, "before" and "after" being true do not necessarily - // mean that the document is in the cursor's output after skip/limit is - // applied... but if they are false, then the document definitely is NOT - // in the output. So it's safe to skip recompute if neither before or - // after are true.) - if (before || after) - recomputeQids[qid] = true; - } else if (before && !after) { - LocalCollection._removeFromResults(query, doc); - } else if (!before && after) { - LocalCollection._insertInResults(query, doc); - } else if (before && after) { - LocalCollection._updateInResults(query, doc, old_doc); - } - } -}; - -// XXX the sorted-query logic below is laughably inefficient. we'll -// need to come up with a better datastructure for this. -// -// XXX the logic for observing with a skip or a limit is even more -// laughably inefficient. we recompute the whole results every time! - -LocalCollection._insertInResults = function (query, doc) { - var fields = EJSON.clone(doc); - delete fields._id; - if (query.ordered) { - if (!query.sorter) { - query.addedBefore(doc._id, query.projectionFn(fields), null); - query.results.push(doc); - } else { - var i = LocalCollection._insertInSortedList( - query.sorter.getComparator({distances: query.distances}), - query.results, doc); - var next = query.results[i+1]; - if (next) - next = next._id; - else - next = null; - query.addedBefore(doc._id, query.projectionFn(fields), next); - } - query.added(doc._id, query.projectionFn(fields)); - } else { - query.added(doc._id, query.projectionFn(fields)); - query.results.set(doc._id, doc); - } -}; - -LocalCollection._removeFromResults = function (query, doc) { - if (query.ordered) { - var i = LocalCollection._findInOrderedResults(query, doc); - query.removed(doc._id); - query.results.splice(i, 1); - } else { - var id = doc._id; // in case callback mutates doc - query.removed(doc._id); - query.results.remove(id); - } -}; - -LocalCollection._updateInResults = function (query, doc, old_doc) { - if (!EJSON.equals(doc._id, old_doc._id)) - throw new Error("Can't change a doc's _id while updating"); - var projectionFn = query.projectionFn; - var changedFields = DiffSequence.makeChangedFields( - projectionFn(doc), projectionFn(old_doc)); - - if (!query.ordered) { - if (Object.keys(changedFields).length) { - query.changed(doc._id, changedFields); - query.results.set(doc._id, doc); - } - return; - } - - var orig_idx = LocalCollection._findInOrderedResults(query, doc); - - if (Object.keys(changedFields).length) - query.changed(doc._id, changedFields); - if (!query.sorter) - return; - - // just take it out and put it back in again, and see if the index - // changes - query.results.splice(orig_idx, 1); - var new_idx = LocalCollection._insertInSortedList( - query.sorter.getComparator({distances: query.distances}), - query.results, doc); - if (orig_idx !== new_idx) { - var next = query.results[new_idx+1]; - if (next) - next = next._id; - else - next = null; - query.movedBefore && query.movedBefore(doc._id, next); - } -}; - -// Recomputes the results of a query and runs observe callbacks for the -// difference between the previous results and the current results (unless -// paused). Used for skip/limit queries. -// -// When this is used by insert or remove, it can just use query.results for the -// old results (and there's no need to pass in oldResults), because these -// operations don't mutate the documents in the collection. Update needs to pass -// in an oldResults which was deep-copied before the modifier was applied. -// -// oldResults is guaranteed to be ignored if the query is not paused. -LocalCollection.prototype._recomputeResults = function (query, oldResults) { - var self = this; - if (self.paused) { - // There's no reason to recompute the results now as we're still paused. - // By flagging the query as "dirty", the recompute will be performed - // when resumeObservers is called. - query.dirty = true; - return; - } - - if (! self.paused && ! oldResults) - oldResults = query.results; - if (query.distances) - query.distances.clear(); - query.results = query.cursor._getRawObjects({ - ordered: query.ordered, distances: query.distances}); - - if (! self.paused) { - LocalCollection._diffQueryChanges( - query.ordered, oldResults, query.results, query, - { projectionFn: query.projectionFn }); - } -}; - - -LocalCollection._findInOrderedResults = function (query, doc) { - if (!query.ordered) - throw new Error("Can't call _findInOrderedResults on unordered query"); - for (var i = 0; i < query.results.length; i++) - if (query.results[i] === doc) - return i; - throw Error("object missing from query"); -}; - -// This binary search puts a value between any equal values, and the first -// lesser value. -LocalCollection._binarySearch = function (cmp, array, value) { - var first = 0, rangeLength = array.length; - - while (rangeLength > 0) { - var halfRange = Math.floor(rangeLength/2); - if (cmp(value, array[first + halfRange]) >= 0) { - first += halfRange + 1; - rangeLength -= halfRange + 1; - } else { - rangeLength = halfRange; - } - } - return first; -}; - -LocalCollection._insertInSortedList = function (cmp, array, value) { - if (array.length === 0) { - array.push(value); - return 0; - } - - var idx = LocalCollection._binarySearch(cmp, array, value); - array.splice(idx, 0, value); - return idx; -}; - -// To track what documents are affected by a piece of code, call saveOriginals() -// before it and retrieveOriginals() after it. retrieveOriginals returns an -// object whose keys are the ids of the documents that were affected since the -// call to saveOriginals(), and the values are equal to the document's contents -// at the time of saveOriginals. (In the case of an inserted document, undefined -// is the value.) You must alternate between calls to saveOriginals() and -// retrieveOriginals(). -LocalCollection.prototype.saveOriginals = function () { - var self = this; - if (self._savedOriginals) - throw new Error("Called saveOriginals twice without retrieveOriginals"); - self._savedOriginals = new LocalCollection._IdMap; -}; -LocalCollection.prototype.retrieveOriginals = function () { - var self = this; - if (!self._savedOriginals) - throw new Error("Called retrieveOriginals without saveOriginals"); - - var originals = self._savedOriginals; - self._savedOriginals = null; - return originals; -}; - -LocalCollection.prototype._saveOriginal = function (id, doc) { - var self = this; - // Are we even trying to save originals? - if (!self._savedOriginals) - return; - // Have we previously mutated the original (and so 'doc' is not actually - // original)? (Note the 'has' check rather than truth: we store undefined - // here for inserted docs!) - if (self._savedOriginals.has(id)) - return; - self._savedOriginals.set(id, EJSON.clone(doc)); -}; - -// Pause the observers. No callbacks from observers will fire until -// 'resumeObservers' is called. -LocalCollection.prototype.pauseObservers = function () { - // No-op if already paused. - if (this.paused) - return; - - // Set the 'paused' flag such that new observer messages don't fire. - this.paused = true; - - // Take a snapshot of the query results for each query. - for (var qid in this.queries) { - var query = this.queries[qid]; - - query.resultsSnapshot = EJSON.clone(query.results); - } -}; - -// Resume the observers. Observers immediately receive change -// notifications to bring them to the current state of the -// database. Note that this is not just replaying all the changes that -// happened during the pause, it is a smarter 'coalesced' diff. -LocalCollection.prototype.resumeObservers = function () { - var self = this; - // No-op if not paused. - if (!this.paused) - return; - - // Unset the 'paused' flag. Make sure to do this first, otherwise - // observer methods won't actually fire when we trigger them. - this.paused = false; - - for (var qid in this.queries) { - var query = self.queries[qid]; - if (query.dirty) { - query.dirty = false; - // re-compute results will perform `LocalCollection._diffQueryChanges` automatically. - self._recomputeResults(query, query.resultsSnapshot); - } else { - // Diff the current results against the snapshot and send to observers. - // pass the query object for its observer callbacks. - LocalCollection._diffQueryChanges( - query.ordered, query.resultsSnapshot, query.results, query, - {projectionFn: query.projectionFn}); - } - query.resultsSnapshot = null; - } - self._observeQueue.drain(); -}; - -// Wrap a transform function to return objects that have the _id field -// of the untransformed document. This ensures that subsystems such as -// the observe-sequence package that call `observe` can keep track of -// the documents identities. -// -// - Require that it returns objects -// - If the return value has an _id field, verify that it matches the -// original _id field -// - If the return value doesn't have an _id field, add it back. -LocalCollection.wrapTransform = function (transform) { - if (! transform) - return null; - - // No need to doubly-wrap transforms. - if (transform.__wrappedTransform__) - return transform; - - var wrapped = function (doc) { - if (!doc.hasOwnProperty('_id')) { - // XXX do we ever have a transform on the oplog's collection? because that - // collection has no _id. - throw new Error("can only transform documents with _id"); - } - - var id = doc._id; - // XXX consider making tracker a weak dependency and checking Package.tracker here - var transformed = Tracker.nonreactive(function () { - return transform(doc); - }); - - if (!LocalCollection._isPlainObject(transformed)) { - throw new Error("transform must return object"); - } - - if (transformed.hasOwnProperty('_id')) { - if (!EJSON.equals(transformed._id, id)) { - throw new Error("transformed document can't have different _id"); - } - } else { - transformed._id = id; - } - return transformed; - }; - wrapped.__wrappedTransform__ = true; - return wrapped; -}; - -// XXX maybe this should be EJSON.isObject, though EJSON doesn't know about -// RegExp -// XXX note that _type(undefined) === 3!!!! -LocalCollection._isPlainObject = function (x) { - return x && LocalCollection._f._type(x) === 3; -}; - -function objectOnlyHasDollarKeys (object) { - const keys = Object.keys(object); - return keys.length > 0 && keys.every(key => key.charAt(0) === '$'); -}; - -// When performing an upsert, the incoming selector object can be re-used as -// the upsert modifier object, as long as Mongo query and projection -// operators (prefixed with a $ character) are removed from the newly -// created modifier object. This function attempts to strip all $ based Mongo -// operators when creating the upsert modifier object. -// NOTE: There is a known issue here in that some Mongo $ based opeartors -// should not actually be stripped. -// See https://github.com/meteor/meteor/issues/8806. -LocalCollection._removeDollarOperators = (selector) => { - let cleansed = {}; - Object.keys(selector).forEach((key) => { - const value = selector[key]; - if (key.charAt(0) !== '$' && !objectOnlyHasDollarKeys(value)) { - if (value !== null - && value.constructor - && Object.getPrototypeOf(value) === Object.prototype) { - cleansed[key] = LocalCollection._removeDollarOperators(value); - } else { - cleansed[key] = value; - } - } - }); - return cleansed; -}; - // Give a sort spec, which can be in any of these forms: // {"key1": 1, "key2": -1} // [["key1", "asc"], ["key2", "desc"]] @@ -1670,947 +871,3 @@ function composeComparators (comparatorArray) { return 0; }; } - -// Knows how to compile a fields projection to a predicate function. -// @returns - Function: a closure that filters out an object according to the -// fields projection rules: -// @param obj - Object: MongoDB-styled document -// @returns - Object: a document with the fields filtered out -// according to projection rules. Doesn't retain subfields -// of passed argument. -LocalCollection._compileProjection = function (fields) { - LocalCollection._checkSupportedProjection(fields); - - var _idProjection = fields._id === undefined ? true : fields._id; - var details = projectionDetails(fields); - - // returns transformed doc according to ruleTree - var transform = function (doc, ruleTree) { - // Special case for "sets" - if (Array.isArray(doc)) - return doc.map(function (subdoc) { return transform(subdoc, ruleTree); }); - - var res = details.including ? {} : EJSON.clone(doc); - Object.keys(ruleTree).forEach(function (key) { - var rule = ruleTree[key]; - if (!doc.hasOwnProperty(key)) - return; - if (rule === Object(rule)) { - // For sub-objects/subsets we branch - if (doc[key] === Object(doc[key])) - res[key] = transform(doc[key], rule); - // Otherwise we don't even touch this subfield - } else if (details.including) - res[key] = EJSON.clone(doc[key]); - else - delete res[key]; - }); - - return res; - }; - - return function (obj) { - var res = transform(obj, details.tree); - - if (_idProjection && obj.hasOwnProperty('_id')) - res._id = obj._id; - if (!_idProjection && res.hasOwnProperty('_id')) - delete res._id; - return res; - }; -}; - -// Traverses the keys of passed projection and constructs a tree where all -// leaves are either all True or all False -// @returns Object: -// - tree - Object - tree representation of keys involved in projection -// (exception for '_id' as it is a special case handled separately) -// - including - Boolean - "take only certain fields" type of projection -function projectionDetails (fields) { - // Find the non-_id keys (_id is handled specially because it is included unless - // explicitly excluded). Sort the keys, so that our code to detect overlaps - // like 'foo' and 'foo.bar' can assume that 'foo' comes first. - var fieldsKeys = Object.keys(fields).sort(); - - // If _id is the only field in the projection, do not remove it, since it is - // required to determine if this is an exclusion or exclusion. Also keep an - // inclusive _id, since inclusive _id follows the normal rules about mixing - // inclusive and exclusive fields. If _id is not the only field in the - // projection and is exclusive, remove it so it can be handled later by a - // special case, since exclusive _id is always allowed. - if (fieldsKeys.length > 0 && - !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && - !(fieldsKeys.includes('_id') && fields['_id'])) - fieldsKeys = fieldsKeys.filter(function (key) { return key !== '_id'; }); - - var including = null; // Unknown - - fieldsKeys.forEach(function (keyPath) { - var rule = !!fields[keyPath]; - if (including === null) - including = rule; - if (including !== rule) - // This error message is copied from MongoDB shell - throw MinimongoError("You cannot currently mix including and excluding fields."); - }); - - - var projectionRulesTree = pathsToTree( - fieldsKeys, - function (path) { return including; }, - function (node, path, fullPath) { - // Check passed projection fields' keys: If you have two rules such as - // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If - // that happens, there is a probability you are doing something wrong, - // framework should notify you about such mistake earlier on cursor - // compilation step than later during runtime. Note, that real mongo - // doesn't do anything about it and the later rule appears in projection - // project, more priority it takes. - // - // Example, assume following in mongo shell: - // > db.coll.insert({ a: { b: 23, c: 44 } }) - // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) - // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } } - // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) - // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } - // - // Note, how second time the return set of keys is different. - - var currentPath = fullPath; - var anotherPath = path; - throw MinimongoError("both " + currentPath + " and " + anotherPath + - " found in fields option, using both of them may trigger " + - "unexpected behavior. Did you mean to use only one of them?"); - }); - - return { - tree: projectionRulesTree, - including: including - }; -} - -// paths - Array: list of mongo style paths -// newLeafFn - Function: of form function(path) should return a scalar value to -// put into list created for that path -// conflictFn - Function: of form function(node, path, fullPath) is called -// when building a tree path for 'fullPath' node on -// 'path' was already a leaf with a value. Must return a -// conflict resolution. -// initial tree - Optional Object: starting tree. -// @returns - Object: tree represented as a set of nested objects -function pathsToTree (paths, newLeafFn, conflictFn, tree) { - tree = tree || {}; - paths.forEach(function (keyPath) { - var treePos = tree; - var pathArr = keyPath.split('.'); - - // use .every just for iteration with break - var success = pathArr.slice(0, -1).every(function (key, idx) { - if (!treePos.hasOwnProperty(key)) - treePos[key] = {}; - else if (treePos[key] !== Object(treePos[key])) { - treePos[key] = conflictFn(treePos[key], - pathArr.slice(0, idx + 1).join('.'), - keyPath); - // break out of loop if we are failing for this path - if (treePos[key] !== Object(treePos[key])) - return false; - } - - treePos = treePos[key]; - return true; - }); - - if (success) { - var lastKey = pathArr[pathArr.length - 1]; - if (!treePos.hasOwnProperty(lastKey)) - treePos[lastKey] = newLeafFn(keyPath); - else - treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); - } - }); - - return tree; -} - -LocalCollection._checkSupportedProjection = function (fields) { - if (fields !== Object(fields) || Array.isArray(fields)) - throw MinimongoError("fields option must be an object"); - - Object.keys(fields).forEach(function (keyPath) { - var val = fields[keyPath]; - if (keyPath.split('.').includes('$')) - throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); - if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) - throw MinimongoError("Minimongo doesn't support operators in projections yet."); - if (![1, 0, true, false].includes(val)) - throw MinimongoError("Projection values should be one of 1, 0, true, or false"); - }); -}; - -// XXX need a strategy for passing the binding of $ into this -// function, from the compiled selector -// -// maybe just {key.up.to.just.before.dollarsign: array_index} -// -// XXX atomicity: if one modification fails, do we roll back the whole -// change? -// -// options: -// - isInsert is set when _modify is being called to compute the document to -// insert as part of an upsert operation. We use this primarily to figure -// out when to set the fields in $setOnInsert, if present. -LocalCollection._modify = function (doc, mod, options) { - options = options || {}; - if (!LocalCollection._isPlainObject(mod)) - throw MinimongoError("Modifier must be an object"); - - // Make sure the caller can't mutate our data structures. - mod = EJSON.clone(mod); - - var isModifier = isOperatorObject(mod); - - var newDoc; - - if (!isModifier) { - if (mod._id && !EJSON.equals(doc._id, mod._id)) - throw MinimongoError("Cannot change the _id of a document"); - - // replace the whole document - assertHasValidFieldNames(mod); - newDoc = mod; - } else { - // apply modifiers to the doc. - newDoc = EJSON.clone(doc); - - Object.keys(mod).forEach(function (op) { - var operand = mod[op]; - var modFunc = MODIFIERS[op]; - // Treat $setOnInsert as $set if this is an insert. - if (options.isInsert && op === '$setOnInsert') - modFunc = MODIFIERS['$set']; - if (!modFunc) - throw MinimongoError("Invalid modifier specified " + op); - Object.keys(operand).forEach(function (keypath) { - var arg = operand[keypath]; - if (keypath === '') { - throw MinimongoError("An empty update path is not valid."); - } - - if (keypath === '_id' && op !== '$setOnInsert') { - throw MinimongoError("Mod on _id not allowed"); - } - - var keyparts = keypath.split('.'); - - if (!keyparts.every(Boolean)) { - throw MinimongoError( - "The update path '" + keypath + - "' contains an empty field name, which is not allowed."); - } - - var noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); - var forbidArray = (op === "$rename"); - var target = findModTarget(newDoc, keyparts, { - noCreate: NO_CREATE_MODIFIERS[op], - forbidArray: (op === "$rename"), - arrayIndices: options.arrayIndices - }); - var field = keyparts.pop(); - modFunc(target, field, arg, keypath, newDoc); - }); - }); - } - - // move new document into place. - Object.keys(doc).forEach(function (k) { - // Note: this used to be for (var k in doc) however, this does not - // work right in Opera. Deleting from a doc while iterating over it - // would sometimes cause opera to skip some keys. - if (k !== '_id') - delete doc[k]; - }); - Object.keys(newDoc).forEach(function (k) { - doc[k] = newDoc[k]; - }); -}; - -// for a.b.c.2.d.e, keyparts should be ['a', 'b', 'c', '2', 'd', 'e'], -// and then you would operate on the 'e' property of the returned -// object. -// -// if options.noCreate is falsey, creates intermediate levels of -// structure as necessary, like mkdir -p (and raises an exception if -// that would mean giving a non-numeric property to an array.) if -// options.noCreate is true, return undefined instead. -// -// may modify the last element of keyparts to signal to the caller that it needs -// to use a different value to index into the returned object (for example, -// ['a', '01'] -> ['a', 1]). -// -// if forbidArray is true, return null if the keypath goes through an array. -// -// if options.arrayIndices is set, use its first element for the (first) '$' in -// the path. -function findModTarget (doc, keyparts, options) { - options = options || {}; - var usedArrayIndex = false; - for (var i = 0; i < keyparts.length; i++) { - var last = (i === keyparts.length - 1); - var keypart = keyparts[i]; - var indexable = isIndexable(doc); - if (!indexable) { - if (options.noCreate) - return undefined; - var e = MinimongoError( - "cannot use the part '" + keypart + "' to traverse " + doc); - e.setPropertyError = true; - throw e; - } - if (doc instanceof Array) { - if (options.forbidArray) - return null; - if (keypart === '$') { - if (usedArrayIndex) - throw MinimongoError("Too many positional (i.e. '$') elements"); - if (!options.arrayIndices || !options.arrayIndices.length) { - throw MinimongoError("The positional operator did not find the " + - "match needed from the query"); - } - keypart = options.arrayIndices[0]; - usedArrayIndex = true; - } else if (isNumericKey(keypart)) { - keypart = parseInt(keypart); - } else { - if (options.noCreate) - return undefined; - throw MinimongoError( - "can't append to array using string field name [" - + keypart + "]"); - } - if (last) - // handle 'a.01' - keyparts[i] = keypart; - if (options.noCreate && keypart >= doc.length) - return undefined; - while (doc.length < keypart) - doc.push(null); - if (!last) { - if (doc.length === keypart) - doc.push({}); - else if (typeof doc[keypart] !== "object") - throw MinimongoError("can't modify field '" + keyparts[i + 1] + - "' of list value " + JSON.stringify(doc[keypart])); - } - } else { - assertIsValidFieldName(keypart); - if (!(keypart in doc)) { - if (options.noCreate) - return undefined; - if (!last) - doc[keypart] = {}; - } - } - - if (last) - return doc; - doc = doc[keypart]; - } - - // notreached -} - -const NO_CREATE_MODIFIERS = { - $unset: true, - $pop: true, - $rename: true, - $pull: true, - $pullAll: true -}; - -const MODIFIERS = { - $currentDate: function (target, field, arg) { - if (typeof arg === "object" && arg.hasOwnProperty("$type")) { - if (arg.$type !== "date") { - throw MinimongoError( - "Minimongo does currently only support the date type " + - "in $currentDate modifiers", - { field }); - } - } else if (arg !== true) { - throw MinimongoError("Invalid $currentDate modifier", { field }); - } - target[field] = new Date(); - }, - $min: function (target, field, arg) { - if (typeof arg !== "number") { - throw MinimongoError("Modifier $min allowed for numbers only", { field }); - } - if (field in target) { - if (typeof target[field] !== "number") { - throw MinimongoError( - "Cannot apply $min modifier to non-number", { field }); - } - if (target[field] > arg) { - target[field] = arg; - } - } else { - target[field] = arg; - } - }, - $max: function (target, field, arg) { - if (typeof arg !== "number") { - throw MinimongoError("Modifier $max allowed for numbers only", { field }); - } - if (field in target) { - if (typeof target[field] !== "number") { - throw MinimongoError( - "Cannot apply $max modifier to non-number", { field }); - } - if (target[field] < arg) { - target[field] = arg; - } - } else { - target[field] = arg; - } - }, - $inc: function (target, field, arg) { - if (typeof arg !== "number") - throw MinimongoError("Modifier $inc allowed for numbers only", { field }); - if (field in target) { - if (typeof target[field] !== "number") - throw MinimongoError( - "Cannot apply $inc modifier to non-number", { field }); - target[field] += arg; - } else { - target[field] = arg; - } - }, - $set: function (target, field, arg) { - if (target !== Object(target)) { // not an array or an object - var e = MinimongoError( - "Cannot set property on non-object field", { field }); - e.setPropertyError = true; - throw e; - } - if (target === null) { - var e = MinimongoError("Cannot set property on null", { field }); - e.setPropertyError = true; - throw e; - } - assertHasValidFieldNames(arg); - target[field] = arg; - }, - $setOnInsert: function (target, field, arg) { - // converted to `$set` in `_modify` - }, - $unset: function (target, field, arg) { - if (target !== undefined) { - if (target instanceof Array) { - if (field in target) - target[field] = null; - } else - delete target[field]; - } - }, - $push: function (target, field, arg) { - if (target[field] === undefined) - target[field] = []; - if (!(target[field] instanceof Array)) - throw MinimongoError( - "Cannot apply $push modifier to non-array", { field }); - - if (!(arg && arg.$each)) { - // Simple mode: not $each - assertHasValidFieldNames(arg); - target[field].push(arg); - return; - } - - // Fancy mode: $each (and maybe $slice and $sort and $position) - var toPush = arg.$each; - if (!(toPush instanceof Array)) - throw MinimongoError("$each must be an array", { field }); - assertHasValidFieldNames(toPush); - - // Parse $position - var position = undefined; - if ('$position' in arg) { - if (typeof arg.$position !== "number") - throw MinimongoError("$position must be a numeric value", { field }); - // XXX should check to make sure integer - if (arg.$position < 0) - throw MinimongoError( - "$position in $push must be zero or positive", { field }); - position = arg.$position; - } - - // Parse $slice. - var slice = undefined; - if ('$slice' in arg) { - if (typeof arg.$slice !== "number") - throw MinimongoError("$slice must be a numeric value", { field }); - // XXX should check to make sure integer - slice = arg.$slice; - } - - // Parse $sort. - var sortFunction = undefined; - if (arg.$sort) { - if (slice === undefined) - throw MinimongoError("$sort requires $slice to be present", { field }); - // XXX this allows us to use a $sort whose value is an array, but that's - // actually an extension of the Node driver, so it won't work - // server-side. Could be confusing! - // XXX is it correct that we don't do geo-stuff here? - sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); - for (var i = 0; i < toPush.length; i++) { - if (LocalCollection._f._type(toPush[i]) !== 3) { - throw MinimongoError("$push like modifiers using $sort " + - "require all elements to be objects", { field }); - } - } - } - - // Actually push. - if (position === undefined) { - for (var j = 0; j < toPush.length; j++) - target[field].push(toPush[j]); - } else { - var spliceArguments = [position, 0]; - for (var j = 0; j < toPush.length; j++) - spliceArguments.push(toPush[j]); - Array.prototype.splice.apply(target[field], spliceArguments); - } - - // Actually sort. - if (sortFunction) - target[field].sort(sortFunction); - - // Actually slice. - if (slice !== undefined) { - if (slice === 0) - target[field] = []; // differs from Array.slice! - else if (slice < 0) - target[field] = target[field].slice(slice); - else - target[field] = target[field].slice(0, slice); - } - }, - $pushAll: function (target, field, arg) { - if (!(typeof arg === "object" && arg instanceof Array)) - throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); - assertHasValidFieldNames(arg); - var x = target[field]; - if (x === undefined) - target[field] = arg; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $pushAll modifier to non-array", { field }); - else { - for (var i = 0; i < arg.length; i++) - x.push(arg[i]); - } - }, - $addToSet: function (target, field, arg) { - var isEach = false; - if (typeof arg === "object") { - //check if first key is '$each' - const keys = Object.keys(arg); - if (keys[0] === "$each"){ - isEach = true; - } - } - var values = isEach ? arg["$each"] : [arg]; - assertHasValidFieldNames(values); - var x = target[field]; - if (x === undefined) - target[field] = values; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $addToSet modifier to non-array", { field }); - else { - values.forEach(function (value) { - for (var i = 0; i < x.length; i++) - if (LocalCollection._f._equal(value, x[i])) - return; - x.push(value); - }); - } - }, - $pop: function (target, field, arg) { - if (target === undefined) - return; - var x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $pop modifier to non-array", { field }); - else { - if (typeof arg === 'number' && arg < 0) - x.splice(0, 1); - else - x.pop(); - } - }, - $pull: function (target, field, arg) { - if (target === undefined) - return; - var x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $pull/pullAll modifier to non-array", { field }); - else { - var out = []; - if (arg != null && typeof arg === "object" && !(arg instanceof Array)) { - // XXX would be much nicer to compile this once, rather than - // for each document we modify.. but usually we're not - // modifying that many documents, so we'll let it slide for - // now - - // XXX Minimongo.Matcher isn't up for the job, because we need - // to permit stuff like {$pull: {a: {$gt: 4}}}.. something - // like {$gt: 4} is not normally a complete selector. - // same issue as $elemMatch possibly? - var matcher = new Minimongo.Matcher(arg); - for (var i = 0; i < x.length; i++) - if (!matcher.documentMatches(x[i]).result) - out.push(x[i]); - } else { - for (var i = 0; i < x.length; i++) - if (!LocalCollection._f._equal(x[i], arg)) - out.push(x[i]); - } - target[field] = out; - } - }, - $pullAll: function (target, field, arg) { - if (!(typeof arg === "object" && arg instanceof Array)) - throw MinimongoError( - "Modifier $pushAll/pullAll allowed for arrays only", { field }); - if (target === undefined) - return; - var x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) - throw MinimongoError( - "Cannot apply $pull/pullAll modifier to non-array", { field }); - else { - var out = []; - for (var i = 0; i < x.length; i++) { - var exclude = false; - for (var j = 0; j < arg.length; j++) { - if (LocalCollection._f._equal(x[i], arg[j])) { - exclude = true; - break; - } - } - if (!exclude) - out.push(x[i]); - } - target[field] = out; - } - }, - $rename: function (target, field, arg, keypath, doc) { - if (keypath === arg) - // no idea why mongo has this restriction.. - throw MinimongoError("$rename source must differ from target", { field }); - if (target === null) - throw MinimongoError("$rename source field invalid", { field }); - if (typeof arg !== "string") - throw MinimongoError("$rename target must be a string", { field }); - if (arg.indexOf('\0') > -1) { - // Null bytes are not allowed in Mongo field names - // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names - throw MinimongoError( - "The 'to' field for $rename cannot contain an embedded null byte", - { field }); - } - if (target === undefined) - return; - var v = target[field]; - delete target[field]; - - var keyparts = arg.split('.'); - var target2 = findModTarget(doc, keyparts, {forbidArray: true}); - if (target2 === null) - throw MinimongoError("$rename target field invalid", { field }); - var field2 = keyparts.pop(); - target2[field2] = v; - }, - $bit: function (target, field, arg) { - // XXX mongo only supports $bit on integers, and we only support - // native javascript numbers (doubles) so far, so we can't support $bit - throw MinimongoError("$bit is not supported", { field }); - } -}; - -// ordered: bool. -// old_results and new_results: collections of documents. -// if ordered, they are arrays. -// if unordered, they are IdMaps -LocalCollection._diffQueryChanges = function (ordered, oldResults, newResults, observer, options) { - return DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options); -}; - -LocalCollection._diffQueryUnorderedChanges = function (oldResults, newResults, observer, options) { - return DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options); -}; - - -LocalCollection._diffQueryOrderedChanges = function (oldResults, newResults, observer, options) { - return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options); -}; - -LocalCollection._diffObjects = function (left, right, callbacks) { - return DiffSequence.diffObjects(left, right, callbacks); -}; -LocalCollection._IdMap = function () { - var self = this; - IdMap.call(self, MongoID.idStringify, MongoID.idParse); -}; - -Meteor._inherits(LocalCollection._IdMap, IdMap); - -// XXX maybe move these into another ObserveHelpers package or something - -// _CachingChangeObserver is an object which receives observeChanges callbacks -// and keeps a cache of the current cursor state up to date in self.docs. Users -// of this class should read the docs field but not modify it. You should pass -// the "applyChange" field as the callbacks to the underlying observeChanges -// call. Optionally, you can specify your own observeChanges callbacks which are -// invoked immediately before the docs field is updated; this object is made -// available as `this` to those callbacks. -LocalCollection._CachingChangeObserver = function (options) { - var self = this; - options = options || {}; - - var orderedFromCallbacks = options.callbacks && - LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); - if (options.hasOwnProperty('ordered')) { - self.ordered = options.ordered; - if (options.callbacks && options.ordered !== orderedFromCallbacks) - throw Error("ordered option doesn't match callbacks"); - } else if (options.callbacks) { - self.ordered = orderedFromCallbacks; - } else { - throw Error("must provide ordered or callbacks"); - } - var callbacks = options.callbacks || {}; - - if (self.ordered) { - self.docs = new OrderedDict(MongoID.idStringify); - self.applyChange = { - addedBefore: function (id, fields, before) { - var doc = EJSON.clone(fields); - doc._id = id; - callbacks.addedBefore && callbacks.addedBefore.call( - self, id, fields, before); - // This line triggers if we provide added with movedBefore. - callbacks.added && callbacks.added.call(self, id, fields); - // XXX could `before` be a falsy ID? Technically - // idStringify seems to allow for them -- though - // OrderedDict won't call stringify on a falsy arg. - self.docs.putBefore(id, doc, before || null); - }, - movedBefore: function (id, before) { - var doc = self.docs.get(id); - callbacks.movedBefore && callbacks.movedBefore.call(self, id, before); - self.docs.moveBefore(id, before || null); - } - }; - } else { - self.docs = new LocalCollection._IdMap; - self.applyChange = { - added: function (id, fields) { - var doc = EJSON.clone(fields); - callbacks.added && callbacks.added.call(self, id, fields); - doc._id = id; - self.docs.set(id, doc); - } - }; - } - - // The methods in _IdMap and OrderedDict used by these callbacks are - // identical. - self.applyChange.changed = function (id, fields) { - var doc = self.docs.get(id); - if (!doc) - throw new Error("Unknown id for changed: " + id); - callbacks.changed && callbacks.changed.call( - self, id, EJSON.clone(fields)); - DiffSequence.applyChanges(doc, fields); - }; - self.applyChange.removed = function (id) { - callbacks.removed && callbacks.removed.call(self, id); - self.docs.remove(id); - }; -}; - -LocalCollection._observeFromObserveChanges = function (cursor, observeCallbacks) { - var transform = cursor.getTransform() || function (doc) {return doc;}; - var suppressed = !!observeCallbacks._suppress_initial; - - var observeChangesCallbacks; - if (LocalCollection._observeCallbacksAreOrdered(observeCallbacks)) { - // The "_no_indices" option sets all index arguments to -1 and skips the - // linear scans required to generate them. This lets observers that don't - // need absolute indices benefit from the other features of this API -- - // relative order, transforms, and applyChanges -- without the speed hit. - var indices = !observeCallbacks._no_indices; - observeChangesCallbacks = { - addedBefore: function (id, fields, before) { - var self = this; - if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) - return; - var doc = transform(Object.assign(fields, {_id: id})); - if (observeCallbacks.addedAt) { - var index = indices - ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; - observeCallbacks.addedAt(doc, index, before); - } else { - observeCallbacks.added(doc); - } - }, - changed: function (id, fields) { - var self = this; - if (!(observeCallbacks.changedAt || observeCallbacks.changed)) - return; - var doc = EJSON.clone(self.docs.get(id)); - if (!doc) - throw new Error("Unknown id for changed: " + id); - var oldDoc = transform(EJSON.clone(doc)); - DiffSequence.applyChanges(doc, fields); - doc = transform(doc); - if (observeCallbacks.changedAt) { - var index = indices ? self.docs.indexOf(id) : -1; - observeCallbacks.changedAt(doc, oldDoc, index); - } else { - observeCallbacks.changed(doc, oldDoc); - } - }, - movedBefore: function (id, before) { - var self = this; - if (!observeCallbacks.movedTo) - return; - var from = indices ? self.docs.indexOf(id) : -1; - - var to = indices - ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; - // When not moving backwards, adjust for the fact that removing the - // document slides everything back one slot. - if (to > from) - --to; - observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))), - from, to, before || null); - }, - removed: function (id) { - var self = this; - if (!(observeCallbacks.removedAt || observeCallbacks.removed)) - return; - // technically maybe there should be an EJSON.clone here, but it's about - // to be removed from self.docs! - var doc = transform(self.docs.get(id)); - if (observeCallbacks.removedAt) { - var index = indices ? self.docs.indexOf(id) : -1; - observeCallbacks.removedAt(doc, index); - } else { - observeCallbacks.removed(doc); - } - } - }; - } else { - observeChangesCallbacks = { - added: function (id, fields) { - if (!suppressed && observeCallbacks.added) { - var doc = Object.assign(fields, {_id: id}); - observeCallbacks.added(transform(doc)); - } - }, - changed: function (id, fields) { - var self = this; - if (observeCallbacks.changed) { - var oldDoc = self.docs.get(id); - var doc = EJSON.clone(oldDoc); - DiffSequence.applyChanges(doc, fields); - observeCallbacks.changed(transform(doc), - transform(EJSON.clone(oldDoc))); - } - }, - removed: function (id) { - var self = this; - if (observeCallbacks.removed) { - observeCallbacks.removed(transform(self.docs.get(id))); - } - } - }; - } - - var changeObserver = new LocalCollection._CachingChangeObserver( - {callbacks: observeChangesCallbacks}); - var handle = cursor.observeChanges(changeObserver.applyChange); - suppressed = false; - - return handle; -}; -// Is this selector just shorthand for lookup by _id? -LocalCollection._selectorIsId = function (selector) { - return (typeof selector === "string") || - (typeof selector === "number") || - selector instanceof MongoID.ObjectID; -}; - -// Is the selector just lookup by _id (shorthand or not)? -LocalCollection._selectorIsIdPerhapsAsObject = function (selector) { - return LocalCollection._selectorIsId(selector) || - (selector && typeof selector === "object" && - selector._id && LocalCollection._selectorIsId(selector._id) && - Object.keys(selector).length === 1); -}; - -// If this is a selector which explicitly constrains the match by ID to a finite -// number of documents, returns a list of their IDs. Otherwise returns -// null. Note that the selector may have other restrictions so it may not even -// match those document! We care about $in and $and since those are generated -// access-controlled update and remove. -LocalCollection._idsMatchedBySelector = function (selector) { - // Is the selector just an ID? - if (LocalCollection._selectorIsId(selector)) - return [selector]; - if (!selector) - return null; - - // Do we have an _id clause? - if (selector.hasOwnProperty('_id')) { - // Is the _id clause just an ID? - if (LocalCollection._selectorIsId(selector._id)) - return [selector._id]; - // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? - if (selector._id && selector._id.$in - && Array.isArray(selector._id.$in) - && selector._id.$in.length - && selector._id.$in.every(LocalCollection._selectorIsId)) { - return selector._id.$in; - } - return null; - } - - // If this is a top-level $and, and any of the clauses constrain their - // documents, then the whole selector is constrained by any one clause's - // constraint. (Well, by their intersection, but that seems unlikely.) - if (selector.$and && Array.isArray(selector.$and)) { - for (var i = 0; i < selector.$and.length; ++i) { - var subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); - if (subIds) - return subIds; - } - } - - return null; -}; - - diff --git a/packages/minimongo/observe_handle.js b/packages/minimongo/observe_handle.js new file mode 100644 index 0000000000..45a48581dc --- /dev/null +++ b/packages/minimongo/observe_handle.js @@ -0,0 +1,2 @@ +// the handle that comes back from observe. +export class ObserveHandle {} From fdc11af6da68b0e7e6914138d8fc3357608fe199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Tue, 11 Jul 2017 22:32:30 +0200 Subject: [PATCH 06/28] Separated Cursor. --- packages/minimongo/cursor.js | 417 +++++++++++++++++++++++++++++++- packages/minimongo/minimongo.js | 417 -------------------------------- 2 files changed, 416 insertions(+), 418 deletions(-) diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index 62a2836c34..d2c13c7eb6 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -1,3 +1,418 @@ -export class Cursor { +import {LocalCollection} from './local_collection.js'; +export class Cursor { + // don't call this ctor directly. use LocalCollection.find(). + constructor (collection, selector, options) { + var self = this; + if (!options) options = {}; + + self.collection = collection; + self.sorter = null; + self.matcher = new Minimongo.Matcher(selector); + + if (LocalCollection._selectorIsId(selector)) { + // stash for fast path + self._selectorId = selector; + } else if (LocalCollection._selectorIsIdPerhapsAsObject(selector)) { + // also do the fast path for { _id: idString } + self._selectorId = selector._id; + } else { + self._selectorId = undefined; + if (self.matcher.hasGeoQuery() || options.sort) { + self.sorter = new Minimongo.Sorter(options.sort || [], + { matcher: self.matcher }); + } + } + + self.skip = options.skip; + self.limit = options.limit; + self.fields = options.fields; + + self._projectionFn = LocalCollection._compileProjection(self.fields || {}); + + self._transform = LocalCollection.wrapTransform(options.transform); + + // by default, queries register w/ Tracker when it is available. + if (typeof Tracker !== "undefined") + self.reactive = (options.reactive === undefined) ? true : options.reactive; + } + + /** + * @summary Returns the number of documents that match a query. + * @memberOf Mongo.Cursor + * @method count + * @instance + * @locus Anywhere + * @returns {Number} + */ + count () { + var self = this; + + if (self.reactive) + self._depend({added: true, removed: true}, + true /* allow the observe to be unordered */); + + return self._getRawObjects({ordered: true}).length; + } + + /** + * @summary Return all matching documents as an Array. + * @memberOf Mongo.Cursor + * @method fetch + * @instance + * @locus Anywhere + * @returns {Object[]} + */ + fetch () { + var self = this; + var res = []; + self.forEach(function (doc) { + res.push(doc); + }); + return res; + } + + /** + * @callback IterationCallback + * @param {Object} doc + * @param {Number} index + */ + /** + * @summary Call `callback` once for each matching document, sequentially and synchronously. + * @locus Anywhere + * @method forEach + * @instance + * @memberOf Mongo.Cursor + * @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`. + */ + forEach (callback, thisArg) { + var self = this; + + var objects = self._getRawObjects({ordered: true}); + + if (self.reactive) { + self._depend({ + addedBefore: true, + removed: true, + changed: true, + movedBefore: true}); + } + + objects.forEach(function (elt, i) { + // This doubles as a clone operation. + elt = self._projectionFn(elt); + + if (self._transform) + elt = self._transform(elt); + callback.call(thisArg, elt, i, self); + }); + } + + getTransform () { + return this._transform; + } + + /** + * @summary Map callback over all matching documents. Returns an Array. + * @locus Anywhere + * @method map + * @instance + * @memberOf Mongo.Cursor + * @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`. + */ + map (callback, thisArg) { + var self = this; + var res = []; + self.forEach(function (doc, index) { + res.push(callback.call(thisArg, doc, index, self)); + }); + return res; + } + + // options to contain: + // * callbacks for observe(): + // - addedAt (document, atIndex) + // - added (document) + // - changedAt (newDocument, oldDocument, atIndex) + // - changed (newDocument, oldDocument) + // - removedAt (document, atIndex) + // - removed (document) + // - movedTo (document, oldIndex, newIndex) + // + // attributes available on returned query handle: + // * stop(): end updates + // * collection: the collection this query is querying + // + // iff x is a returned query handle, (x instanceof + // LocalCollection.ObserveHandle) is true + // + // initial results delivered through added callback + // XXX maybe callbacks should take a list of objects, to expose transactions? + // XXX maybe support field limiting (to limit what you're notified on) + + /** + * @summary Watch a query. Receive callbacks as the result set changes. + * @locus Anywhere + * @memberOf Mongo.Cursor + * @instance + * @param {Object} callbacks Functions to call to deliver the result set as it changes + */ + observe (options) { + var self = this; + return LocalCollection._observeFromObserveChanges(self, options); + } + + /** + * @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 + * @memberOf Mongo.Cursor + * @instance + * @param {Object} callbacks Functions to call to deliver the result set as it changes + */ + observeChanges (options) { + var self = this; + + var ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); + + // there are several places that assume you aren't combining skip/limit with + // unordered observe. eg, update's EJSON.clone, and the "there are several" + // comment in _modifyAndNotify + // XXX allow skip/limit with unordered observe + if (!options._allow_unordered && !ordered && (self.skip || self.limit)) + throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); + + if (self.fields && (self.fields._id === 0 || self.fields._id === false)) + throw Error("You may not observe a cursor with {fields: {_id: 0}}"); + + var query = { + dirty: false, + matcher: self.matcher, // not fast pathed + sorter: ordered && self.sorter, + distances: ( + self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), + resultsSnapshot: null, + ordered: ordered, + cursor: self, + projectionFn: self._projectionFn + }; + var qid; + + // Non-reactive queries call added[Before] and then never call anything + // else. + if (self.reactive) { + qid = self.collection.next_qid++; + self.collection.queries[qid] = query; + } + query.results = self._getRawObjects({ + ordered: ordered, distances: query.distances}); + if (self.collection.paused) + query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); + + // wrap callbacks we were passed. callbacks only fire when not paused and + // are never undefined + // Filters out blacklisted fields according to cursor's projection. + // XXX wrong place for this? + + // furthermore, callbacks enqueue until the operation we're working on is + // done. + var wrapCallback = function (f) { + if (!f) + return function () {}; + return function (/*args*/) { + var context = this; + var args = arguments; + + if (self.collection.paused) + return; + + self.collection._observeQueue.queueTask(function () { + f.apply(context, args); + }); + }; + }; + query.added = wrapCallback(options.added); + query.changed = wrapCallback(options.changed); + query.removed = wrapCallback(options.removed); + if (ordered) { + query.addedBefore = wrapCallback(options.addedBefore); + query.movedBefore = wrapCallback(options.movedBefore); + } + + if (!options._suppress_initial && !self.collection.paused) { + var results = query.results._map || query.results; + Object.keys(results).forEach(function (key) { + var doc = results[key]; + var fields = EJSON.clone(doc); + + delete fields._id; + if (ordered) + query.addedBefore(doc._id, self._projectionFn(fields), null); + query.added(doc._id, self._projectionFn(fields)); + }); + } + + var handle = new LocalCollection.ObserveHandle; + Object.assign(handle, { + collection: self.collection, + stop: function () { + if (self.reactive) + delete self.collection.queries[qid]; + } + }); + + if (self.reactive && Tracker.active) { + // XXX in many cases, the same observe will be recreated when + // the current autorun is rerun. we could save work by + // letting it linger across rerun and potentially get + // repurposed if the same observe is performed, using logic + // similar to that of Meteor.subscribe. + Tracker.onInvalidate(function () { + handle.stop(); + }); + } + // run the observe callbacks resulting from the initial contents + // before we leave the observe. + self.collection._observeQueue.drain(); + + return handle; + } + + // Since we don't actually have a "nextObject" interface, there's really no + // reason to have a "rewind" interface. All it did was make multiple calls + // to fetch/map/forEach return nothing the second time. + // XXX COMPAT WITH 0.8.1 + rewind () {} + + // XXX Maybe we need a version of observe that just calls a callback if + // anything changed. + _depend (changers, _allow_unordered) { + var self = this; + + if (Tracker.active) { + var v = new Tracker.Dependency; + v.depend(); + var notifyChange = v.changed.bind(v); + + var options = { + _suppress_initial: true, + _allow_unordered: _allow_unordered + }; + ['added', 'changed', 'removed', 'addedBefore', 'movedBefore'].forEach(function (fnName) { + if (changers[fnName]) + options[fnName] = notifyChange; + }); + + // observeChanges will stop() when this computation is invalidated + self.observeChanges(options); + } + } + + _getCollectionName () { + var self = this; + return self.collection.name; + } + + // Returns a collection of matching objects, but doesn't deep copy them. + // + // If ordered is set, returns a sorted array, respecting sorter, skip, and limit + // properties of the query. if sorter is falsey, no sort -- you get the natural + // order. + // + // If ordered is not set, returns an object mapping from ID to doc (sorter, skip + // and limit should not be set). + // + // If ordered is set and this cursor is a $near geoquery, then this function + // will use an _IdMap to track each distance from the $near argument point in + // order to use it as a sort key. If an _IdMap is passed in the 'distances' + // argument, this function will clear it and use it for this purpose (otherwise + // it will just create its own _IdMap). The observeChanges implementation uses + // this to remember the distances after this function returns. + _getRawObjects (options) { + var self = this; + options = options || {}; + + // XXX use OrderedDict instead of array, and make IdMap and OrderedDict + // compatible + var results = options.ordered ? [] : new LocalCollection._IdMap; + + // fast path for single ID value + if (self._selectorId !== undefined) { + // If you have non-zero skip and ask for a single id, you get + // nothing. This is so it matches the behavior of the '{_id: foo}' + // path. + if (self.skip) + return results; + + var selectedDoc = self.collection._docs.get(self._selectorId); + if (selectedDoc) { + if (options.ordered) + results.push(selectedDoc); + else + results.set(self._selectorId, selectedDoc); + } + return results; + } + + // slow path for arbitrary selector, sort, skip, limit + + // in the observeChanges case, distances is actually part of the "query" (ie, + // live results set) object. in other cases, distances is only used inside + // this function. + var distances; + if (self.matcher.hasGeoQuery() && options.ordered) { + if (options.distances) { + distances = options.distances; + distances.clear(); + } else { + distances = new LocalCollection._IdMap(); + } + } + + self.collection._docs.forEach(function (doc, id) { + var matchResult = self.matcher.documentMatches(doc); + if (matchResult.result) { + if (options.ordered) { + results.push(doc); + if (distances && matchResult.distance !== undefined) + distances.set(id, matchResult.distance); + } else { + results.set(id, doc); + } + } + // Fast path for limited unsorted queries. + // XXX 'length' check here seems wrong for ordered + if (self.limit && !self.skip && !self.sorter && + results.length === self.limit) + return false; // break + return true; // continue + }); + + if (!options.ordered) + return results; + + if (self.sorter) { + var comparator = self.sorter.getComparator({distances: distances}); + results.sort(comparator); + } + + var idx_start = self.skip || 0; + var idx_end = self.limit ? (self.limit + idx_start) : results.length; + return results.slice(idx_start, idx_end); + } + + _publishCursor (sub) { + var self = this; + if (! self.collection.name) + throw new Error("Can't publish a cursor from a collection without a name."); + var collection = self.collection.name; + + // XXX minimongo should not depend on mongo-livedata! + if (! Package.mongo) { + throw new Error("Can't publish from Minimongo without the `mongo` package."); + } + + return Package.mongo.Mongo.Collection._publishCursor(self, sub, collection); + } } diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 081b13596e..23b6507f9b 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -32,423 +32,6 @@ MinimongoError = function (message, options={}) { return e; }; -// don't call this ctor directly. use LocalCollection.find(). - -LocalCollection.Cursor = function (collection, selector, options) { - var self = this; - if (!options) options = {}; - - self.collection = collection; - self.sorter = null; - self.matcher = new Minimongo.Matcher(selector); - - if (LocalCollection._selectorIsId(selector)) { - // stash for fast path - self._selectorId = selector; - } else if (LocalCollection._selectorIsIdPerhapsAsObject(selector)) { - // also do the fast path for { _id: idString } - self._selectorId = selector._id; - } else { - self._selectorId = undefined; - if (self.matcher.hasGeoQuery() || options.sort) { - self.sorter = new Minimongo.Sorter(options.sort || [], - { matcher: self.matcher }); - } - } - - self.skip = options.skip; - self.limit = options.limit; - self.fields = options.fields; - - self._projectionFn = LocalCollection._compileProjection(self.fields || {}); - - self._transform = LocalCollection.wrapTransform(options.transform); - - // by default, queries register w/ Tracker when it is available. - if (typeof Tracker !== "undefined") - self.reactive = (options.reactive === undefined) ? true : options.reactive; -}; - -// Since we don't actually have a "nextObject" interface, there's really no -// reason to have a "rewind" interface. All it did was make multiple calls -// to fetch/map/forEach return nothing the second time. -// XXX COMPAT WITH 0.8.1 -LocalCollection.Cursor.prototype.rewind = function () { -}; - -/** - * @callback IterationCallback - * @param {Object} doc - * @param {Number} index - */ -/** - * @summary Call `callback` once for each matching document, sequentially and synchronously. - * @locus Anywhere - * @method forEach - * @instance - * @memberOf Mongo.Cursor - * @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`. - */ -LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) { - var self = this; - - var objects = self._getRawObjects({ordered: true}); - - if (self.reactive) { - self._depend({ - addedBefore: true, - removed: true, - changed: true, - movedBefore: true}); - } - - objects.forEach(function (elt, i) { - // This doubles as a clone operation. - elt = self._projectionFn(elt); - - if (self._transform) - elt = self._transform(elt); - callback.call(thisArg, elt, i, self); - }); -}; - -LocalCollection.Cursor.prototype.getTransform = function () { - return this._transform; -}; - -/** - * @summary Map callback over all matching documents. Returns an Array. - * @locus Anywhere - * @method map - * @instance - * @memberOf Mongo.Cursor - * @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`. - */ -LocalCollection.Cursor.prototype.map = function (callback, thisArg) { - var self = this; - var res = []; - self.forEach(function (doc, index) { - res.push(callback.call(thisArg, doc, index, self)); - }); - return res; -}; - -/** - * @summary Return all matching documents as an Array. - * @memberOf Mongo.Cursor - * @method fetch - * @instance - * @locus Anywhere - * @returns {Object[]} - */ -LocalCollection.Cursor.prototype.fetch = function () { - var self = this; - var res = []; - self.forEach(function (doc) { - res.push(doc); - }); - return res; -}; - -/** - * @summary Returns the number of documents that match a query. - * @memberOf Mongo.Cursor - * @method count - * @instance - * @locus Anywhere - * @returns {Number} - */ -LocalCollection.Cursor.prototype.count = function () { - var self = this; - - if (self.reactive) - self._depend({added: true, removed: true}, - true /* allow the observe to be unordered */); - - return self._getRawObjects({ordered: true}).length; -}; - -LocalCollection.Cursor.prototype._publishCursor = function (sub) { - var self = this; - if (! self.collection.name) - throw new Error("Can't publish a cursor from a collection without a name."); - var collection = self.collection.name; - - // XXX minimongo should not depend on mongo-livedata! - if (! Package.mongo) { - throw new Error("Can't publish from Minimongo without the `mongo` package."); - } - - return Package.mongo.Mongo.Collection._publishCursor(self, sub, collection); -}; - -LocalCollection.Cursor.prototype._getCollectionName = function () { - var self = this; - return self.collection.name; -}; - -// options to contain: -// * callbacks for observe(): -// - addedAt (document, atIndex) -// - added (document) -// - changedAt (newDocument, oldDocument, atIndex) -// - changed (newDocument, oldDocument) -// - removedAt (document, atIndex) -// - removed (document) -// - movedTo (document, oldIndex, newIndex) -// -// attributes available on returned query handle: -// * stop(): end updates -// * collection: the collection this query is querying -// -// iff x is a returned query handle, (x instanceof -// LocalCollection.ObserveHandle) is true -// -// initial results delivered through added callback -// XXX maybe callbacks should take a list of objects, to expose transactions? -// XXX maybe support field limiting (to limit what you're notified on) - -/** - * @summary Watch a query. Receive callbacks as the result set changes. - * @locus Anywhere - * @memberOf Mongo.Cursor - * @instance - * @param {Object} callbacks Functions to call to deliver the result set as it changes - */ -LocalCollection.Cursor.prototype.observe = function (options) { - var self = this; - return LocalCollection._observeFromObserveChanges(self, options); -}; - -/** - * @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 - * @memberOf Mongo.Cursor - * @instance - * @param {Object} callbacks Functions to call to deliver the result set as it changes - */ -LocalCollection.Cursor.prototype.observeChanges = function (options) { - var self = this; - - var ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); - - // there are several places that assume you aren't combining skip/limit with - // unordered observe. eg, update's EJSON.clone, and the "there are several" - // comment in _modifyAndNotify - // XXX allow skip/limit with unordered observe - if (!options._allow_unordered && !ordered && (self.skip || self.limit)) - throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); - - if (self.fields && (self.fields._id === 0 || self.fields._id === false)) - throw Error("You may not observe a cursor with {fields: {_id: 0}}"); - - var query = { - dirty: false, - matcher: self.matcher, // not fast pathed - sorter: ordered && self.sorter, - distances: ( - self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), - resultsSnapshot: null, - ordered: ordered, - cursor: self, - projectionFn: self._projectionFn - }; - var qid; - - // Non-reactive queries call added[Before] and then never call anything - // else. - if (self.reactive) { - qid = self.collection.next_qid++; - self.collection.queries[qid] = query; - } - query.results = self._getRawObjects({ - ordered: ordered, distances: query.distances}); - if (self.collection.paused) - query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); - - // wrap callbacks we were passed. callbacks only fire when not paused and - // are never undefined - // Filters out blacklisted fields according to cursor's projection. - // XXX wrong place for this? - - // furthermore, callbacks enqueue until the operation we're working on is - // done. - var wrapCallback = function (f) { - if (!f) - return function () {}; - return function (/*args*/) { - var context = this; - var args = arguments; - - if (self.collection.paused) - return; - - self.collection._observeQueue.queueTask(function () { - f.apply(context, args); - }); - }; - }; - query.added = wrapCallback(options.added); - query.changed = wrapCallback(options.changed); - query.removed = wrapCallback(options.removed); - if (ordered) { - query.addedBefore = wrapCallback(options.addedBefore); - query.movedBefore = wrapCallback(options.movedBefore); - } - - if (!options._suppress_initial && !self.collection.paused) { - var results = query.results._map || query.results; - Object.keys(results).forEach(function (key) { - var doc = results[key]; - var fields = EJSON.clone(doc); - - delete fields._id; - if (ordered) - query.addedBefore(doc._id, self._projectionFn(fields), null); - query.added(doc._id, self._projectionFn(fields)); - }); - } - - var handle = new LocalCollection.ObserveHandle; - Object.assign(handle, { - collection: self.collection, - stop: function () { - if (self.reactive) - delete self.collection.queries[qid]; - } - }); - - if (self.reactive && Tracker.active) { - // XXX in many cases, the same observe will be recreated when - // the current autorun is rerun. we could save work by - // letting it linger across rerun and potentially get - // repurposed if the same observe is performed, using logic - // similar to that of Meteor.subscribe. - Tracker.onInvalidate(function () { - handle.stop(); - }); - } - // run the observe callbacks resulting from the initial contents - // before we leave the observe. - self.collection._observeQueue.drain(); - - return handle; -}; - -// Returns a collection of matching objects, but doesn't deep copy them. -// -// If ordered is set, returns a sorted array, respecting sorter, skip, and limit -// properties of the query. if sorter is falsey, no sort -- you get the natural -// order. -// -// If ordered is not set, returns an object mapping from ID to doc (sorter, skip -// and limit should not be set). -// -// If ordered is set and this cursor is a $near geoquery, then this function -// will use an _IdMap to track each distance from the $near argument point in -// order to use it as a sort key. If an _IdMap is passed in the 'distances' -// argument, this function will clear it and use it for this purpose (otherwise -// it will just create its own _IdMap). The observeChanges implementation uses -// this to remember the distances after this function returns. -LocalCollection.Cursor.prototype._getRawObjects = function (options) { - var self = this; - options = options || {}; - - // XXX use OrderedDict instead of array, and make IdMap and OrderedDict - // compatible - var results = options.ordered ? [] : new LocalCollection._IdMap; - - // fast path for single ID value - if (self._selectorId !== undefined) { - // If you have non-zero skip and ask for a single id, you get - // nothing. This is so it matches the behavior of the '{_id: foo}' - // path. - if (self.skip) - return results; - - var selectedDoc = self.collection._docs.get(self._selectorId); - if (selectedDoc) { - if (options.ordered) - results.push(selectedDoc); - else - results.set(self._selectorId, selectedDoc); - } - return results; - } - - // slow path for arbitrary selector, sort, skip, limit - - // in the observeChanges case, distances is actually part of the "query" (ie, - // live results set) object. in other cases, distances is only used inside - // this function. - var distances; - if (self.matcher.hasGeoQuery() && options.ordered) { - if (options.distances) { - distances = options.distances; - distances.clear(); - } else { - distances = new LocalCollection._IdMap(); - } - } - - self.collection._docs.forEach(function (doc, id) { - var matchResult = self.matcher.documentMatches(doc); - if (matchResult.result) { - if (options.ordered) { - results.push(doc); - if (distances && matchResult.distance !== undefined) - distances.set(id, matchResult.distance); - } else { - results.set(id, doc); - } - } - // Fast path for limited unsorted queries. - // XXX 'length' check here seems wrong for ordered - if (self.limit && !self.skip && !self.sorter && - results.length === self.limit) - return false; // break - return true; // continue - }); - - if (!options.ordered) - return results; - - if (self.sorter) { - var comparator = self.sorter.getComparator({distances: distances}); - results.sort(comparator); - } - - var idx_start = self.skip || 0; - var idx_end = self.limit ? (self.limit + idx_start) : results.length; - return results.slice(idx_start, idx_end); -}; - -// XXX Maybe we need a version of observe that just calls a callback if -// anything changed. -LocalCollection.Cursor.prototype._depend = function (changers, _allow_unordered) { - var self = this; - - if (Tracker.active) { - var v = new Tracker.Dependency; - v.depend(); - var notifyChange = v.changed.bind(v); - - var options = { - _suppress_initial: true, - _allow_unordered: _allow_unordered - }; - ['added', 'changed', 'removed', 'addedBefore', 'movedBefore'].forEach(function (fnName) { - if (changers[fnName]) - options[fnName] = notifyChange; - }); - - // observeChanges will stop() when this computation is invalidated - self.observeChanges(options); - } -}; - // Give a sort spec, which can be in any of these forms: // {"key1": 1, "key2": -1} // [["key1", "asc"], ["key2", "desc"]] From b893390895047249716b108ac97270ce21b71dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Tue, 11 Jul 2017 22:45:34 +0200 Subject: [PATCH 07/28] Separated. --- packages/minimongo/common_main.js | 5 + packages/minimongo/cursor.js | 2 + packages/minimongo/local_collection.js | 3 + packages/minimongo/matcher.js | 13 +- packages/minimongo/observe_handle.js | 2 +- packages/minimongo/package.js | 7 +- .../{minimongo_server.js => server_main.js} | 170 +++++----- .../minimongo/{minimongo.js => sorter.js} | 304 ++++++++---------- 8 files changed, 245 insertions(+), 261 deletions(-) create mode 100644 packages/minimongo/common_main.js rename packages/minimongo/{minimongo_server.js => server_main.js} (97%) rename packages/minimongo/{minimongo.js => sorter.js} (78%) diff --git a/packages/minimongo/common_main.js b/packages/minimongo/common_main.js new file mode 100644 index 0000000000..6d784d22fb --- /dev/null +++ b/packages/minimongo/common_main.js @@ -0,0 +1,5 @@ +import {LocalCollection} from './local_collection.js'; +import {Matcher} from './matcher.js'; +import {Sorter} from './sorter.js'; + +Minimongo = {LocalCollection, Matcher, Sorter}; diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index d2c13c7eb6..ad573b0e4d 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -1,5 +1,7 @@ import {LocalCollection} from './local_collection.js'; +// Cursor: a specification for a particular subset of documents, w/ +// a defined order, limit, and offset. creating a Cursor with LocalCollection.find(), export class Cursor { // don't call this ctor directly. use LocalCollection.find(). constructor (collection, selector, options) { diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 5ebcee16a7..5f155ae6f2 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1,6 +1,9 @@ import {Cursor} from './cursor.js'; import {ObserveHandle} from './observe_handle.js'; +// XXX type checking on selectors (graceful error if malformed) + +// LocalCollection: a set of documents that supports queries and modifiers. export class LocalCollection { static Cursor = Cursor; diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index 05746fc776..b037e9cf43 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -1186,7 +1186,18 @@ function makeLookupFunction (key, options) { }; } -MinimongoTest.makeLookupFunction = makeLookupFunction; +// Object exported only for unit testing. +// Use it to export private functions to test in Tinytest. +MinimongoTest = {makeLookupFunction}; +MinimongoError = function (message, options = {}) { + if (typeof message === "string" && options.field) { + message += ` for field '${options.field}'`; + } + + var e = new Error(message); + e.name = "MinimongoError"; + return e; +}; function nothingMatcher (docOrBranchedValues) { return {result: false}; diff --git a/packages/minimongo/observe_handle.js b/packages/minimongo/observe_handle.js index 45a48581dc..ae40fc0594 100644 --- a/packages/minimongo/observe_handle.js +++ b/packages/minimongo/observe_handle.js @@ -1,2 +1,2 @@ -// the handle that comes back from observe. +// ObserveHandle: the return value of a live query. export class ObserveHandle {} diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 9d784276c9..18238ca383 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -22,11 +22,8 @@ Package.onUse(api => { 'tracker' ]); - api.addFiles('minimongo.js'); - api.addFiles('minimongo_server.js', 'server'); - - // api.mainModule('client_main.js', 'client'); - // api.mainModule('server_main.js', 'server'); + api.mainModule('common_main.js', 'client'); + api.mainModule('server_main.js', 'server'); }); Package.onTest(api => { diff --git a/packages/minimongo/minimongo_server.js b/packages/minimongo/server_main.js similarity index 97% rename from packages/minimongo/minimongo_server.js rename to packages/minimongo/server_main.js index acedd58e30..27a5748bad 100644 --- a/packages/minimongo/minimongo_server.js +++ b/packages/minimongo/server_main.js @@ -1,19 +1,4 @@ -// Knows how to combine a mongo selector and a fields projection to a new fields -// projection taking into account active fields from the passed selector. -// @returns Object - projection object (same as fields option of mongo cursor) -Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { - var self = this; - var selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); - - // Special case for $where operator in the selector - projection should depend - // on all fields of the document. getSelectorPaths returns a list of paths - // selector depends on. If one of the paths is '' (empty string) representing - // the root or the whole document, complete projection should be returned. - if (selectorPaths.includes('')) - return {}; - - return combineImportantPathsIntoProjection(selectorPaths, projection); -}; +import './common_main.js'; Minimongo._pathsElidingNumericKeys = function (paths) { var self = this; @@ -22,53 +7,6 @@ Minimongo._pathsElidingNumericKeys = function (paths) { }); }; -combineImportantPathsIntoProjection = function (paths, projection) { - var prjDetails = projectionDetails(projection); - var tree = prjDetails.tree; - var mergedProjection = {}; - - // merge the paths to include - tree = pathsToTree(paths, - function (path) { return true; }, - function (node, path, fullPath) { return true; }, - tree); - mergedProjection = treeToPaths(tree); - if (prjDetails.including) { - // both selector and projection are pointing on fields to include - // so we can just return the merged tree - return mergedProjection; - } else { - // selector is pointing at fields to include - // projection is pointing at fields to exclude - // make sure we don't exclude important paths - var mergedExclProjection = {}; - Object.keys(mergedProjection).forEach(function (path) { - var incl = mergedProjection[path]; - if (!incl) - mergedExclProjection[path] = false; - }); - - return mergedExclProjection; - } -}; - -// Returns a set of key paths similar to -// { 'foo.bar': 1, 'a.b.c': 1 } -var treeToPaths = function (tree, prefix) { - prefix = prefix || ''; - var result = {}; - - Object.keys(tree).forEach(function (key) { - var val = tree[key]; - if (val === Object(val)) - Object.assign(result, treeToPaths(val, prefix + key + '.')); - else - result[prefix + key] = val; - }); - - return result; -}; - // Returns true if the modifier applied to some document may change the result // of matching the document by selector // The modifier is always in a form of Object: @@ -115,13 +53,6 @@ Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { }); }; -// Minimongo.Sorter gets a similar method, which delegates to a Matcher it made -// for this exact purpose. -Minimongo.Sorter.prototype.affectedByModifier = function (modifier) { - var self = this; - return self._selectorForAffectedByModifier.affectedByModifier(modifier); -}; - // @param modifier - Object: MongoDB-styled modifier with `$set`s and `$unsets` // only. (assumed to come from oplog) // @returns - Boolean: if after applying the modifier, selector can start @@ -192,6 +123,23 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { return self.documentMatches(matchingDocument).result; }; +// Knows how to combine a mongo selector and a fields projection to a new fields +// projection taking into account active fields from the passed selector. +// @returns Object - projection object (same as fields option of mongo cursor) +Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { + var self = this; + var selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); + + // Special case for $where operator in the selector - projection should depend + // on all fields of the document. getSelectorPaths returns a list of paths + // selector depends on. If one of the paths is '' (empty string) representing + // the root or the whole document, complete projection should be returned. + if (selectorPaths.includes('')) + return {}; + + return combineImportantPathsIntoProjection(selectorPaths, projection); +}; + // Returns an object that would match the selector if possible or null if the // selector is too complex for us to analyze // { 'a.b': { ans: 42 }, 'foo.bar': null, 'foo.baz': "something" } @@ -261,7 +209,50 @@ Minimongo.Matcher.prototype.matchingDocument = function () { return self._matchingDocument; }; -var getPaths = function (sel) { +// Minimongo.Sorter gets a similar method, which delegates to a Matcher it made +// for this exact purpose. +Minimongo.Sorter.prototype.affectedByModifier = function (modifier) { + var self = this; + return self._selectorForAffectedByModifier.affectedByModifier(modifier); +}; + +Minimongo.Sorter.prototype.combineIntoProjection = function (projection) { + var self = this; + var specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); + return combineImportantPathsIntoProjection(specPaths, projection); +}; + +function combineImportantPathsIntoProjection (paths, projection) { + var prjDetails = projectionDetails(projection); + var tree = prjDetails.tree; + var mergedProjection = {}; + + // merge the paths to include + tree = pathsToTree(paths, + function (path) { return true; }, + function (node, path, fullPath) { return true; }, + tree); + mergedProjection = treeToPaths(tree); + if (prjDetails.including) { + // both selector and projection are pointing on fields to include + // so we can just return the merged tree + return mergedProjection; + } else { + // selector is pointing at fields to include + // projection is pointing at fields to exclude + // make sure we don't exclude important paths + var mergedExclProjection = {}; + Object.keys(mergedProjection).forEach(function (path) { + var incl = mergedProjection[path]; + if (!incl) + mergedExclProjection[path] = false; + }); + + return mergedExclProjection; + } +} + +function getPaths (sel) { return Object.keys(new Minimongo.Matcher(sel)._paths); return Object.keys(sel).map(function (k) { var v = sel[k]; @@ -276,26 +267,37 @@ var getPaths = function (sel) { }) .reduce(function (a, b) { return a.concat(b); }, []) .filter(function (a, b, c) { return c.indexOf(a) === b; }); -}; +} // A helper to ensure object has only certain keys -var onlyContainsKeys = function (obj, keys) { +function onlyContainsKeys (obj, keys) { return Object.keys(obj).every(function (k) { return keys.includes(k); }); -}; - -var pathHasNumericKeys = function (path) { - return path.split('.').some(isNumericKey); } +function pathHasNumericKeys (path) { + return path.split('.').some(isNumericKey); + // XXX from Underscore.String (http://epeli.github.com/underscore.string/) -var startsWith = function(str, starts) { +function startsWith(str, starts) { return str.length >= starts.length && str.substring(0, starts.length) === starts; -}; -Minimongo.Sorter.prototype.combineIntoProjection = function (projection) { - var self = this; - var specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); - return combineImportantPathsIntoProjection(specPaths, projection); -}; +} + +// Returns a set of key paths similar to +// { 'foo.bar': 1, 'a.b.c': 1 } +function treeToPaths (tree, prefix) { + prefix = prefix || ''; + var result = {}; + + Object.keys(tree).forEach(function (key) { + var val = tree[key]; + if (val === Object(val)) + Object.assign(result, treeToPaths(val, prefix + key + '.')); + else + result[prefix + key] = val; + }); + + return result; +} diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/sorter.js similarity index 78% rename from packages/minimongo/minimongo.js rename to packages/minimongo/sorter.js index 23b6507f9b..ee3a90d77d 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/sorter.js @@ -1,37 +1,3 @@ -import {LocalCollection} from './local_collection.js'; -import {Matcher} from './matcher.js'; -import { - isIndexable, - isNumericKey, - isOperatorObject, -} from './common.js'; - - -// XXX type checking on selectors (graceful error if malformed) - -// LocalCollection: a set of documents that supports queries and modifiers. - -// Cursor: a specification for a particular subset of documents, w/ -// a defined order, limit, and offset. creating a Cursor with LocalCollection.find(), - -// ObserveHandle: the return value of a live query. - -Minimongo = {LocalCollection, Matcher}; - -// Object exported only for unit testing. -// Use it to export private functions to test in Tinytest. -MinimongoTest = {}; - -MinimongoError = function (message, options={}) { - if (typeof message === "string" && options.field) { - message += ` for field '${options.field}'`; - } - - var e = new Error(message); - e.name = "MinimongoError"; - return e; -}; - // Give a sort spec, which can be in any of these forms: // {"key1": 1, "key2": -1} // [["key1", "asc"], ["key2", "desc"]] @@ -45,75 +11,73 @@ MinimongoError = function (message, options={}) { // first object comes first in order, 1 if the second object comes // first, or 0 if neither object comes before the other. -Minimongo.Sorter = function (spec, options) { - var self = this; - options = options || {}; +export class Sorter { + constructor (spec, options) { + var self = this; + options = options || {}; - self._sortSpecParts = []; - self._sortFunction = null; + self._sortSpecParts = []; + self._sortFunction = null; - var addSpecPart = function (path, ascending) { - if (!path) - throw Error("sort keys must be non-empty"); - if (path.charAt(0) === '$') - throw Error("unsupported sort key: " + path); - self._sortSpecParts.push({ - path: path, - lookup: makeLookupFunction(path, {forSort: true}), - ascending: ascending - }); - }; + var addSpecPart = function (path, ascending) { + if (!path) + throw Error("sort keys must be non-empty"); + if (path.charAt(0) === '$') + throw Error("unsupported sort key: " + path); + self._sortSpecParts.push({ + path: path, + lookup: makeLookupFunction(path, {forSort: true}), + ascending: ascending + }); + }; - if (spec instanceof Array) { - for (var i = 0; i < spec.length; i++) { - if (typeof spec[i] === "string") { - addSpecPart(spec[i], true); - } else { - addSpecPart(spec[i][0], spec[i][1] !== "desc"); + if (spec instanceof Array) { + for (var i = 0; i < spec.length; i++) { + if (typeof spec[i] === "string") { + addSpecPart(spec[i], true); + } else { + addSpecPart(spec[i][0], spec[i][1] !== "desc"); + } } + } else if (typeof spec === "object") { + Object.keys(spec).forEach(function (key) { + var value = spec[key]; + addSpecPart(key, value >= 0); + }); + } else if (typeof spec === "function") { + self._sortFunction = spec; + } else { + throw Error("Bad sort specification: " + JSON.stringify(spec)); } - } else if (typeof spec === "object") { - Object.keys(spec).forEach(function (key) { - var value = spec[key]; - addSpecPart(key, value >= 0); - }); - } else if (typeof spec === "function") { - self._sortFunction = spec; - } else { - throw Error("Bad sort specification: " + JSON.stringify(spec)); + + // If a function is specified for sorting, we skip the rest. + if (self._sortFunction) + return; + + // To implement affectedByModifier, we piggy-back on top of Matcher's + // affectedByModifier code; we create a selector that is affected by the same + // modifiers as this sort order. This is only implemented on the server. + if (self.affectedByModifier) { + var selector = {}; + self._sortSpecParts.forEach(function (spec) { + selector[spec.path] = 1; + }); + self._selectorForAffectedByModifier = new Minimongo.Matcher(selector); + } + + self._keyComparator = composeComparators( + self._sortSpecParts.map(function (spec, i) { + return self._keyFieldComparator(i); + })); + + // If you specify a matcher for this Sorter, _keyFilter may be set to a + // function which selects whether or not a given "sort key" (tuple of values + // for the different sort spec fields) is compatible with the selector. + self._keyFilter = null; + options.matcher && self._useWithMatcher(options.matcher); } - // If a function is specified for sorting, we skip the rest. - if (self._sortFunction) - return; - - // To implement affectedByModifier, we piggy-back on top of Matcher's - // affectedByModifier code; we create a selector that is affected by the same - // modifiers as this sort order. This is only implemented on the server. - if (self.affectedByModifier) { - var selector = {}; - self._sortSpecParts.forEach(function (spec) { - selector[spec.path] = 1; - }); - self._selectorForAffectedByModifier = new Minimongo.Matcher(selector); - } - - self._keyComparator = composeComparators( - self._sortSpecParts.map(function (spec, i) { - return self._keyFieldComparator(i); - })); - - // If you specify a matcher for this Sorter, _keyFilter may be set to a - // function which selects whether or not a given "sort key" (tuple of values - // for the different sort spec fields) is compatible with the selector. - self._keyFilter = null; - options.matcher && self._useWithMatcher(options.matcher); -}; - -// In addition to these methods, sorter_project.js defines combineIntoProjection -// on the server only. -Object.assign(Minimongo.Sorter.prototype, { - getComparator: function (options) { + getComparator (options) { var self = this; // If sort is specified or have no distances, just use the comparator from @@ -135,55 +99,24 @@ Object.assign(Minimongo.Sorter.prototype, { throw Error("Missing distance for " + b._id); return distances.get(a._id) - distances.get(b._id); }; - }, + } - _getPaths: function () { + // Takes in two keys: arrays whose lengths match the number of spec + // parts. Returns negative, 0, or positive based on using the sort spec to + // compare fields. + _compareKeys (key1, key2) { var self = this; - return self._sortSpecParts.map(function (part) { return part.path; }); - }, + if (key1.length !== self._sortSpecParts.length || + key2.length !== self._sortSpecParts.length) { + throw Error("Key has wrong length"); + } - // Finds the minimum key from the doc, according to the sort specs. (We say - // "minimum" here but this is with respect to the sort spec, so "descending" - // sort fields mean we're finding the max for that field.) - // - // Note that this is NOT "find the minimum value of the first field, the - // minimum value of the second field, etc"... it's "choose the - // lexicographically minimum value of the key vector, allowing only keys which - // you can find along the same paths". ie, for a doc {a: [{x: 0, y: 5}, {x: - // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and - // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key. - _getMinKeyFromDoc: function (doc) { - var self = this; - var minKey = null; - - self._generateKeysFromDoc(doc, function (key) { - if (!self._keyCompatibleWithSelector(key)) - return; - - if (minKey === null) { - minKey = key; - return; - } - if (self._compareKeys(key, minKey) < 0) { - minKey = key; - } - }); - - // This could happen if our key filter somehow filters out all the keys even - // though somehow the selector matches. - if (minKey === null) - throw Error("sort selector found no keys in doc?"); - return minKey; - }, - - _keyCompatibleWithSelector: function (key) { - var self = this; - return !self._keyFilter || self._keyFilter(key); - }, + return self._keyComparator(key1, key2); + } // Iterates over each possible "key" from doc (ie, over each branch), calling // 'cb' with the key. - _generateKeysFromDoc: function (doc, cb) { + _generateKeysFromDoc (doc, cb) { var self = this; if (self._sortSpecParts.length === 0) @@ -278,37 +211,11 @@ Object.assign(Minimongo.Sorter.prototype, { }); cb(key); }); - }, - - // Takes in two keys: arrays whose lengths match the number of spec - // parts. Returns negative, 0, or positive based on using the sort spec to - // compare fields. - _compareKeys: function (key1, key2) { - var self = this; - if (key1.length !== self._sortSpecParts.length || - key2.length !== self._sortSpecParts.length) { - throw Error("Key has wrong length"); - } - - return self._keyComparator(key1, key2); - }, - - // Given an index 'i', returns a comparator that compares two key arrays based - // on field 'i'. - _keyFieldComparator: function (i) { - var self = this; - var invert = !self._sortSpecParts[i].ascending; - return function (key1, key2) { - var compare = LocalCollection._f._cmp(key1[i], key2[i]); - if (invert) - compare = -compare; - return compare; - }; - }, + } // Returns a comparator that represents the sort specification (but not // including a possible geoquery distance tie-breaker). - _getBaseComparator: function () { + _getBaseComparator () { var self = this; if (self._sortFunction) @@ -327,7 +234,64 @@ Object.assign(Minimongo.Sorter.prototype, { var key2 = self._getMinKeyFromDoc(doc2); return self._compareKeys(key1, key2); }; - }, + } + + // Finds the minimum key from the doc, according to the sort specs. (We say + // "minimum" here but this is with respect to the sort spec, so "descending" + // sort fields mean we're finding the max for that field.) + // + // Note that this is NOT "find the minimum value of the first field, the + // minimum value of the second field, etc"... it's "choose the + // lexicographically minimum value of the key vector, allowing only keys which + // you can find along the same paths". ie, for a doc {a: [{x: 0, y: 5}, {x: + // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and + // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key. + _getMinKeyFromDoc (doc) { + var self = this; + var minKey = null; + + self._generateKeysFromDoc(doc, function (key) { + if (!self._keyCompatibleWithSelector(key)) + return; + + if (minKey === null) { + minKey = key; + return; + } + if (self._compareKeys(key, minKey) < 0) { + minKey = key; + } + }); + + // This could happen if our key filter somehow filters out all the keys even + // though somehow the selector matches. + if (minKey === null) + throw Error("sort selector found no keys in doc?"); + return minKey; + } + + _getPaths () { + var self = this; + return self._sortSpecParts.map(function (part) { return part.path; }); + } + + _keyCompatibleWithSelector (key) { + var self = this; + return !self._keyFilter || self._keyFilter(key); + } + + // Given an index 'i', returns a comparator that compares two key arrays based + // on field 'i'. + _keyFieldComparator (i) { + var self = this; + var invert = !self._sortSpecParts[i].ascending; + return function (key1, key2) { + var compare = LocalCollection._f._cmp(key1[i], key2[i]); + if (invert) + compare = -compare; + return compare; + }; + } // In MongoDB, if you have documents // {_id: 'x', a: [1, 10]} and @@ -348,7 +312,7 @@ Object.assign(Minimongo.Sorter.prototype, { // skip sort keys that don't match the selector. The logic here is pretty // subtle and undocumented; we've gotten as close as we can figure out based // on our understanding of Mongo's behavior. - _useWithMatcher: function (matcher) { + _useWithMatcher (matcher) { var self = this; if (self._keyFilter) @@ -438,7 +402,7 @@ Object.assign(Minimongo.Sorter.prototype, { }); }; } -}); +} // Given an array of comparators // (functions (a,b)->(negative or positive or zero)), returns a single From 0343e52d81c159422f4ef58c34ef8c4f2bcbd9b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Tue, 11 Jul 2017 23:12:18 +0200 Subject: [PATCH 08/28] Separation finished. --- packages/minimongo/common.js | 1096 +++++++++++++++++ packages/minimongo/common_main.js | 5 - packages/minimongo/local_collection.js | 119 +- packages/minimongo/main.js | 6 + .../{server_main.js => main_server.js} | 9 +- packages/minimongo/matcher.js | 988 +-------------- packages/minimongo/minimongo_tests_client.js | 1 - packages/minimongo/package.js | 4 +- packages/minimongo/sorter.js | 9 + 9 files changed, 1129 insertions(+), 1108 deletions(-) delete mode 100644 packages/minimongo/common_main.js create mode 100644 packages/minimongo/main.js rename packages/minimongo/{server_main.js => main_server.js} (98%) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index b471cda7b3..a0db09f7a4 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -1,5 +1,707 @@ import {LocalCollection} from './local_collection.js'; +// Each element selector contains: +// - compileElementSelector, a function with args: +// - operand - the "right hand side" of the operator +// - valueSelector - the "context" for the operator (so that $regex can find +// $options) +// - matcher - the Matcher this is going into (so that $elemMatch can compile +// more things) +// returning a function mapping a single value to bool. +// - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from +// being called +// - dontIncludeLeafArrays, a bool which causes an argument to be passed to +// expandArraysInBranches if it is called +export const ELEMENT_OPERATORS = { + $lt: makeInequality(function (cmpValue) { + return cmpValue < 0; + }), + $gt: makeInequality(function (cmpValue) { + return cmpValue > 0; + }), + $lte: makeInequality(function (cmpValue) { + return cmpValue <= 0; + }), + $gte: makeInequality(function (cmpValue) { + return cmpValue >= 0; + }), + $mod: { + compileElementSelector: function (operand) { + if (!(Array.isArray(operand) && operand.length === 2 + && typeof(operand[0]) === 'number' + && typeof(operand[1]) === 'number')) { + throw Error("argument to $mod must be an array of two numbers"); + } + // XXX could require to be ints or round or something + var divisor = operand[0]; + var remainder = operand[1]; + return function (value) { + return typeof value === 'number' && value % divisor === remainder; + }; + } + }, + $in: { + compileElementSelector: function (operand) { + if (!Array.isArray(operand)) + throw Error("$in needs an array"); + + var elementMatchers = []; + operand.forEach(function (option) { + if (option instanceof RegExp) + elementMatchers.push(regexpElementMatcher(option)); + else if (isOperatorObject(option)) + throw Error("cannot nest $ under $in"); + else + elementMatchers.push(equalityElementMatcher(option)); + }); + + return function (value) { + // Allow {a: {$in: [null]}} to match when 'a' does not exist. + if (value === undefined) + value = null; + return elementMatchers.some(function (e) { + return e(value); + }); + }; + } + }, + $size: { + // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we + // don't want to consider the element [5,5] in the leaf array [[5,5]] as a + // possible value. + dontExpandLeafArrays: true, + compileElementSelector: function (operand) { + if (typeof operand === 'string') { + // Don't ask me why, but by experimentation, this seems to be what Mongo + // does. + operand = 0; + } else if (typeof operand !== 'number') { + throw Error("$size needs a number"); + } + return function (value) { + return Array.isArray(value) && value.length === operand; + }; + } + }, + $type: { + // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should + // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: + // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but + // should *not* include it itself. + dontIncludeLeafArrays: true, + compileElementSelector: function (operand) { + if (typeof operand !== 'number') + throw Error("$type needs a number"); + return function (value) { + return value !== undefined + && LocalCollection._f._type(value) === operand; + }; + } + }, + $bitsAllSet: { + compileElementSelector: function (operand) { + var op = getOperandBitmask(operand, '$bitsAllSet') + return function (value) { + var bitmask = getValueBitmask(value, op.length) + return bitmask && op.every(function (byte, idx) { + return ((bitmask[idx] & byte) == byte) + }) + } + } + }, + $bitsAnySet: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAnySet') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.some(function (byte, idx) { + return ((~bitmask[idx] & byte) !== byte) + }) + } + } + }, + $bitsAllClear: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAllClear') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.every(function (byte, idx) { + return !(bitmask[idx] & byte) + }) + } + } + }, + $bitsAnyClear: { + compileElementSelector: function (operand) { + var query = getOperandBitmask(operand, '$bitsAnyClear') + return function (value) { + var bitmask = getValueBitmask(value, query.length) + return bitmask && query.some(function (byte, idx) { + return ((bitmask[idx] & byte) !== byte) + }) + } + } + }, + $regex: { + compileElementSelector: function (operand, valueSelector) { + if (!(typeof operand === 'string' || operand instanceof RegExp)) + throw Error("$regex has to be a string or RegExp"); + + var regexp; + if (valueSelector.$options !== undefined) { + // Options passed in $options (even the empty string) always overrides + // options in the RegExp object itself. (See also + // Mongo.Collection._rewriteSelector.) + + // Be clear that we only support the JS-supported options, not extended + // ones (eg, Mongo supports x and s). Ideally we would implement x and s + // by transforming the regexp, but not today... + if (/[^gim]/.test(valueSelector.$options)) + throw new Error("Only the i, m, and g regexp options are supported"); + + var regexSource = operand instanceof RegExp ? operand.source : operand; + regexp = new RegExp(regexSource, valueSelector.$options); + } else if (operand instanceof RegExp) { + regexp = operand; + } else { + regexp = new RegExp(operand); + } + return regexpElementMatcher(regexp); + } + }, + $elemMatch: { + dontExpandLeafArrays: true, + compileElementSelector: function (operand, valueSelector, matcher) { + if (!LocalCollection._isPlainObject(operand)) + throw Error("$elemMatch need an object"); + + var subMatcher, isDocMatcher; + if (isOperatorObject(Object.keys(operand) + .filter(function (key) { return !Object.keys(LOGICAL_OPERATORS).includes(key); }) + .reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { + subMatcher = compileValueSelector(operand, matcher); + isDocMatcher = false; + } else { + // This is NOT the same as compileValueSelector(operand), and not just + // because of the slightly different calling convention. + // {$elemMatch: {x: 3}} means "an element has a field x:3", not + // "consists only of a field x:3". Also, regexps and sub-$ are allowed. + subMatcher = compileDocumentSelector(operand, matcher, + {inElemMatch: true}); + isDocMatcher = true; + } + + return function (value) { + if (!Array.isArray(value)) + return false; + for (var i = 0; i < value.length; ++i) { + var arrayElement = value[i]; + var arg; + if (isDocMatcher) { + // We can only match {$elemMatch: {b: 3}} against objects. + // (We can also match against arrays, if there's numeric indices, + // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) + if (!isIndexable(arrayElement)) + return false; + arg = arrayElement; + } else { + // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches + // {a: [8]} but not {a: [[8]]} + arg = [{value: arrayElement, dontIterate: true}]; + } + // XXX support $near in $elemMatch by propagating $distance? + if (subMatcher(arg).result) + return i; // specially understood to mean "use as arrayIndices" + } + return false; + }; + } + } +}; + +// Operators that appear at the top level of a document selector. +const LOGICAL_OPERATORS = { + $and: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + return andDocumentMatchers(matchers); + }, + + $or: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + + // Special case: if there is only one matcher, use it directly, *preserving* + // any arrayIndices it returns. + if (matchers.length === 1) + return matchers[0]; + + return function (doc) { + var result = matchers.some(function (f) { + return f(doc).result; + }); + // $or does NOT set arrayIndices when it has multiple + // sub-expressions. (Tested against MongoDB.) + return {result: result}; + }; + }, + + $nor: function (subSelector, matcher, inElemMatch) { + var matchers = compileArrayOfDocumentSelectors( + subSelector, matcher, inElemMatch); + return function (doc) { + var result = matchers.every(function (f) { + return !f(doc).result; + }); + // Never set arrayIndices, because we only match if nothing in particular + // 'matched' (and because this is consistent with MongoDB). + return {result: result}; + }; + }, + + $where: function (selectorValue, matcher) { + // Record that *any* path may be used. + matcher._recordPathUsed(''); + matcher._hasWhere = true; + if (!(selectorValue instanceof Function)) { + // XXX MongoDB seems to have more complex logic to decide where or or not + // to add 'return'; not sure exactly what it is. + selectorValue = Function('obj', `return ${selectorValue}`); + } + return function (doc) { + // We make the document available as both `this` and `obj`. + // XXX not sure what we should do if this throws + return {result: selectorValue.call(doc, doc)}; + }; + }, + + // This is just used as a comment in the query (in MongoDB, it also ends up in + // query logs); it has no effect on the actual selection. + $comment: function () { + return function () { + return {result: true}; + }; + } +}; + +// Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a +// document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as +// "match each branched value independently and combine with +// convertElementMatcherToBranchedMatcher". +const VALUE_OPERATORS = { + $eq: function (operand) { + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(operand)); + }, + $not: function (operand, valueSelector, matcher) { + return invertBranchedMatcher(compileValueSelector(operand, matcher)); + }, + $ne: function (operand) { + return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( + equalityElementMatcher(operand))); + }, + $nin: function (operand) { + return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( + ELEMENT_OPERATORS.$in.compileElementSelector(operand))); + }, + $exists: function (operand) { + var exists = convertElementMatcherToBranchedMatcher(function (value) { + return value !== undefined; + }); + return operand ? exists : invertBranchedMatcher(exists); + }, + // $options just provides options for $regex; its logic is inside $regex + $options: function (operand, valueSelector) { + if (!valueSelector.hasOwnProperty('$regex')) + throw Error("$options needs a $regex"); + return everythingMatcher; + }, + // $maxDistance is basically an argument to $near + $maxDistance: function (operand, valueSelector) { + if (!valueSelector.$near) + throw Error("$maxDistance needs a $near"); + return everythingMatcher; + }, + $all: function (operand, valueSelector, matcher) { + if (!Array.isArray(operand)) + throw Error("$all requires array"); + // Not sure why, but this seems to be what MongoDB does. + if (operand.length === 0) + return nothingMatcher; + + var branchedMatchers = []; + operand.forEach(function (criterion) { + // XXX handle $all/$elemMatch combination + if (isOperatorObject(criterion)) + throw Error("no $ expressions in $all"); + // This is always a regexp or equality selector. + branchedMatchers.push(compileValueSelector(criterion, matcher)); + }); + // andBranchedMatchers does NOT require all selectors to return true on the + // SAME branch. + return andBranchedMatchers(branchedMatchers); + }, + $near: function (operand, valueSelector, matcher, isRoot) { + if (!isRoot) + throw Error("$near can't be inside another $ operator"); + matcher._hasGeoQuery = true; + + // There are two kinds of geodata in MongoDB: legacy coordinate pairs and + // GeoJSON. They use different distance metrics, too. GeoJSON queries are + // marked with a $geometry property, though legacy coordinates can be + // matched using $geometry. + + var maxDistance, point, distance; + if (LocalCollection._isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { + // GeoJSON "2dsphere" mode. + maxDistance = operand.$maxDistance; + point = operand.$geometry; + distance = function (value) { + // XXX: for now, we don't calculate the actual distance between, say, + // polygon and circle. If people care about this use-case it will get + // a priority. + if (!value) + return null; + if(!value.type) + return GeoJSON.pointDistance(point, + { type: "Point", coordinates: pointToArray(value) }); + if (value.type === "Point") { + return GeoJSON.pointDistance(point, value); + } else { + return GeoJSON.geometryWithinRadius(value, point, maxDistance) + ? 0 : maxDistance + 1; + } + }; + } else { + maxDistance = valueSelector.$maxDistance; + if (!isIndexable(operand)) + throw Error("$near argument must be coordinate pair or GeoJSON"); + point = pointToArray(operand); + distance = function (value) { + if (!isIndexable(value)) + return null; + return distanceCoordinatePairs(point, value); + }; + } + + return function (branchedValues) { + // There might be multiple points in the document that match the given + // field. Only one of them needs to be within $maxDistance, but we need to + // evaluate all of them and use the nearest one for the implicit sort + // specifier. (That's why we can't just use ELEMENT_OPERATORS here.) + // + // Note: This differs from MongoDB's implementation, where a document will + // actually show up *multiple times* in the result set, with one entry for + // each within-$maxDistance branching point. + branchedValues = expandArraysInBranches(branchedValues); + var result = {result: false}; + branchedValues.every(function (branch) { + // if operation is an update, don't skip branches, just return the first one (#3599) + if (!matcher._isUpdate){ + if (!(typeof branch.value === "object")){ + return true; + } + var curDistance = distance(branch.value); + // Skip branches that aren't real points or are too far away. + if (curDistance === null || curDistance > maxDistance) + return true; + // Skip anything that's a tie. + if (result.distance !== undefined && result.distance <= curDistance) + return true; + } + result.result = true; + result.distance = curDistance; + if (!branch.arrayIndices) + delete result.arrayIndices; + else + result.arrayIndices = branch.arrayIndices; + if (matcher._isUpdate) + return false; + return true; + }); + return result; + }; + } +}; + +// NB: We are cheating and using this function to implement 'AND' for both +// 'document matchers' and 'branched matchers'. They both return result objects +// but the argument is different: for the former it's a whole doc, whereas for +// the latter it's an array of 'branched values'. +function andSomeMatchers (subMatchers) { + if (subMatchers.length === 0) + return everythingMatcher; + if (subMatchers.length === 1) + return subMatchers[0]; + + return function (docOrBranches) { + var ret = {}; + ret.result = subMatchers.every(function (f) { + var subResult = f(docOrBranches); + // Copy a 'distance' number out of the first sub-matcher that has + // one. Yes, this means that if there are multiple $near fields in a + // query, something arbitrary happens; this appears to be consistent with + // Mongo. + if (subResult.result && subResult.distance !== undefined + && ret.distance === undefined) { + ret.distance = subResult.distance; + } + // Similarly, propagate arrayIndices from sub-matchers... but to match + // MongoDB behavior, this time the *last* sub-matcher with arrayIndices + // wins. + if (subResult.result && subResult.arrayIndices) { + ret.arrayIndices = subResult.arrayIndices; + } + return subResult.result; + }); + + // If we didn't actually match, forget any extra metadata we came up with. + if (!ret.result) { + delete ret.distance; + delete ret.arrayIndices; + } + return ret; + }; +} + +const andDocumentMatchers = andSomeMatchers; +const andBranchedMatchers = andSomeMatchers; + +function compileArrayOfDocumentSelectors (selectors, matcher, inElemMatch) { + if (!Array.isArray(selectors) || selectors.length === 0) + throw Error('$and/$or/$nor must be nonempty array'); + return selectors.map(function (subSelector) { + if (!LocalCollection._isPlainObject(subSelector)) + throw Error('$or/$and/$nor entries need to be full objects'); + return compileDocumentSelector( + subSelector, matcher, {inElemMatch: inElemMatch}); + }); +} + +// Takes in a selector that could match a full document (eg, the original +// selector). Returns a function mapping document->result object. +// +// matcher is the Matcher object we are compiling. +// +// If this is the root document selector (ie, not wrapped in $and or the like), +// then isRoot is true. (This is used by $near.) +export function compileDocumentSelector (docSelector, matcher, options = {}) { + let docMatchers = []; + Object.keys(docSelector).forEach(function (key) { + let subSelector = docSelector[key]; + if (key.substr(0, 1) === '$') { + // Outer operators are either logical operators (they recurse back into + // this function), or $where. + if (!LOGICAL_OPERATORS.hasOwnProperty(key)) + throw new Error(`Unrecognized logical operator: ${key}`); + matcher._isSimple = false; + docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, + options.inElemMatch)); + } else { + // Record this path, but only if we aren't in an elemMatcher, since in an + // elemMatch this is a path inside an object in an array, not in the doc + // root. + if (!options.inElemMatch) + matcher._recordPathUsed(key); + let lookUpByIndex = makeLookupFunction(key); + let valueMatcher = + compileValueSelector(subSelector, matcher, options.isRoot); + docMatchers.push(function (doc) { + let branchValues = lookUpByIndex(doc); + return valueMatcher(branchValues); + }); + } + }); + + return andDocumentMatchers(docMatchers); +} + +// Takes in a selector that could match a key-indexed value in a document; eg, +// {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to +// indicate equality). Returns a branched matcher: a function mapping +// [branched value]->result object. +function compileValueSelector (valueSelector, matcher, isRoot) { + if (valueSelector instanceof RegExp) { + matcher._isSimple = false; + return convertElementMatcherToBranchedMatcher( + regexpElementMatcher(valueSelector)); + } else if (isOperatorObject(valueSelector)) { + return operatorBranchedMatcher(valueSelector, matcher, isRoot); + } else { + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(valueSelector)); + } +} + +// Given an element matcher (which evaluates a single value), returns a branched +// value (which evaluates the element matcher on all the branches and returns a +// more structured return value possibly including arrayIndices). +function convertElementMatcherToBranchedMatcher (elementMatcher, options) { + options = options || {}; + return function (branches) { + var expanded = branches; + if (!options.dontExpandLeafArrays) { + expanded = expandArraysInBranches( + branches, options.dontIncludeLeafArrays); + } + var ret = {}; + ret.result = expanded.some(function (element) { + var matched = elementMatcher(element.value); + + // Special case for $elemMatch: it means "true, and use this as an array + // index if I didn't already have one". + if (typeof matched === 'number') { + // XXX This code dates from when we only stored a single array index + // (for the outermost array). Should we be also including deeper array + // indices from the $elemMatch match? + if (!element.arrayIndices) + element.arrayIndices = [matched]; + matched = true; + } + + // If some element matched, and it's tagged with array indices, include + // those indices in our result object. + if (matched && element.arrayIndices) + ret.arrayIndices = element.arrayIndices; + + return matched; + }); + return ret; + }; +} + +// Helpers for $near. +function distanceCoordinatePairs (a, b) { + a = pointToArray(a); + b = pointToArray(b); + var x = a[0] - b[0]; + var y = a[1] - b[1]; + if (Number.isNaN(x) || Number.isNaN(y)) + return null; + return Math.sqrt(x * x + y * y); +} + +// Takes something that is not an operator object and returns an element matcher +// for equality with that thing. +export function equalityElementMatcher (elementSelector) { + if (isOperatorObject(elementSelector)) + throw Error("Can't create equalityValueSelector for operator object"); + + // Special-case: null and undefined are equal (if you got undefined in there + // somewhere, or if you got it due to some branch being non-existent in the + // weird special case), even though they aren't with EJSON.equals. + if (elementSelector == null) { // undefined or null + return function (value) { + return value == null; // undefined or null + }; + } + + return function (value) { + return LocalCollection._f._equal(elementSelector, value); + }; +} + +function everythingMatcher (docOrBranchedValues) { + return {result: true}; +} + +export function expandArraysInBranches (branches, skipTheArrays) { + var branchesOut = []; + branches.forEach(function (branch) { + var thisIsArray = Array.isArray(branch.value); + // We include the branch itself, *UNLESS* we it's an array that we're going + // to iterate and we're told to skip arrays. (That's right, we include some + // arrays even skipTheArrays is true: these are arrays that were found via + // explicit numerical indices.) + if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { + branchesOut.push({ + value: branch.value, + arrayIndices: branch.arrayIndices + }); + } + if (thisIsArray && !branch.dontIterate) { + branch.value.forEach(function (leaf, i) { + branchesOut.push({ + value: leaf, + arrayIndices: (branch.arrayIndices || []).concat(i) + }); + }); + } + }); + return branchesOut; +} + +// Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear. +function getOperandBitmask (operand, selector) { + // numeric bitmask + // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. + // Otherwise, $bitsAllSet will return an error. + if (Number.isInteger(operand) && operand >= 0) { + return new Uint8Array(new Int32Array([operand]).buffer) + } + // bindata bitmask + // You can also use an arbitrarily large BinData instance as a bitmask. + else if (EJSON.isBinary(operand)) { + return new Uint8Array(operand.buffer) + } + // position list + // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. + else if (Array.isArray(operand) && operand.every(function (e) { + return Number.isInteger(e) && e >= 0 + })) { + var buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1) + var view = new Uint8Array(buffer) + operand.forEach(function (x) { + view[x >> 3] |= (1 << (x & 0x7)) + }) + return view + } + // bad operand + else { + throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`) + } +} + +function getValueBitmask (value, length) { + // The field value must be either numerical or a BinData instance. Otherwise, $bits... will not match the current document. + // numerical + if (Number.isSafeInteger(value)) { + // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer + // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. + var buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); + var view = new Uint32Array(buffer, 0, 2) + view[0] = (value % ((1 << 16) * (1 << 16))) | 0 + view[1] = (value / ((1 << 16) * (1 << 16))) | 0 + // sign extension + if (value < 0) { + view = new Uint8Array(buffer, 2) + view.forEach(function (byte, idx) { + view[idx] = 0xff + }) + } + return new Uint8Array(buffer) + } + // bindata + else if (EJSON.isBinary(value)) { + return new Uint8Array(value.buffer) + } + // no match + return false +} + +// Returns a branched matcher that matches iff the given matcher does not. +// Note that this implicitly "deMorganizes" the wrapped function. ie, it +// means that ALL branch values need to fail to match innerBranchedMatcher. +function invertBranchedMatcher (branchedMatcher) { + return function (branchValues) { + var invertMe = branchedMatcher(branchValues); + // We explicitly choose to strip arrayIndices here: it doesn't make sense to + // say "update the array element that does not match something", at least + // in mongo-land. + return {result: !invertMe.result}; + }; +} + export function isIndexable (obj) { return Array.isArray(obj) || LocalCollection._isPlainObject(obj); } @@ -28,3 +730,397 @@ export function isOperatorObject (valueSelector, inconsistentOK) { }); return !!theseAreOperators; // {} has no operators } + +// Helper for $lt/$gt/$lte/$gte. +function makeInequality (cmpValueComparator) { + return { + compileElementSelector: function (operand) { + // Arrays never compare false with non-arrays for any inequality. + // XXX This was behavior we observed in pre-release MongoDB 2.5, but + // it seems to have been reverted. + // See https://jira.mongodb.org/browse/SERVER-11444 + if (Array.isArray(operand)) { + return function () { + return false; + }; + } + + // Special case: consider undefined and null the same (so true with + // $gte/$lte). + if (operand === undefined) + operand = null; + + var operandType = LocalCollection._f._type(operand); + + return function (value) { + if (value === undefined) + value = null; + // Comparisons are never true among things of different type (except + // null vs undefined). + if (LocalCollection._f._type(value) !== operandType) + return false; + return cmpValueComparator(LocalCollection._f._cmp(value, operand)); + }; + } + }; +} + +// makeLookupFunction(key) returns a lookup function. +// +// A lookup function takes in a document and returns an array of matching +// branches. If no arrays are found while looking up the key, this array will +// have exactly one branches (possibly 'undefined', if some segment of the key +// was not found). +// +// If arrays are found in the middle, this can have more than one element, since +// we 'branch'. When we 'branch', if there are more key segments to look up, +// then we only pursue branches that are plain objects (not arrays or scalars). +// This means we can actually end up with no branches! +// +// We do *NOT* branch on arrays that are found at the end (ie, at the last +// dotted member of the key). We just return that array; if you want to +// effectively 'branch' over the array's values, post-process the lookup +// function with expandArraysInBranches. +// +// Each branch is an object with keys: +// - value: the value at the branch +// - dontIterate: an optional bool; if true, it means that 'value' is an array +// that expandArraysInBranches should NOT expand. This specifically happens +// when there is a numeric index in the key, and ensures the +// perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT +// match {a: [[5]]}. +// - arrayIndices: if any array indexing was done during lookup (either due to +// explicit numeric indices or implicit branching), this will be an array of +// the array indices used, from outermost to innermost; it is falsey or +// absent if no array index is used. If an explicit numeric index is used, +// the index will be followed in arrayIndices by the string 'x'. +// +// Note: arrayIndices is used for two purposes. First, it is used to +// implement the '$' modifier feature, which only ever looks at its first +// element. +// +// Second, it is used for sort key generation, which needs to be able to tell +// the difference between different paths. Moreover, it needs to +// differentiate between explicit and implicit branching, which is why +// there's the somewhat hacky 'x' entry: this means that explicit and +// implicit array lookups will have different full arrayIndices paths. (That +// code only requires that different paths have different arrayIndices; it +// doesn't actually 'parse' arrayIndices. As an alternative, arrayIndices +// could contain objects with flags like 'implicit', but I think that only +// makes the code surrounding them more complex.) +// +// (By the way, this field ends up getting passed around a lot without +// cloning, so never mutate any arrayIndices field/var in this package!) +// +// +// At the top level, you may only pass in a plain object or array. +// +// See the test 'minimongo - lookup' for some examples of what lookup functions +// return. +export function makeLookupFunction (key, options) { + options = options || {}; + var parts = key.split('.'); + var firstPart = parts.length ? parts[0] : ''; + var firstPartIsNumeric = isNumericKey(firstPart); + var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); + var lookupRest; + if (parts.length > 1) { + lookupRest = makeLookupFunction(parts.slice(1).join('.')); + } + + var omitUnnecessaryFields = function (retVal) { + if (!retVal.dontIterate) + delete retVal.dontIterate; + if (retVal.arrayIndices && !retVal.arrayIndices.length) + delete retVal.arrayIndices; + return retVal; + }; + + // Doc will always be a plain object or an array. + // apply an explicit numeric index, an array. + return function (doc, arrayIndices) { + if (!arrayIndices) + arrayIndices = []; + + if (Array.isArray(doc)) { + // If we're being asked to do an invalid lookup into an array (non-integer + // or out-of-bounds), return no results (which is different from returning + // a single undefined result, in that `null` equality checks won't match). + if (!(firstPartIsNumeric && firstPart < doc.length)) + return []; + + // Remember that we used this array index. Include an 'x' to indicate that + // the previous index came from being considered as an explicit array + // index (not branching). + arrayIndices = arrayIndices.concat(+firstPart, 'x'); + } + + // Do our first lookup. + var firstLevel = doc[firstPart]; + + // If there is no deeper to dig, return what we found. + // + // If what we found is an array, most value selectors will choose to treat + // the elements of the array as matchable values in their own right, but + // that's done outside of the lookup function. (Exceptions to this are $size + // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a: + // [[1, 2]]}.) + // + // That said, if we just did an *explicit* array lookup (on doc) to find + // firstLevel, and firstLevel is an array too, we do NOT want value + // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}. + // So in that case, we mark the return value as 'don't iterate'. + if (!lookupRest) { + return [omitUnnecessaryFields({ + value: firstLevel, + dontIterate: Array.isArray(doc) && Array.isArray(firstLevel), + arrayIndices: arrayIndices})]; + } + + // We need to dig deeper. But if we can't, because what we've found is not + // an array or plain object, we're done. If we just did a numeric index into + // an array, we return nothing here (this is a change in Mongo 2.5 from + // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise, + // return a single `undefined` (which can, for example, match via equality + // with `null`). + if (!isIndexable(firstLevel)) { + if (Array.isArray(doc)) + return []; + return [omitUnnecessaryFields({value: undefined, + arrayIndices: arrayIndices})]; + } + + var result = []; + var appendToResult = function (more) { + Array.prototype.push.apply(result, more); + }; + + // Dig deeper: look up the rest of the parts on whatever we've found. + // (lookupRest is smart enough to not try to do invalid lookups into + // firstLevel if it's an array.) + appendToResult(lookupRest(firstLevel, arrayIndices)); + + // If we found an array, then in *addition* to potentially treating the next + // part as a literal integer lookup, we should also 'branch': try to look up + // the rest of the parts on each array element in parallel. + // + // In this case, we *only* dig deeper into array elements that are plain + // objects. (Recall that we only got this far if we have further to dig.) + // This makes sense: we certainly don't dig deeper into non-indexable + // objects. And it would be weird to dig into an array: it's simpler to have + // a rule that explicit integer indexes only apply to an outer array, not to + // an array you find after a branching search. + // + // In the special case of a numeric part in a *sort selector* (not a query + // selector), we skip the branching: we ONLY allow the numeric part to mean + // 'look up this index' in that case, not 'also look up this index in all + // the elements of the array'. + if (Array.isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { + firstLevel.forEach(function (branch, arrayIndex) { + if (LocalCollection._isPlainObject(branch)) { + appendToResult(lookupRest( + branch, + arrayIndices.concat(arrayIndex))); + } + }); + } + + return result; + }; +} + +// Object exported only for unit testing. +// Use it to export private functions to test in Tinytest. +MinimongoTest = {makeLookupFunction}; +MinimongoError = function (message, options = {}) { + if (typeof message === "string" && options.field) { + message += ` for field '${options.field}'`; + } + + var e = new Error(message); + e.name = "MinimongoError"; + return e; +}; + +export function nothingMatcher (docOrBranchedValues) { + return {result: false}; +} + +// Takes an operator object (an object with $ keys) and returns a branched +// matcher for it. +function operatorBranchedMatcher (valueSelector, matcher, isRoot) { + // Each valueSelector works separately on the various branches. So one + // operator can match one branch and another can match another branch. This + // is OK. + + var operatorMatchers = []; + Object.keys(valueSelector).forEach(function (operator) { + var operand = valueSelector[operator]; + var simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && + typeof operand === 'number'; + var simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); + var simpleInclusion = ['$in', '$nin'].includes(operator) && + Array.isArray(operand) && !operand.some(function (x) { return x === Object(x); }); + + if (! (simpleRange || simpleInclusion || simpleEquality)) { + matcher._isSimple = false; + } + + if (VALUE_OPERATORS.hasOwnProperty(operator)) { + operatorMatchers.push( + VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); + } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { + var options = ELEMENT_OPERATORS[operator]; + operatorMatchers.push( + convertElementMatcherToBranchedMatcher( + options.compileElementSelector( + operand, valueSelector, matcher), + options)); + } else { + throw new Error("Unrecognized operator: " + operator); + } + }); + + return andBranchedMatchers(operatorMatchers); +} + +// paths - Array: list of mongo style paths +// newLeafFn - Function: of form function(path) should return a scalar value to +// put into list created for that path +// conflictFn - Function: of form function(node, path, fullPath) is called +// when building a tree path for 'fullPath' node on +// 'path' was already a leaf with a value. Must return a +// conflict resolution. +// initial tree - Optional Object: starting tree. +// @returns - Object: tree represented as a set of nested objects +export function pathsToTree (paths, newLeafFn, conflictFn, tree) { + tree = tree || {}; + paths.forEach(function (keyPath) { + var treePos = tree; + var pathArr = keyPath.split('.'); + + // use .every just for iteration with break + var success = pathArr.slice(0, -1).every(function (key, idx) { + if (!treePos.hasOwnProperty(key)) + treePos[key] = {}; + else if (treePos[key] !== Object(treePos[key])) { + treePos[key] = conflictFn(treePos[key], + pathArr.slice(0, idx + 1).join('.'), + keyPath); + // break out of loop if we are failing for this path + if (treePos[key] !== Object(treePos[key])) + return false; + } + + treePos = treePos[key]; + return true; + }); + + if (success) { + var lastKey = pathArr[pathArr.length - 1]; + if (!treePos.hasOwnProperty(lastKey)) + treePos[lastKey] = newLeafFn(keyPath); + else + treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); + } + }); + + return tree; +} + +// Makes sure we get 2 elements array and assume the first one to be x and +// the second one to y no matter what user passes. +// In case user passes { lon: x, lat: y } returns [x, y] +function pointToArray (point) { + return Array.isArray(point) ? point.slice() : [point.x, point.y]; +} + +// Traverses the keys of passed projection and constructs a tree where all +// leaves are either all True or all False +// @returns Object: +// - tree - Object - tree representation of keys involved in projection +// (exception for '_id' as it is a special case handled separately) +// - including - Boolean - "take only certain fields" type of projection +export function projectionDetails (fields) { + // Find the non-_id keys (_id is handled specially because it is included unless + // explicitly excluded). Sort the keys, so that our code to detect overlaps + // like 'foo' and 'foo.bar' can assume that 'foo' comes first. + var fieldsKeys = Object.keys(fields).sort(); + + // If _id is the only field in the projection, do not remove it, since it is + // required to determine if this is an exclusion or exclusion. Also keep an + // inclusive _id, since inclusive _id follows the normal rules about mixing + // inclusive and exclusive fields. If _id is not the only field in the + // projection and is exclusive, remove it so it can be handled later by a + // special case, since exclusive _id is always allowed. + if (fieldsKeys.length > 0 && + !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && + !(fieldsKeys.includes('_id') && fields['_id'])) + fieldsKeys = fieldsKeys.filter(function (key) { return key !== '_id'; }); + + var including = null; // Unknown + + fieldsKeys.forEach(function (keyPath) { + var rule = !!fields[keyPath]; + if (including === null) + including = rule; + if (including !== rule) + // This error message is copied from MongoDB shell + throw MinimongoError("You cannot currently mix including and excluding fields."); + }); + + + var projectionRulesTree = pathsToTree( + fieldsKeys, + function (path) { return including; }, + function (node, path, fullPath) { + // Check passed projection fields' keys: If you have two rules such as + // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If + // that happens, there is a probability you are doing something wrong, + // framework should notify you about such mistake earlier on cursor + // compilation step than later during runtime. Note, that real mongo + // doesn't do anything about it and the later rule appears in projection + // project, more priority it takes. + // + // Example, assume following in mongo shell: + // > db.coll.insert({ a: { b: 23, c: 44 } }) + // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) + // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } } + // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) + // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } + // + // Note, how second time the return set of keys is different. + + var currentPath = fullPath; + var anotherPath = path; + throw MinimongoError("both " + currentPath + " and " + anotherPath + + " found in fields option, using both of them may trigger " + + "unexpected behavior. Did you mean to use only one of them?"); + }); + + return { + tree: projectionRulesTree, + including: including + }; +} + +// Takes a RegExp object and returns an element matcher. +export function regexpElementMatcher (regexp) { + return function (value) { + if (value instanceof RegExp) { + return value.toString() === regexp.toString(); + } + // Regexps only work against strings. + if (typeof value !== 'string') + return false; + + // Reset regexp's state to avoid inconsistent matching for objects with the + // same value on consecutive calls of regexp.test. This happens only if the + // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for + // which we should *not* change the lastIndex but MongoDB doesn't support + // either of these flags. + regexp.lastIndex = 0; + + return regexp.test(value); + }; +} diff --git a/packages/minimongo/common_main.js b/packages/minimongo/common_main.js deleted file mode 100644 index 6d784d22fb..0000000000 --- a/packages/minimongo/common_main.js +++ /dev/null @@ -1,5 +0,0 @@ -import {LocalCollection} from './local_collection.js'; -import {Matcher} from './matcher.js'; -import {Sorter} from './sorter.js'; - -Minimongo = {LocalCollection, Matcher, Sorter}; diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 5f155ae6f2..aa98fffc4d 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1,5 +1,11 @@ import {Cursor} from './cursor.js'; import {ObserveHandle} from './observe_handle.js'; +import { + isIndexable, + isNumericKey, + isOperatorObject, + projectionDetails +} from './common.js'; // XXX type checking on selectors (graceful error if malformed) @@ -1633,116 +1639,3 @@ function objectOnlyHasDollarKeys (object) { const keys = Object.keys(object); return keys.length > 0 && keys.every(key => key.charAt(0) === '$'); } - -// paths - Array: list of mongo style paths -// newLeafFn - Function: of form function(path) should return a scalar value to -// put into list created for that path -// conflictFn - Function: of form function(node, path, fullPath) is called -// when building a tree path for 'fullPath' node on -// 'path' was already a leaf with a value. Must return a -// conflict resolution. -// initial tree - Optional Object: starting tree. -// @returns - Object: tree represented as a set of nested objects -function pathsToTree (paths, newLeafFn, conflictFn, tree) { - tree = tree || {}; - paths.forEach(function (keyPath) { - var treePos = tree; - var pathArr = keyPath.split('.'); - - // use .every just for iteration with break - var success = pathArr.slice(0, -1).every(function (key, idx) { - if (!treePos.hasOwnProperty(key)) - treePos[key] = {}; - else if (treePos[key] !== Object(treePos[key])) { - treePos[key] = conflictFn(treePos[key], - pathArr.slice(0, idx + 1).join('.'), - keyPath); - // break out of loop if we are failing for this path - if (treePos[key] !== Object(treePos[key])) - return false; - } - - treePos = treePos[key]; - return true; - }); - - if (success) { - var lastKey = pathArr[pathArr.length - 1]; - if (!treePos.hasOwnProperty(lastKey)) - treePos[lastKey] = newLeafFn(keyPath); - else - treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); - } - }); - - return tree; -} - -// Traverses the keys of passed projection and constructs a tree where all -// leaves are either all True or all False -// @returns Object: -// - tree - Object - tree representation of keys involved in projection -// (exception for '_id' as it is a special case handled separately) -// - including - Boolean - "take only certain fields" type of projection -function projectionDetails (fields) { - // Find the non-_id keys (_id is handled specially because it is included unless - // explicitly excluded). Sort the keys, so that our code to detect overlaps - // like 'foo' and 'foo.bar' can assume that 'foo' comes first. - var fieldsKeys = Object.keys(fields).sort(); - - // If _id is the only field in the projection, do not remove it, since it is - // required to determine if this is an exclusion or exclusion. Also keep an - // inclusive _id, since inclusive _id follows the normal rules about mixing - // inclusive and exclusive fields. If _id is not the only field in the - // projection and is exclusive, remove it so it can be handled later by a - // special case, since exclusive _id is always allowed. - if (fieldsKeys.length > 0 && - !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && - !(fieldsKeys.includes('_id') && fields['_id'])) - fieldsKeys = fieldsKeys.filter(function (key) { return key !== '_id'; }); - - var including = null; // Unknown - - fieldsKeys.forEach(function (keyPath) { - var rule = !!fields[keyPath]; - if (including === null) - including = rule; - if (including !== rule) - // This error message is copied from MongoDB shell - throw MinimongoError("You cannot currently mix including and excluding fields."); - }); - - - var projectionRulesTree = pathsToTree( - fieldsKeys, - function (path) { return including; }, - function (node, path, fullPath) { - // Check passed projection fields' keys: If you have two rules such as - // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If - // that happens, there is a probability you are doing something wrong, - // framework should notify you about such mistake earlier on cursor - // compilation step than later during runtime. Note, that real mongo - // doesn't do anything about it and the later rule appears in projection - // project, more priority it takes. - // - // Example, assume following in mongo shell: - // > db.coll.insert({ a: { b: 23, c: 44 } }) - // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) - // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } } - // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) - // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } - // - // Note, how second time the return set of keys is different. - - var currentPath = fullPath; - var anotherPath = path; - throw MinimongoError("both " + currentPath + " and " + anotherPath + - " found in fields option, using both of them may trigger " + - "unexpected behavior. Did you mean to use only one of them?"); - }); - - return { - tree: projectionRulesTree, - including: including - }; -} diff --git a/packages/minimongo/main.js b/packages/minimongo/main.js new file mode 100644 index 0000000000..45cdf002f0 --- /dev/null +++ b/packages/minimongo/main.js @@ -0,0 +1,6 @@ +import {LocalCollection as LocalCollection_} from './local_collection.js'; +import {Matcher} from './matcher.js'; +import {Sorter} from './sorter.js'; + +Minimongo = {LocalCollection: LocalCollection_, Matcher, Sorter}; +LocalCollection = LocalCollection_; diff --git a/packages/minimongo/server_main.js b/packages/minimongo/main_server.js similarity index 98% rename from packages/minimongo/server_main.js rename to packages/minimongo/main_server.js index 27a5748bad..241a0e0f0a 100644 --- a/packages/minimongo/server_main.js +++ b/packages/minimongo/main_server.js @@ -1,4 +1,10 @@ -import './common_main.js'; +import './main.js'; +import { + isNumericKey, + isOperatorObject, + pathsToTree, + projectionDetails +} from './common.js'; Minimongo._pathsElidingNumericKeys = function (paths) { var self = this; @@ -278,6 +284,7 @@ function onlyContainsKeys (obj, keys) { function pathHasNumericKeys (path) { return path.split('.').some(isNumericKey); +} // XXX from Underscore.String (http://epeli.github.com/underscore.string/) function startsWith(str, starts) { diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index b037e9cf43..832db9c1ec 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -1,8 +1,7 @@ import {LocalCollection} from './local_collection.js'; import { - isIndexable, - isNumericKey, - isOperatorObject, + compileDocumentSelector, + nothingMatcher } from './common.js'; // The minimongo selector compiler! @@ -285,986 +284,3 @@ LocalCollection._f = { throw Error("Unknown type to sort"); } }; - -// Each element selector contains: -// - compileElementSelector, a function with args: -// - operand - the "right hand side" of the operator -// - valueSelector - the "context" for the operator (so that $regex can find -// $options) -// - matcher - the Matcher this is going into (so that $elemMatch can compile -// more things) -// returning a function mapping a single value to bool. -// - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from -// being called -// - dontIncludeLeafArrays, a bool which causes an argument to be passed to -// expandArraysInBranches if it is called -const ELEMENT_OPERATORS = { - $lt: makeInequality(function (cmpValue) { - return cmpValue < 0; - }), - $gt: makeInequality(function (cmpValue) { - return cmpValue > 0; - }), - $lte: makeInequality(function (cmpValue) { - return cmpValue <= 0; - }), - $gte: makeInequality(function (cmpValue) { - return cmpValue >= 0; - }), - $mod: { - compileElementSelector: function (operand) { - if (!(Array.isArray(operand) && operand.length === 2 - && typeof(operand[0]) === 'number' - && typeof(operand[1]) === 'number')) { - throw Error("argument to $mod must be an array of two numbers"); - } - // XXX could require to be ints or round or something - var divisor = operand[0]; - var remainder = operand[1]; - return function (value) { - return typeof value === 'number' && value % divisor === remainder; - }; - } - }, - $in: { - compileElementSelector: function (operand) { - if (!Array.isArray(operand)) - throw Error("$in needs an array"); - - var elementMatchers = []; - operand.forEach(function (option) { - if (option instanceof RegExp) - elementMatchers.push(regexpElementMatcher(option)); - else if (isOperatorObject(option)) - throw Error("cannot nest $ under $in"); - else - elementMatchers.push(equalityElementMatcher(option)); - }); - - return function (value) { - // Allow {a: {$in: [null]}} to match when 'a' does not exist. - if (value === undefined) - value = null; - return elementMatchers.some(function (e) { - return e(value); - }); - }; - } - }, - $size: { - // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we - // don't want to consider the element [5,5] in the leaf array [[5,5]] as a - // possible value. - dontExpandLeafArrays: true, - compileElementSelector: function (operand) { - if (typeof operand === 'string') { - // Don't ask me why, but by experimentation, this seems to be what Mongo - // does. - operand = 0; - } else if (typeof operand !== 'number') { - throw Error("$size needs a number"); - } - return function (value) { - return Array.isArray(value) && value.length === operand; - }; - } - }, - $type: { - // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should - // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a: - // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but - // should *not* include it itself. - dontIncludeLeafArrays: true, - compileElementSelector: function (operand) { - if (typeof operand !== 'number') - throw Error("$type needs a number"); - return function (value) { - return value !== undefined - && LocalCollection._f._type(value) === operand; - }; - } - }, - $bitsAllSet: { - compileElementSelector: function (operand) { - var op = getOperandBitmask(operand, '$bitsAllSet') - return function (value) { - var bitmask = getValueBitmask(value, op.length) - return bitmask && op.every(function (byte, idx) { - return ((bitmask[idx] & byte) == byte) - }) - } - } - }, - $bitsAnySet: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAnySet') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.some(function (byte, idx) { - return ((~bitmask[idx] & byte) !== byte) - }) - } - } - }, - $bitsAllClear: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAllClear') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.every(function (byte, idx) { - return !(bitmask[idx] & byte) - }) - } - } - }, - $bitsAnyClear: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAnyClear') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.some(function (byte, idx) { - return ((bitmask[idx] & byte) !== byte) - }) - } - } - }, - $regex: { - compileElementSelector: function (operand, valueSelector) { - if (!(typeof operand === 'string' || operand instanceof RegExp)) - throw Error("$regex has to be a string or RegExp"); - - var regexp; - if (valueSelector.$options !== undefined) { - // Options passed in $options (even the empty string) always overrides - // options in the RegExp object itself. (See also - // Mongo.Collection._rewriteSelector.) - - // Be clear that we only support the JS-supported options, not extended - // ones (eg, Mongo supports x and s). Ideally we would implement x and s - // by transforming the regexp, but not today... - if (/[^gim]/.test(valueSelector.$options)) - throw new Error("Only the i, m, and g regexp options are supported"); - - var regexSource = operand instanceof RegExp ? operand.source : operand; - regexp = new RegExp(regexSource, valueSelector.$options); - } else if (operand instanceof RegExp) { - regexp = operand; - } else { - regexp = new RegExp(operand); - } - return regexpElementMatcher(regexp); - } - }, - $elemMatch: { - dontExpandLeafArrays: true, - compileElementSelector: function (operand, valueSelector, matcher) { - if (!LocalCollection._isPlainObject(operand)) - throw Error("$elemMatch need an object"); - - var subMatcher, isDocMatcher; - if (isOperatorObject(Object.keys(operand) - .filter(function (key) { return !Object.keys(LOGICAL_OPERATORS).includes(key); }) - .reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { - subMatcher = compileValueSelector(operand, matcher); - isDocMatcher = false; - } else { - // This is NOT the same as compileValueSelector(operand), and not just - // because of the slightly different calling convention. - // {$elemMatch: {x: 3}} means "an element has a field x:3", not - // "consists only of a field x:3". Also, regexps and sub-$ are allowed. - subMatcher = compileDocumentSelector(operand, matcher, - {inElemMatch: true}); - isDocMatcher = true; - } - - return function (value) { - if (!Array.isArray(value)) - return false; - for (var i = 0; i < value.length; ++i) { - var arrayElement = value[i]; - var arg; - if (isDocMatcher) { - // We can only match {$elemMatch: {b: 3}} against objects. - // (We can also match against arrays, if there's numeric indices, - // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) - if (!isIndexable(arrayElement)) - return false; - arg = arrayElement; - } else { - // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches - // {a: [8]} but not {a: [[8]]} - arg = [{value: arrayElement, dontIterate: true}]; - } - // XXX support $near in $elemMatch by propagating $distance? - if (subMatcher(arg).result) - return i; // specially understood to mean "use as arrayIndices" - } - return false; - }; - } - } -}; - -// Operators that appear at the top level of a document selector. -const LOGICAL_OPERATORS = { - $and: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - return andDocumentMatchers(matchers); - }, - - $or: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - - // Special case: if there is only one matcher, use it directly, *preserving* - // any arrayIndices it returns. - if (matchers.length === 1) - return matchers[0]; - - return function (doc) { - var result = matchers.some(function (f) { - return f(doc).result; - }); - // $or does NOT set arrayIndices when it has multiple - // sub-expressions. (Tested against MongoDB.) - return {result: result}; - }; - }, - - $nor: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); - return function (doc) { - var result = matchers.every(function (f) { - return !f(doc).result; - }); - // Never set arrayIndices, because we only match if nothing in particular - // 'matched' (and because this is consistent with MongoDB). - return {result: result}; - }; - }, - - $where: function (selectorValue, matcher) { - // Record that *any* path may be used. - matcher._recordPathUsed(''); - matcher._hasWhere = true; - if (!(selectorValue instanceof Function)) { - // XXX MongoDB seems to have more complex logic to decide where or or not - // to add 'return'; not sure exactly what it is. - selectorValue = Function('obj', `return ${selectorValue}`); - } - return function (doc) { - // We make the document available as both `this` and `obj`. - // XXX not sure what we should do if this throws - return {result: selectorValue.call(doc, doc)}; - }; - }, - - // This is just used as a comment in the query (in MongoDB, it also ends up in - // query logs); it has no effect on the actual selection. - $comment: function () { - return function () { - return {result: true}; - }; - } -}; - -// Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a -// document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as -// "match each branched value independently and combine with -// convertElementMatcherToBranchedMatcher". -var VALUE_OPERATORS = { - $eq: function (operand) { - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(operand)); - }, - $not: function (operand, valueSelector, matcher) { - return invertBranchedMatcher(compileValueSelector(operand, matcher)); - }, - $ne: function (operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( - equalityElementMatcher(operand))); - }, - $nin: function (operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( - ELEMENT_OPERATORS.$in.compileElementSelector(operand))); - }, - $exists: function (operand) { - var exists = convertElementMatcherToBranchedMatcher(function (value) { - return value !== undefined; - }); - return operand ? exists : invertBranchedMatcher(exists); - }, - // $options just provides options for $regex; its logic is inside $regex - $options: function (operand, valueSelector) { - if (!valueSelector.hasOwnProperty('$regex')) - throw Error("$options needs a $regex"); - return everythingMatcher; - }, - // $maxDistance is basically an argument to $near - $maxDistance: function (operand, valueSelector) { - if (!valueSelector.$near) - throw Error("$maxDistance needs a $near"); - return everythingMatcher; - }, - $all: function (operand, valueSelector, matcher) { - if (!Array.isArray(operand)) - throw Error("$all requires array"); - // Not sure why, but this seems to be what MongoDB does. - if (operand.length === 0) - return nothingMatcher; - - var branchedMatchers = []; - operand.forEach(function (criterion) { - // XXX handle $all/$elemMatch combination - if (isOperatorObject(criterion)) - throw Error("no $ expressions in $all"); - // This is always a regexp or equality selector. - branchedMatchers.push(compileValueSelector(criterion, matcher)); - }); - // andBranchedMatchers does NOT require all selectors to return true on the - // SAME branch. - return andBranchedMatchers(branchedMatchers); - }, - $near: function (operand, valueSelector, matcher, isRoot) { - if (!isRoot) - throw Error("$near can't be inside another $ operator"); - matcher._hasGeoQuery = true; - - // There are two kinds of geodata in MongoDB: legacy coordinate pairs and - // GeoJSON. They use different distance metrics, too. GeoJSON queries are - // marked with a $geometry property, though legacy coordinates can be - // matched using $geometry. - - var maxDistance, point, distance; - if (LocalCollection._isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { - // GeoJSON "2dsphere" mode. - maxDistance = operand.$maxDistance; - point = operand.$geometry; - distance = function (value) { - // XXX: for now, we don't calculate the actual distance between, say, - // polygon and circle. If people care about this use-case it will get - // a priority. - if (!value) - return null; - if(!value.type) - return GeoJSON.pointDistance(point, - { type: "Point", coordinates: pointToArray(value) }); - if (value.type === "Point") { - return GeoJSON.pointDistance(point, value); - } else { - return GeoJSON.geometryWithinRadius(value, point, maxDistance) - ? 0 : maxDistance + 1; - } - }; - } else { - maxDistance = valueSelector.$maxDistance; - if (!isIndexable(operand)) - throw Error("$near argument must be coordinate pair or GeoJSON"); - point = pointToArray(operand); - distance = function (value) { - if (!isIndexable(value)) - return null; - return distanceCoordinatePairs(point, value); - }; - } - - return function (branchedValues) { - // There might be multiple points in the document that match the given - // field. Only one of them needs to be within $maxDistance, but we need to - // evaluate all of them and use the nearest one for the implicit sort - // specifier. (That's why we can't just use ELEMENT_OPERATORS here.) - // - // Note: This differs from MongoDB's implementation, where a document will - // actually show up *multiple times* in the result set, with one entry for - // each within-$maxDistance branching point. - branchedValues = expandArraysInBranches(branchedValues); - var result = {result: false}; - branchedValues.every(function (branch) { - // if operation is an update, don't skip branches, just return the first one (#3599) - if (!matcher._isUpdate){ - if (!(typeof branch.value === "object")){ - return true; - } - var curDistance = distance(branch.value); - // Skip branches that aren't real points or are too far away. - if (curDistance === null || curDistance > maxDistance) - return true; - // Skip anything that's a tie. - if (result.distance !== undefined && result.distance <= curDistance) - return true; - } - result.result = true; - result.distance = curDistance; - if (!branch.arrayIndices) - delete result.arrayIndices; - else - result.arrayIndices = branch.arrayIndices; - if (matcher._isUpdate) - return false; - return true; - }); - return result; - }; - } -}; - -// NB: We are cheating and using this function to implement 'AND' for both -// 'document matchers' and 'branched matchers'. They both return result objects -// but the argument is different: for the former it's a whole doc, whereas for -// the latter it's an array of 'branched values'. -function andSomeMatchers (subMatchers) { - if (subMatchers.length === 0) - return everythingMatcher; - if (subMatchers.length === 1) - return subMatchers[0]; - - return function (docOrBranches) { - var ret = {}; - ret.result = subMatchers.every(function (f) { - var subResult = f(docOrBranches); - // Copy a 'distance' number out of the first sub-matcher that has - // one. Yes, this means that if there are multiple $near fields in a - // query, something arbitrary happens; this appears to be consistent with - // Mongo. - if (subResult.result && subResult.distance !== undefined - && ret.distance === undefined) { - ret.distance = subResult.distance; - } - // Similarly, propagate arrayIndices from sub-matchers... but to match - // MongoDB behavior, this time the *last* sub-matcher with arrayIndices - // wins. - if (subResult.result && subResult.arrayIndices) { - ret.arrayIndices = subResult.arrayIndices; - } - return subResult.result; - }); - - // If we didn't actually match, forget any extra metadata we came up with. - if (!ret.result) { - delete ret.distance; - delete ret.arrayIndices; - } - return ret; - }; -} - -const andDocumentMatchers = andSomeMatchers; -const andBranchedMatchers = andSomeMatchers; - -function compileArrayOfDocumentSelectors (selectors, matcher, inElemMatch) { - if (!Array.isArray(selectors) || selectors.length === 0) - throw Error('$and/$or/$nor must be nonempty array'); - return selectors.map(function (subSelector) { - if (!LocalCollection._isPlainObject(subSelector)) - throw Error('$or/$and/$nor entries need to be full objects'); - return compileDocumentSelector( - subSelector, matcher, {inElemMatch: inElemMatch}); - }); -} - -// Takes in a selector that could match a full document (eg, the original -// selector). Returns a function mapping document->result object. -// -// matcher is the Matcher object we are compiling. -// -// If this is the root document selector (ie, not wrapped in $and or the like), -// then isRoot is true. (This is used by $near.) -function compileDocumentSelector (docSelector, matcher, options = {}) { - let docMatchers = []; - Object.keys(docSelector).forEach(function (key) { - let subSelector = docSelector[key]; - if (key.substr(0, 1) === '$') { - // Outer operators are either logical operators (they recurse back into - // this function), or $where. - if (!LOGICAL_OPERATORS.hasOwnProperty(key)) - throw new Error(`Unrecognized logical operator: ${key}`); - matcher._isSimple = false; - docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, - options.inElemMatch)); - } else { - // Record this path, but only if we aren't in an elemMatcher, since in an - // elemMatch this is a path inside an object in an array, not in the doc - // root. - if (!options.inElemMatch) - matcher._recordPathUsed(key); - let lookUpByIndex = makeLookupFunction(key); - let valueMatcher = - compileValueSelector(subSelector, matcher, options.isRoot); - docMatchers.push(function (doc) { - let branchValues = lookUpByIndex(doc); - return valueMatcher(branchValues); - }); - } - }); - - return andDocumentMatchers(docMatchers); -} - -// Takes in a selector that could match a key-indexed value in a document; eg, -// {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to -// indicate equality). Returns a branched matcher: a function mapping -// [branched value]->result object. -function compileValueSelector (valueSelector, matcher, isRoot) { - if (valueSelector instanceof RegExp) { - matcher._isSimple = false; - return convertElementMatcherToBranchedMatcher( - regexpElementMatcher(valueSelector)); - } else if (isOperatorObject(valueSelector)) { - return operatorBranchedMatcher(valueSelector, matcher, isRoot); - } else { - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(valueSelector)); - } -} - -// Given an element matcher (which evaluates a single value), returns a branched -// value (which evaluates the element matcher on all the branches and returns a -// more structured return value possibly including arrayIndices). -function convertElementMatcherToBranchedMatcher (elementMatcher, options) { - options = options || {}; - return function (branches) { - var expanded = branches; - if (!options.dontExpandLeafArrays) { - expanded = expandArraysInBranches( - branches, options.dontIncludeLeafArrays); - } - var ret = {}; - ret.result = expanded.some(function (element) { - var matched = elementMatcher(element.value); - - // Special case for $elemMatch: it means "true, and use this as an array - // index if I didn't already have one". - if (typeof matched === 'number') { - // XXX This code dates from when we only stored a single array index - // (for the outermost array). Should we be also including deeper array - // indices from the $elemMatch match? - if (!element.arrayIndices) - element.arrayIndices = [matched]; - matched = true; - } - - // If some element matched, and it's tagged with array indices, include - // those indices in our result object. - if (matched && element.arrayIndices) - ret.arrayIndices = element.arrayIndices; - - return matched; - }); - return ret; - }; -} - -// Helpers for $near. -function distanceCoordinatePairs (a, b) { - a = pointToArray(a); - b = pointToArray(b); - var x = a[0] - b[0]; - var y = a[1] - b[1]; - if (Number.isNaN(x) || Number.isNaN(y)) - return null; - return Math.sqrt(x * x + y * y); -} - -// Takes something that is not an operator object and returns an element matcher -// for equality with that thing. -function equalityElementMatcher (elementSelector) { - if (isOperatorObject(elementSelector)) - throw Error("Can't create equalityValueSelector for operator object"); - - // Special-case: null and undefined are equal (if you got undefined in there - // somewhere, or if you got it due to some branch being non-existent in the - // weird special case), even though they aren't with EJSON.equals. - if (elementSelector == null) { // undefined or null - return function (value) { - return value == null; // undefined or null - }; - } - - return function (value) { - return LocalCollection._f._equal(elementSelector, value); - }; -} - -function everythingMatcher (docOrBranchedValues) { - return {result: true}; -} - -function expandArraysInBranches (branches, skipTheArrays) { - var branchesOut = []; - branches.forEach(function (branch) { - var thisIsArray = Array.isArray(branch.value); - // We include the branch itself, *UNLESS* we it's an array that we're going - // to iterate and we're told to skip arrays. (That's right, we include some - // arrays even skipTheArrays is true: these are arrays that were found via - // explicit numerical indices.) - if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { - branchesOut.push({ - value: branch.value, - arrayIndices: branch.arrayIndices - }); - } - if (thisIsArray && !branch.dontIterate) { - branch.value.forEach(function (leaf, i) { - branchesOut.push({ - value: leaf, - arrayIndices: (branch.arrayIndices || []).concat(i) - }); - }); - } - }); - return branchesOut; -} - -// Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear. -function getOperandBitmask (operand, selector) { - // numeric bitmask - // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. - // Otherwise, $bitsAllSet will return an error. - if (Number.isInteger(operand) && operand >= 0) { - return new Uint8Array(new Int32Array([operand]).buffer) - } - // bindata bitmask - // You can also use an arbitrarily large BinData instance as a bitmask. - else if (EJSON.isBinary(operand)) { - return new Uint8Array(operand.buffer) - } - // position list - // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. - else if (Array.isArray(operand) && operand.every(function (e) { - return Number.isInteger(e) && e >= 0 - })) { - var buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1) - var view = new Uint8Array(buffer) - operand.forEach(function (x) { - view[x >> 3] |= (1 << (x & 0x7)) - }) - return view - } - // bad operand - else { - throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`) - } -} - -function getValueBitmask (value, length) { - // The field value must be either numerical or a BinData instance. Otherwise, $bits... will not match the current document. - // numerical - if (Number.isSafeInteger(value)) { - // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer - // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. - var buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); - var view = new Uint32Array(buffer, 0, 2) - view[0] = (value % ((1 << 16) * (1 << 16))) | 0 - view[1] = (value / ((1 << 16) * (1 << 16))) | 0 - // sign extension - if (value < 0) { - view = new Uint8Array(buffer, 2) - view.forEach(function (byte, idx) { - view[idx] = 0xff - }) - } - return new Uint8Array(buffer) - } - // bindata - else if (EJSON.isBinary(value)) { - return new Uint8Array(value.buffer) - } - // no match - return false -} - -// Returns a branched matcher that matches iff the given matcher does not. -// Note that this implicitly "deMorganizes" the wrapped function. ie, it -// means that ALL branch values need to fail to match innerBranchedMatcher. -function invertBranchedMatcher (branchedMatcher) { - return function (branchValues) { - var invertMe = branchedMatcher(branchValues); - // We explicitly choose to strip arrayIndices here: it doesn't make sense to - // say "update the array element that does not match something", at least - // in mongo-land. - return {result: !invertMe.result}; - }; -} - -// Helper for $lt/$gt/$lte/$gte. -function makeInequality (cmpValueComparator) { - return { - compileElementSelector: function (operand) { - // Arrays never compare false with non-arrays for any inequality. - // XXX This was behavior we observed in pre-release MongoDB 2.5, but - // it seems to have been reverted. - // See https://jira.mongodb.org/browse/SERVER-11444 - if (Array.isArray(operand)) { - return function () { - return false; - }; - } - - // Special case: consider undefined and null the same (so true with - // $gte/$lte). - if (operand === undefined) - operand = null; - - var operandType = LocalCollection._f._type(operand); - - return function (value) { - if (value === undefined) - value = null; - // Comparisons are never true among things of different type (except - // null vs undefined). - if (LocalCollection._f._type(value) !== operandType) - return false; - return cmpValueComparator(LocalCollection._f._cmp(value, operand)); - }; - } - }; -} - -// makeLookupFunction(key) returns a lookup function. -// -// A lookup function takes in a document and returns an array of matching -// branches. If no arrays are found while looking up the key, this array will -// have exactly one branches (possibly 'undefined', if some segment of the key -// was not found). -// -// If arrays are found in the middle, this can have more than one element, since -// we 'branch'. When we 'branch', if there are more key segments to look up, -// then we only pursue branches that are plain objects (not arrays or scalars). -// This means we can actually end up with no branches! -// -// We do *NOT* branch on arrays that are found at the end (ie, at the last -// dotted member of the key). We just return that array; if you want to -// effectively 'branch' over the array's values, post-process the lookup -// function with expandArraysInBranches. -// -// Each branch is an object with keys: -// - value: the value at the branch -// - dontIterate: an optional bool; if true, it means that 'value' is an array -// that expandArraysInBranches should NOT expand. This specifically happens -// when there is a numeric index in the key, and ensures the -// perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT -// match {a: [[5]]}. -// - arrayIndices: if any array indexing was done during lookup (either due to -// explicit numeric indices or implicit branching), this will be an array of -// the array indices used, from outermost to innermost; it is falsey or -// absent if no array index is used. If an explicit numeric index is used, -// the index will be followed in arrayIndices by the string 'x'. -// -// Note: arrayIndices is used for two purposes. First, it is used to -// implement the '$' modifier feature, which only ever looks at its first -// element. -// -// Second, it is used for sort key generation, which needs to be able to tell -// the difference between different paths. Moreover, it needs to -// differentiate between explicit and implicit branching, which is why -// there's the somewhat hacky 'x' entry: this means that explicit and -// implicit array lookups will have different full arrayIndices paths. (That -// code only requires that different paths have different arrayIndices; it -// doesn't actually 'parse' arrayIndices. As an alternative, arrayIndices -// could contain objects with flags like 'implicit', but I think that only -// makes the code surrounding them more complex.) -// -// (By the way, this field ends up getting passed around a lot without -// cloning, so never mutate any arrayIndices field/var in this package!) -// -// -// At the top level, you may only pass in a plain object or array. -// -// See the test 'minimongo - lookup' for some examples of what lookup functions -// return. -function makeLookupFunction (key, options) { - options = options || {}; - var parts = key.split('.'); - var firstPart = parts.length ? parts[0] : ''; - var firstPartIsNumeric = isNumericKey(firstPart); - var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); - var lookupRest; - if (parts.length > 1) { - lookupRest = makeLookupFunction(parts.slice(1).join('.')); - } - - var omitUnnecessaryFields = function (retVal) { - if (!retVal.dontIterate) - delete retVal.dontIterate; - if (retVal.arrayIndices && !retVal.arrayIndices.length) - delete retVal.arrayIndices; - return retVal; - }; - - // Doc will always be a plain object or an array. - // apply an explicit numeric index, an array. - return function (doc, arrayIndices) { - if (!arrayIndices) - arrayIndices = []; - - if (Array.isArray(doc)) { - // If we're being asked to do an invalid lookup into an array (non-integer - // or out-of-bounds), return no results (which is different from returning - // a single undefined result, in that `null` equality checks won't match). - if (!(firstPartIsNumeric && firstPart < doc.length)) - return []; - - // Remember that we used this array index. Include an 'x' to indicate that - // the previous index came from being considered as an explicit array - // index (not branching). - arrayIndices = arrayIndices.concat(+firstPart, 'x'); - } - - // Do our first lookup. - var firstLevel = doc[firstPart]; - - // If there is no deeper to dig, return what we found. - // - // If what we found is an array, most value selectors will choose to treat - // the elements of the array as matchable values in their own right, but - // that's done outside of the lookup function. (Exceptions to this are $size - // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a: - // [[1, 2]]}.) - // - // That said, if we just did an *explicit* array lookup (on doc) to find - // firstLevel, and firstLevel is an array too, we do NOT want value - // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}. - // So in that case, we mark the return value as 'don't iterate'. - if (!lookupRest) { - return [omitUnnecessaryFields({ - value: firstLevel, - dontIterate: Array.isArray(doc) && Array.isArray(firstLevel), - arrayIndices: arrayIndices})]; - } - - // We need to dig deeper. But if we can't, because what we've found is not - // an array or plain object, we're done. If we just did a numeric index into - // an array, we return nothing here (this is a change in Mongo 2.5 from - // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise, - // return a single `undefined` (which can, for example, match via equality - // with `null`). - if (!isIndexable(firstLevel)) { - if (Array.isArray(doc)) - return []; - return [omitUnnecessaryFields({value: undefined, - arrayIndices: arrayIndices})]; - } - - var result = []; - var appendToResult = function (more) { - Array.prototype.push.apply(result, more); - }; - - // Dig deeper: look up the rest of the parts on whatever we've found. - // (lookupRest is smart enough to not try to do invalid lookups into - // firstLevel if it's an array.) - appendToResult(lookupRest(firstLevel, arrayIndices)); - - // If we found an array, then in *addition* to potentially treating the next - // part as a literal integer lookup, we should also 'branch': try to look up - // the rest of the parts on each array element in parallel. - // - // In this case, we *only* dig deeper into array elements that are plain - // objects. (Recall that we only got this far if we have further to dig.) - // This makes sense: we certainly don't dig deeper into non-indexable - // objects. And it would be weird to dig into an array: it's simpler to have - // a rule that explicit integer indexes only apply to an outer array, not to - // an array you find after a branching search. - // - // In the special case of a numeric part in a *sort selector* (not a query - // selector), we skip the branching: we ONLY allow the numeric part to mean - // 'look up this index' in that case, not 'also look up this index in all - // the elements of the array'. - if (Array.isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { - firstLevel.forEach(function (branch, arrayIndex) { - if (LocalCollection._isPlainObject(branch)) { - appendToResult(lookupRest( - branch, - arrayIndices.concat(arrayIndex))); - } - }); - } - - return result; - }; -} - -// Object exported only for unit testing. -// Use it to export private functions to test in Tinytest. -MinimongoTest = {makeLookupFunction}; -MinimongoError = function (message, options = {}) { - if (typeof message === "string" && options.field) { - message += ` for field '${options.field}'`; - } - - var e = new Error(message); - e.name = "MinimongoError"; - return e; -}; - -function nothingMatcher (docOrBranchedValues) { - return {result: false}; -} - -// Takes an operator object (an object with $ keys) and returns a branched -// matcher for it. -function operatorBranchedMatcher (valueSelector, matcher, isRoot) { - // Each valueSelector works separately on the various branches. So one - // operator can match one branch and another can match another branch. This - // is OK. - - var operatorMatchers = []; - Object.keys(valueSelector).forEach(function (operator) { - var operand = valueSelector[operator]; - var simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && - typeof operand === 'number'; - var simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); - var simpleInclusion = ['$in', '$nin'].includes(operator) && - Array.isArray(operand) && !operand.some(function (x) { return x === Object(x); }); - - if (! (simpleRange || simpleInclusion || simpleEquality)) { - matcher._isSimple = false; - } - - if (VALUE_OPERATORS.hasOwnProperty(operator)) { - operatorMatchers.push( - VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); - } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { - var options = ELEMENT_OPERATORS[operator]; - operatorMatchers.push( - convertElementMatcherToBranchedMatcher( - options.compileElementSelector( - operand, valueSelector, matcher), - options)); - } else { - throw new Error("Unrecognized operator: " + operator); - } - }); - - return andBranchedMatchers(operatorMatchers); -} - -// Makes sure we get 2 elements array and assume the first one to be x and -// the second one to y no matter what user passes. -// In case user passes { lon: x, lat: y } returns [x, y] -function pointToArray (point) { - return Array.isArray(point) ? point.slice() : [point.x, point.y]; -} - -// Takes a RegExp object and returns an element matcher. -function regexpElementMatcher (regexp) { - return function (value) { - if (value instanceof RegExp) { - return value.toString() === regexp.toString(); - } - // Regexps only work against strings. - if (typeof value !== 'string') - return false; - - // Reset regexp's state to avoid inconsistent matching for objects with the - // same value on consecutive calls of regexp.test. This happens only if the - // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for - // which we should *not* change the lastIndex but MongoDB doesn't support - // either of these flags. - regexp.lastIndex = 0; - - return regexp.test(value); - }; -} diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index 8bc09c95d0..02d421607d 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -1,4 +1,3 @@ - // Hack to make LocalCollection generate ObjectIDs by default. LocalCollection._useOID = true; diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 18238ca383..bc9682ff47 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -22,8 +22,8 @@ Package.onUse(api => { 'tracker' ]); - api.mainModule('common_main.js', 'client'); - api.mainModule('server_main.js', 'server'); + api.mainModule('main.js', 'client'); + api.mainModule('main_server.js', 'server'); }); Package.onTest(api => { diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index ee3a90d77d..b998055f64 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -1,3 +1,12 @@ +import { + ELEMENT_OPERATORS, + equalityElementMatcher, + expandArraysInBranches, + isOperatorObject, + makeLookupFunction, + regexpElementMatcher +} from './common.js'; + // Give a sort spec, which can be in any of these forms: // {"key1": 1, "key2": -1} // [["key1", "asc"], ["key2", "desc"]] From d01c0bbf88810046f7da037f3613c5b186ea14f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Tue, 11 Jul 2017 23:30:29 +0200 Subject: [PATCH 09/28] Modernization in progress. --- packages/minimongo/common.js | 397 ++--- packages/minimongo/cursor.js | 96 +- packages/minimongo/local_collection.js | 1546 +++++++++--------- packages/minimongo/main_server.js | 111 +- packages/minimongo/matcher.js | 28 +- packages/minimongo/minimongo_tests.js | 38 +- packages/minimongo/minimongo_tests_client.js | 739 ++++----- packages/minimongo/minimongo_tests_server.js | 50 +- packages/minimongo/sorter.js | 132 +- 9 files changed, 1528 insertions(+), 1609 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index a0db09f7a4..6399f762b3 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -13,40 +13,30 @@ import {LocalCollection} from './local_collection.js'; // - dontIncludeLeafArrays, a bool which causes an argument to be passed to // expandArraysInBranches if it is called export const ELEMENT_OPERATORS = { - $lt: makeInequality(function (cmpValue) { - return cmpValue < 0; - }), - $gt: makeInequality(function (cmpValue) { - return cmpValue > 0; - }), - $lte: makeInequality(function (cmpValue) { - return cmpValue <= 0; - }), - $gte: makeInequality(function (cmpValue) { - return cmpValue >= 0; - }), + $lt: makeInequality(cmpValue => cmpValue < 0), + $gt: makeInequality(cmpValue => cmpValue > 0), + $lte: makeInequality(cmpValue => cmpValue <= 0), + $gte: makeInequality(cmpValue => cmpValue >= 0), $mod: { - compileElementSelector: function (operand) { + compileElementSelector(operand) { if (!(Array.isArray(operand) && operand.length === 2 && typeof(operand[0]) === 'number' && typeof(operand[1]) === 'number')) { throw Error("argument to $mod must be an array of two numbers"); } // XXX could require to be ints or round or something - var divisor = operand[0]; - var remainder = operand[1]; - return function (value) { - return typeof value === 'number' && value % divisor === remainder; - }; + const divisor = operand[0]; + const remainder = operand[1]; + return value => typeof value === 'number' && value % divisor === remainder; } }, $in: { - compileElementSelector: function (operand) { + compileElementSelector(operand) { if (!Array.isArray(operand)) throw Error("$in needs an array"); - var elementMatchers = []; - operand.forEach(function (option) { + const elementMatchers = []; + operand.forEach(option => { if (option instanceof RegExp) elementMatchers.push(regexpElementMatcher(option)); else if (isOperatorObject(option)) @@ -55,13 +45,11 @@ export const ELEMENT_OPERATORS = { elementMatchers.push(equalityElementMatcher(option)); }); - return function (value) { + return value => { // Allow {a: {$in: [null]}} to match when 'a' does not exist. if (value === undefined) value = null; - return elementMatchers.some(function (e) { - return e(value); - }); + return elementMatchers.some(e => e(value)); }; } }, @@ -70,7 +58,7 @@ export const ELEMENT_OPERATORS = { // don't want to consider the element [5,5] in the leaf array [[5,5]] as a // possible value. dontExpandLeafArrays: true, - compileElementSelector: function (operand) { + compileElementSelector(operand) { if (typeof operand === 'string') { // Don't ask me why, but by experimentation, this seems to be what Mongo // does. @@ -78,9 +66,7 @@ export const ELEMENT_OPERATORS = { } else if (typeof operand !== 'number') { throw Error("$size needs a number"); } - return function (value) { - return Array.isArray(value) && value.length === operand; - }; + return value => Array.isArray(value) && value.length === operand; } }, $type: { @@ -89,65 +75,55 @@ export const ELEMENT_OPERATORS = { // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but // should *not* include it itself. dontIncludeLeafArrays: true, - compileElementSelector: function (operand) { + compileElementSelector(operand) { if (typeof operand !== 'number') throw Error("$type needs a number"); - return function (value) { - return value !== undefined - && LocalCollection._f._type(value) === operand; - }; + return value => value !== undefined + && LocalCollection._f._type(value) === operand; } }, $bitsAllSet: { - compileElementSelector: function (operand) { - var op = getOperandBitmask(operand, '$bitsAllSet') - return function (value) { - var bitmask = getValueBitmask(value, op.length) - return bitmask && op.every(function (byte, idx) { - return ((bitmask[idx] & byte) == byte) - }) - } + compileElementSelector(operand) { + const op = getOperandBitmask(operand, '$bitsAllSet'); + return value => { + const bitmask = getValueBitmask(value, op.length); + return bitmask && op.every((byte, idx) => (bitmask[idx] & byte) == byte); + }; } }, $bitsAnySet: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAnySet') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.some(function (byte, idx) { - return ((~bitmask[idx] & byte) !== byte) - }) - } + compileElementSelector(operand) { + const query = getOperandBitmask(operand, '$bitsAnySet'); + return value => { + const bitmask = getValueBitmask(value, query.length); + return bitmask && query.some((byte, idx) => (~bitmask[idx] & byte) !== byte); + }; } }, $bitsAllClear: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAllClear') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.every(function (byte, idx) { - return !(bitmask[idx] & byte) - }) - } + compileElementSelector(operand) { + const query = getOperandBitmask(operand, '$bitsAllClear'); + return value => { + const bitmask = getValueBitmask(value, query.length); + return bitmask && query.every((byte, idx) => !(bitmask[idx] & byte)); + }; } }, $bitsAnyClear: { - compileElementSelector: function (operand) { - var query = getOperandBitmask(operand, '$bitsAnyClear') - return function (value) { - var bitmask = getValueBitmask(value, query.length) - return bitmask && query.some(function (byte, idx) { - return ((bitmask[idx] & byte) !== byte) - }) - } + compileElementSelector(operand) { + const query = getOperandBitmask(operand, '$bitsAnyClear'); + return value => { + const bitmask = getValueBitmask(value, query.length); + return bitmask && query.some((byte, idx) => (bitmask[idx] & byte) !== byte); + }; } }, $regex: { - compileElementSelector: function (operand, valueSelector) { + compileElementSelector(operand, valueSelector) { if (!(typeof operand === 'string' || operand instanceof RegExp)) throw Error("$regex has to be a string or RegExp"); - var regexp; + let regexp; if (valueSelector.$options !== undefined) { // Options passed in $options (even the empty string) always overrides // options in the RegExp object itself. (See also @@ -159,7 +135,7 @@ export const ELEMENT_OPERATORS = { if (/[^gim]/.test(valueSelector.$options)) throw new Error("Only the i, m, and g regexp options are supported"); - var regexSource = operand instanceof RegExp ? operand.source : operand; + const regexSource = operand instanceof RegExp ? operand.source : operand; regexp = new RegExp(regexSource, valueSelector.$options); } else if (operand instanceof RegExp) { regexp = operand; @@ -171,14 +147,14 @@ export const ELEMENT_OPERATORS = { }, $elemMatch: { dontExpandLeafArrays: true, - compileElementSelector: function (operand, valueSelector, matcher) { + compileElementSelector(operand, valueSelector, matcher) { if (!LocalCollection._isPlainObject(operand)) throw Error("$elemMatch need an object"); - var subMatcher, isDocMatcher; + let subMatcher, isDocMatcher; if (isOperatorObject(Object.keys(operand) - .filter(function (key) { return !Object.keys(LOGICAL_OPERATORS).includes(key); }) - .reduce(function (a, b) { return Object.assign(a, {[b]: operand[b]}); }, {}), true)) { + .filter(key => !Object.keys(LOGICAL_OPERATORS).includes(key)) + .reduce((a, b) => Object.assign(a, {[b]: operand[b]}), {}), true)) { subMatcher = compileValueSelector(operand, matcher); isDocMatcher = false; } else { @@ -191,12 +167,12 @@ export const ELEMENT_OPERATORS = { isDocMatcher = true; } - return function (value) { + return value => { if (!Array.isArray(value)) return false; - for (var i = 0; i < value.length; ++i) { - var arrayElement = value[i]; - var arg; + for (let i = 0; i < value.length; ++i) { + const arrayElement = value[i]; + let arg; if (isDocMatcher) { // We can only match {$elemMatch: {b: 3}} against objects. // (We can also match against arrays, if there's numeric indices, @@ -221,14 +197,14 @@ export const ELEMENT_OPERATORS = { // Operators that appear at the top level of a document selector. const LOGICAL_OPERATORS = { - $and: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( + $and(subSelector, matcher, inElemMatch) { + const matchers = compileArrayOfDocumentSelectors( subSelector, matcher, inElemMatch); return andDocumentMatchers(matchers); }, - $or: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( + $or(subSelector, matcher, inElemMatch) { + const matchers = compileArrayOfDocumentSelectors( subSelector, matcher, inElemMatch); // Special case: if there is only one matcher, use it directly, *preserving* @@ -236,30 +212,26 @@ const LOGICAL_OPERATORS = { if (matchers.length === 1) return matchers[0]; - return function (doc) { - var result = matchers.some(function (f) { - return f(doc).result; - }); + return doc => { + const result = matchers.some(f => f(doc).result); // $or does NOT set arrayIndices when it has multiple // sub-expressions. (Tested against MongoDB.) - return {result: result}; + return {result}; }; }, - $nor: function (subSelector, matcher, inElemMatch) { - var matchers = compileArrayOfDocumentSelectors( + $nor(subSelector, matcher, inElemMatch) { + const matchers = compileArrayOfDocumentSelectors( subSelector, matcher, inElemMatch); - return function (doc) { - var result = matchers.every(function (f) { - return !f(doc).result; - }); + return doc => { + const result = matchers.every(f => !f(doc).result); // Never set arrayIndices, because we only match if nothing in particular // 'matched' (and because this is consistent with MongoDB). - return {result: result}; + return {result}; }; }, - $where: function (selectorValue, matcher) { + $where(selectorValue, matcher) { // Record that *any* path may be used. matcher._recordPathUsed(''); matcher._hasWhere = true; @@ -268,19 +240,19 @@ const LOGICAL_OPERATORS = { // to add 'return'; not sure exactly what it is. selectorValue = Function('obj', `return ${selectorValue}`); } - return function (doc) { - // We make the document available as both `this` and `obj`. - // XXX not sure what we should do if this throws - return {result: selectorValue.call(doc, doc)}; - }; + return doc => // We make the document available as both `this` and `obj`. + // XXX not sure what we should do if this throws + ({ + result: selectorValue.call(doc, doc) + }); }, // This is just used as a comment in the query (in MongoDB, it also ends up in // query logs); it has no effect on the actual selection. - $comment: function () { - return function () { - return {result: true}; - }; + $comment() { + return () => ({ + result: true + }); } }; @@ -289,48 +261,46 @@ const LOGICAL_OPERATORS = { // "match each branched value independently and combine with // convertElementMatcherToBranchedMatcher". const VALUE_OPERATORS = { - $eq: function (operand) { + $eq(operand) { return convertElementMatcherToBranchedMatcher( equalityElementMatcher(operand)); }, - $not: function (operand, valueSelector, matcher) { + $not(operand, valueSelector, matcher) { return invertBranchedMatcher(compileValueSelector(operand, matcher)); }, - $ne: function (operand) { + $ne(operand) { return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( equalityElementMatcher(operand))); }, - $nin: function (operand) { + $nin(operand) { return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( ELEMENT_OPERATORS.$in.compileElementSelector(operand))); }, - $exists: function (operand) { - var exists = convertElementMatcherToBranchedMatcher(function (value) { - return value !== undefined; - }); + $exists(operand) { + const exists = convertElementMatcherToBranchedMatcher(value => value !== undefined); return operand ? exists : invertBranchedMatcher(exists); }, // $options just provides options for $regex; its logic is inside $regex - $options: function (operand, valueSelector) { + $options(operand, valueSelector) { if (!valueSelector.hasOwnProperty('$regex')) throw Error("$options needs a $regex"); return everythingMatcher; }, // $maxDistance is basically an argument to $near - $maxDistance: function (operand, valueSelector) { + $maxDistance(operand, valueSelector) { if (!valueSelector.$near) throw Error("$maxDistance needs a $near"); return everythingMatcher; }, - $all: function (operand, valueSelector, matcher) { + $all(operand, valueSelector, matcher) { if (!Array.isArray(operand)) throw Error("$all requires array"); // Not sure why, but this seems to be what MongoDB does. if (operand.length === 0) return nothingMatcher; - var branchedMatchers = []; - operand.forEach(function (criterion) { + const branchedMatchers = []; + operand.forEach(criterion => { // XXX handle $all/$elemMatch combination if (isOperatorObject(criterion)) throw Error("no $ expressions in $all"); @@ -341,7 +311,7 @@ const VALUE_OPERATORS = { // SAME branch. return andBranchedMatchers(branchedMatchers); }, - $near: function (operand, valueSelector, matcher, isRoot) { + $near(operand, valueSelector, matcher, isRoot) { if (!isRoot) throw Error("$near can't be inside another $ operator"); matcher._hasGeoQuery = true; @@ -351,12 +321,12 @@ const VALUE_OPERATORS = { // marked with a $geometry property, though legacy coordinates can be // matched using $geometry. - var maxDistance, point, distance; + let maxDistance, point, distance; if (LocalCollection._isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { // GeoJSON "2dsphere" mode. maxDistance = operand.$maxDistance; point = operand.$geometry; - distance = function (value) { + distance = value => { // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get // a priority. @@ -377,14 +347,14 @@ const VALUE_OPERATORS = { if (!isIndexable(operand)) throw Error("$near argument must be coordinate pair or GeoJSON"); point = pointToArray(operand); - distance = function (value) { + distance = value => { if (!isIndexable(value)) return null; return distanceCoordinatePairs(point, value); }; } - return function (branchedValues) { + return branchedValues => { // There might be multiple points in the document that match the given // field. Only one of them needs to be within $maxDistance, but we need to // evaluate all of them and use the nearest one for the implicit sort @@ -394,14 +364,15 @@ const VALUE_OPERATORS = { // actually show up *multiple times* in the result set, with one entry for // each within-$maxDistance branching point. branchedValues = expandArraysInBranches(branchedValues); - var result = {result: false}; - branchedValues.every(function (branch) { + const result = {result: false}; + branchedValues.every(branch => { // if operation is an update, don't skip branches, just return the first one (#3599) + let curDistance; if (!matcher._isUpdate){ if (!(typeof branch.value === "object")){ return true; } - var curDistance = distance(branch.value); + curDistance = distance(branch.value); // Skip branches that aren't real points or are too far away. if (curDistance === null || curDistance > maxDistance) return true; @@ -434,10 +405,10 @@ function andSomeMatchers (subMatchers) { if (subMatchers.length === 1) return subMatchers[0]; - return function (docOrBranches) { - var ret = {}; - ret.result = subMatchers.every(function (f) { - var subResult = f(docOrBranches); + return docOrBranches => { + const ret = {}; + ret.result = subMatchers.every(f => { + const subResult = f(docOrBranches); // Copy a 'distance' number out of the first sub-matcher that has // one. Yes, this means that if there are multiple $near fields in a // query, something arbitrary happens; this appears to be consistent with @@ -470,11 +441,11 @@ const andBranchedMatchers = andSomeMatchers; function compileArrayOfDocumentSelectors (selectors, matcher, inElemMatch) { if (!Array.isArray(selectors) || selectors.length === 0) throw Error('$and/$or/$nor must be nonempty array'); - return selectors.map(function (subSelector) { + return selectors.map(subSelector => { if (!LocalCollection._isPlainObject(subSelector)) throw Error('$or/$and/$nor entries need to be full objects'); return compileDocumentSelector( - subSelector, matcher, {inElemMatch: inElemMatch}); + subSelector, matcher, {inElemMatch}); }); } @@ -487,7 +458,7 @@ function compileArrayOfDocumentSelectors (selectors, matcher, inElemMatch) { // then isRoot is true. (This is used by $near.) export function compileDocumentSelector (docSelector, matcher, options = {}) { let docMatchers = []; - Object.keys(docSelector).forEach(function (key) { + Object.keys(docSelector).forEach(key => { let subSelector = docSelector[key]; if (key.substr(0, 1) === '$') { // Outer operators are either logical operators (they recurse back into @@ -506,7 +477,7 @@ export function compileDocumentSelector (docSelector, matcher, options = {}) { let lookUpByIndex = makeLookupFunction(key); let valueMatcher = compileValueSelector(subSelector, matcher, options.isRoot); - docMatchers.push(function (doc) { + docMatchers.push(doc => { let branchValues = lookUpByIndex(doc); return valueMatcher(branchValues); }); @@ -536,17 +507,16 @@ function compileValueSelector (valueSelector, matcher, isRoot) { // Given an element matcher (which evaluates a single value), returns a branched // value (which evaluates the element matcher on all the branches and returns a // more structured return value possibly including arrayIndices). -function convertElementMatcherToBranchedMatcher (elementMatcher, options) { - options = options || {}; - return function (branches) { - var expanded = branches; +function convertElementMatcherToBranchedMatcher(elementMatcher, options = {}) { + return branches => { + let expanded = branches; if (!options.dontExpandLeafArrays) { expanded = expandArraysInBranches( branches, options.dontIncludeLeafArrays); } - var ret = {}; - ret.result = expanded.some(function (element) { - var matched = elementMatcher(element.value); + const ret = {}; + ret.result = expanded.some(element => { + let matched = elementMatcher(element.value); // Special case for $elemMatch: it means "true, and use this as an array // index if I didn't already have one". @@ -574,8 +544,8 @@ function convertElementMatcherToBranchedMatcher (elementMatcher, options) { function distanceCoordinatePairs (a, b) { a = pointToArray(a); b = pointToArray(b); - var x = a[0] - b[0]; - var y = a[1] - b[1]; + const x = a[0] - b[0]; + const y = a[1] - b[1]; if (Number.isNaN(x) || Number.isNaN(y)) return null; return Math.sqrt(x * x + y * y); @@ -591,14 +561,11 @@ export function equalityElementMatcher (elementSelector) { // somewhere, or if you got it due to some branch being non-existent in the // weird special case), even though they aren't with EJSON.equals. if (elementSelector == null) { // undefined or null - return function (value) { - return value == null; // undefined or null - }; + return value => // undefined or null + value == null; } - return function (value) { - return LocalCollection._f._equal(elementSelector, value); - }; + return value => LocalCollection._f._equal(elementSelector, value); } function everythingMatcher (docOrBranchedValues) { @@ -606,9 +573,9 @@ function everythingMatcher (docOrBranchedValues) { } export function expandArraysInBranches (branches, skipTheArrays) { - var branchesOut = []; - branches.forEach(function (branch) { - var thisIsArray = Array.isArray(branch.value); + const branchesOut = []; + branches.forEach(branch => { + const thisIsArray = Array.isArray(branch.value); // We include the branch itself, *UNLESS* we it's an array that we're going // to iterate and we're told to skip arrays. (That's right, we include some // arrays even skipTheArrays is true: these are arrays that were found via @@ -620,7 +587,7 @@ export function expandArraysInBranches (branches, skipTheArrays) { }); } if (thisIsArray && !branch.dontIterate) { - branch.value.forEach(function (leaf, i) { + branch.value.forEach((leaf, i) => { branchesOut.push({ value: leaf, arrayIndices: (branch.arrayIndices || []).concat(i) @@ -646,12 +613,10 @@ function getOperandBitmask (operand, selector) { } // position list // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. - else if (Array.isArray(operand) && operand.every(function (e) { - return Number.isInteger(e) && e >= 0 - })) { - var buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1) - var view = new Uint8Array(buffer) - operand.forEach(function (x) { + else if (Array.isArray(operand) && operand.every(e => Number.isInteger(e) && e >= 0)) { + const buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1); + const view = new Uint8Array(buffer); + operand.forEach(x => { view[x >> 3] |= (1 << (x & 0x7)) }) return view @@ -668,14 +633,14 @@ function getValueBitmask (value, length) { if (Number.isSafeInteger(value)) { // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. - var buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); - var view = new Uint32Array(buffer, 0, 2) + const buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); + let view = new Uint32Array(buffer, 0, 2); view[0] = (value % ((1 << 16) * (1 << 16))) | 0 view[1] = (value / ((1 << 16) * (1 << 16))) | 0 // sign extension if (value < 0) { view = new Uint8Array(buffer, 2) - view.forEach(function (byte, idx) { + view.forEach((byte, idx) => { view[idx] = 0xff }) } @@ -693,8 +658,8 @@ function getValueBitmask (value, length) { // Note that this implicitly "deMorganizes" the wrapped function. ie, it // means that ALL branch values need to fail to match innerBranchedMatcher. function invertBranchedMatcher (branchedMatcher) { - return function (branchValues) { - var invertMe = branchedMatcher(branchValues); + return branchValues => { + const invertMe = branchedMatcher(branchValues); // We explicitly choose to strip arrayIndices here: it doesn't make sense to // say "update the array element that does not match something", at least // in mongo-land. @@ -717,9 +682,9 @@ export function isOperatorObject (valueSelector, inconsistentOK) { if (!LocalCollection._isPlainObject(valueSelector)) return false; - var theseAreOperators = undefined; - Object.keys(valueSelector).forEach(function (selKey) { - var thisIsOperator = selKey.substr(0, 1) === '$'; + let theseAreOperators = undefined; + Object.keys(valueSelector).forEach(selKey => { + const thisIsOperator = selKey.substr(0, 1) === '$'; if (theseAreOperators === undefined) { theseAreOperators = thisIsOperator; } else if (theseAreOperators !== thisIsOperator) { @@ -734,15 +699,13 @@ export function isOperatorObject (valueSelector, inconsistentOK) { // Helper for $lt/$gt/$lte/$gte. function makeInequality (cmpValueComparator) { return { - compileElementSelector: function (operand) { + compileElementSelector(operand) { // Arrays never compare false with non-arrays for any inequality. // XXX This was behavior we observed in pre-release MongoDB 2.5, but // it seems to have been reverted. // See https://jira.mongodb.org/browse/SERVER-11444 if (Array.isArray(operand)) { - return function () { - return false; - }; + return () => false; } // Special case: consider undefined and null the same (so true with @@ -750,9 +713,9 @@ function makeInequality (cmpValueComparator) { if (operand === undefined) operand = null; - var operandType = LocalCollection._f._type(operand); + const operandType = LocalCollection._f._type(operand); - return function (value) { + return value => { if (value === undefined) value = null; // Comparisons are never true among things of different type (except @@ -817,18 +780,17 @@ function makeInequality (cmpValueComparator) { // // See the test 'minimongo - lookup' for some examples of what lookup functions // return. -export function makeLookupFunction (key, options) { - options = options || {}; - var parts = key.split('.'); - var firstPart = parts.length ? parts[0] : ''; - var firstPartIsNumeric = isNumericKey(firstPart); - var nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); - var lookupRest; +export function makeLookupFunction(key, options = {}) { + const parts = key.split('.'); + const firstPart = parts.length ? parts[0] : ''; + const firstPartIsNumeric = isNumericKey(firstPart); + const nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); + let lookupRest; if (parts.length > 1) { lookupRest = makeLookupFunction(parts.slice(1).join('.')); } - var omitUnnecessaryFields = function (retVal) { + const omitUnnecessaryFields = retVal => { if (!retVal.dontIterate) delete retVal.dontIterate; if (retVal.arrayIndices && !retVal.arrayIndices.length) @@ -838,7 +800,7 @@ export function makeLookupFunction (key, options) { // Doc will always be a plain object or an array. // apply an explicit numeric index, an array. - return function (doc, arrayIndices) { + return (doc, arrayIndices) => { if (!arrayIndices) arrayIndices = []; @@ -856,7 +818,7 @@ export function makeLookupFunction (key, options) { } // Do our first lookup. - var firstLevel = doc[firstPart]; + const firstLevel = doc[firstPart]; // If there is no deeper to dig, return what we found. // @@ -874,7 +836,7 @@ export function makeLookupFunction (key, options) { return [omitUnnecessaryFields({ value: firstLevel, dontIterate: Array.isArray(doc) && Array.isArray(firstLevel), - arrayIndices: arrayIndices})]; + arrayIndices})]; } // We need to dig deeper. But if we can't, because what we've found is not @@ -887,11 +849,11 @@ export function makeLookupFunction (key, options) { if (Array.isArray(doc)) return []; return [omitUnnecessaryFields({value: undefined, - arrayIndices: arrayIndices})]; + arrayIndices})]; } - var result = []; - var appendToResult = function (more) { + const result = []; + const appendToResult = more => { Array.prototype.push.apply(result, more); }; @@ -916,7 +878,7 @@ export function makeLookupFunction (key, options) { // 'look up this index' in that case, not 'also look up this index in all // the elements of the array'. if (Array.isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { - firstLevel.forEach(function (branch, arrayIndex) { + firstLevel.forEach((branch, arrayIndex) => { if (LocalCollection._isPlainObject(branch)) { appendToResult(lookupRest( branch, @@ -932,12 +894,12 @@ export function makeLookupFunction (key, options) { // Object exported only for unit testing. // Use it to export private functions to test in Tinytest. MinimongoTest = {makeLookupFunction}; -MinimongoError = function (message, options = {}) { +MinimongoError = (message, options = {}) => { if (typeof message === "string" && options.field) { message += ` for field '${options.field}'`; } - var e = new Error(message); + const e = new Error(message); e.name = "MinimongoError"; return e; }; @@ -953,14 +915,14 @@ function operatorBranchedMatcher (valueSelector, matcher, isRoot) { // operator can match one branch and another can match another branch. This // is OK. - var operatorMatchers = []; - Object.keys(valueSelector).forEach(function (operator) { - var operand = valueSelector[operator]; - var simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && + const operatorMatchers = []; + Object.keys(valueSelector).forEach(operator => { + const operand = valueSelector[operator]; + const simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && typeof operand === 'number'; - var simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); - var simpleInclusion = ['$in', '$nin'].includes(operator) && - Array.isArray(operand) && !operand.some(function (x) { return x === Object(x); }); + const simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); + const simpleInclusion = ['$in', '$nin'].includes(operator) && + Array.isArray(operand) && !operand.some(x => x === Object(x)); if (! (simpleRange || simpleInclusion || simpleEquality)) { matcher._isSimple = false; @@ -970,14 +932,14 @@ function operatorBranchedMatcher (valueSelector, matcher, isRoot) { operatorMatchers.push( VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { - var options = ELEMENT_OPERATORS[operator]; + const options = ELEMENT_OPERATORS[operator]; operatorMatchers.push( convertElementMatcherToBranchedMatcher( options.compileElementSelector( operand, valueSelector, matcher), options)); } else { - throw new Error("Unrecognized operator: " + operator); + throw new Error(`Unrecognized operator: ${operator}`); } }); @@ -993,14 +955,13 @@ function operatorBranchedMatcher (valueSelector, matcher, isRoot) { // conflict resolution. // initial tree - Optional Object: starting tree. // @returns - Object: tree represented as a set of nested objects -export function pathsToTree (paths, newLeafFn, conflictFn, tree) { - tree = tree || {}; - paths.forEach(function (keyPath) { - var treePos = tree; - var pathArr = keyPath.split('.'); +export function pathsToTree(paths, newLeafFn, conflictFn, tree = {}) { + paths.forEach(keyPath => { + let treePos = tree; + const pathArr = keyPath.split('.'); // use .every just for iteration with break - var success = pathArr.slice(0, -1).every(function (key, idx) { + const success = pathArr.slice(0, -1).every((key, idx) => { if (!treePos.hasOwnProperty(key)) treePos[key] = {}; else if (treePos[key] !== Object(treePos[key])) { @@ -1017,7 +978,7 @@ export function pathsToTree (paths, newLeafFn, conflictFn, tree) { }); if (success) { - var lastKey = pathArr[pathArr.length - 1]; + const lastKey = pathArr[pathArr.length - 1]; if (!treePos.hasOwnProperty(lastKey)) treePos[lastKey] = newLeafFn(keyPath); else @@ -1045,7 +1006,7 @@ export function projectionDetails (fields) { // Find the non-_id keys (_id is handled specially because it is included unless // explicitly excluded). Sort the keys, so that our code to detect overlaps // like 'foo' and 'foo.bar' can assume that 'foo' comes first. - var fieldsKeys = Object.keys(fields).sort(); + let fieldsKeys = Object.keys(fields).sort(); // If _id is the only field in the projection, do not remove it, since it is // required to determine if this is an exclusion or exclusion. Also keep an @@ -1056,12 +1017,12 @@ export function projectionDetails (fields) { if (fieldsKeys.length > 0 && !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && !(fieldsKeys.includes('_id') && fields['_id'])) - fieldsKeys = fieldsKeys.filter(function (key) { return key !== '_id'; }); + fieldsKeys = fieldsKeys.filter(key => key !== '_id'); - var including = null; // Unknown + let including = null; // Unknown - fieldsKeys.forEach(function (keyPath) { - var rule = !!fields[keyPath]; + fieldsKeys.forEach(keyPath => { + const rule = !!fields[keyPath]; if (including === null) including = rule; if (including !== rule) @@ -1070,10 +1031,10 @@ export function projectionDetails (fields) { }); - var projectionRulesTree = pathsToTree( + const projectionRulesTree = pathsToTree( fieldsKeys, - function (path) { return including; }, - function (node, path, fullPath) { + path => including, + (node, path, fullPath) => { // Check passed projection fields' keys: If you have two rules such as // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If // that happens, there is a probability you are doing something wrong, @@ -1091,22 +1052,20 @@ export function projectionDetails (fields) { // // Note, how second time the return set of keys is different. - var currentPath = fullPath; - var anotherPath = path; - throw MinimongoError("both " + currentPath + " and " + anotherPath + - " found in fields option, using both of them may trigger " + - "unexpected behavior. Did you mean to use only one of them?"); + const currentPath = fullPath; + const anotherPath = path; + throw MinimongoError(`both ${currentPath} and ${anotherPath} found in fields option, using both of them may trigger unexpected behavior. Did you mean to use only one of them?`); }); return { tree: projectionRulesTree, - including: including + including }; } // Takes a RegExp object and returns an element matcher. export function regexpElementMatcher (regexp) { - return function (value) { + return value => { if (value instanceof RegExp) { return value.toString() === regexp.toString(); } diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index ad573b0e4d..364ad26adb 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -5,7 +5,7 @@ import {LocalCollection} from './local_collection.js'; export class Cursor { // don't call this ctor directly. use LocalCollection.find(). constructor (collection, selector, options) { - var self = this; + const self = this; if (!options) options = {}; self.collection = collection; @@ -48,7 +48,7 @@ export class Cursor { * @returns {Number} */ count () { - var self = this; + const self = this; if (self.reactive) self._depend({added: true, removed: true}, @@ -66,9 +66,9 @@ export class Cursor { * @returns {Object[]} */ fetch () { - var self = this; - var res = []; - self.forEach(function (doc) { + const self = this; + const res = []; + self.forEach(doc => { res.push(doc); }); return res; @@ -89,9 +89,9 @@ export class Cursor { * @param {Any} [thisArg] An object which will be the value of `this` inside `callback`. */ forEach (callback, thisArg) { - var self = this; + const self = this; - var objects = self._getRawObjects({ordered: true}); + const objects = self._getRawObjects({ordered: true}); if (self.reactive) { self._depend({ @@ -101,7 +101,7 @@ export class Cursor { movedBefore: true}); } - objects.forEach(function (elt, i) { + objects.forEach((elt, i) => { // This doubles as a clone operation. elt = self._projectionFn(elt); @@ -125,9 +125,9 @@ export class Cursor { * @param {Any} [thisArg] An object which will be the value of `this` inside `callback`. */ map (callback, thisArg) { - var self = this; - var res = []; - self.forEach(function (doc, index) { + const self = this; + const res = []; + self.forEach((doc, index) => { res.push(callback.call(thisArg, doc, index, self)); }); return res; @@ -162,7 +162,7 @@ export class Cursor { * @param {Object} callbacks Functions to call to deliver the result set as it changes */ observe (options) { - var self = this; + const self = this; return LocalCollection._observeFromObserveChanges(self, options); } @@ -174,9 +174,9 @@ export class Cursor { * @param {Object} callbacks Functions to call to deliver the result set as it changes */ observeChanges (options) { - var self = this; + const self = this; - var ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); + const ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); // there are several places that assume you aren't combining skip/limit with // unordered observe. eg, update's EJSON.clone, and the "there are several" @@ -188,18 +188,18 @@ export class Cursor { if (self.fields && (self.fields._id === 0 || self.fields._id === false)) throw Error("You may not observe a cursor with {fields: {_id: 0}}"); - var query = { + const query = { dirty: false, matcher: self.matcher, // not fast pathed sorter: ordered && self.sorter, distances: ( self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), resultsSnapshot: null, - ordered: ordered, + ordered, cursor: self, projectionFn: self._projectionFn }; - var qid; + let qid; // Non-reactive queries call added[Before] and then never call anything // else. @@ -208,7 +208,7 @@ export class Cursor { self.collection.queries[qid] = query; } query.results = self._getRawObjects({ - ordered: ordered, distances: query.distances}); + ordered, distances: query.distances}); if (self.collection.paused) query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); @@ -219,17 +219,17 @@ export class Cursor { // furthermore, callbacks enqueue until the operation we're working on is // done. - var wrapCallback = function (f) { + const wrapCallback = f => { if (!f) - return function () {}; + return () => {}; return function (/*args*/) { - var context = this; - var args = arguments; + const context = this; + const args = arguments; if (self.collection.paused) return; - self.collection._observeQueue.queueTask(function () { + self.collection._observeQueue.queueTask(() => { f.apply(context, args); }); }; @@ -243,10 +243,10 @@ export class Cursor { } if (!options._suppress_initial && !self.collection.paused) { - var results = query.results._map || query.results; - Object.keys(results).forEach(function (key) { - var doc = results[key]; - var fields = EJSON.clone(doc); + const results = query.results._map || query.results; + Object.keys(results).forEach(key => { + const doc = results[key]; + const fields = EJSON.clone(doc); delete fields._id; if (ordered) @@ -255,10 +255,10 @@ export class Cursor { }); } - var handle = new LocalCollection.ObserveHandle; + const handle = new LocalCollection.ObserveHandle; Object.assign(handle, { collection: self.collection, - stop: function () { + stop() { if (self.reactive) delete self.collection.queries[qid]; } @@ -270,7 +270,7 @@ export class Cursor { // letting it linger across rerun and potentially get // repurposed if the same observe is performed, using logic // similar to that of Meteor.subscribe. - Tracker.onInvalidate(function () { + Tracker.onInvalidate(() => { handle.stop(); }); } @@ -290,18 +290,18 @@ export class Cursor { // XXX Maybe we need a version of observe that just calls a callback if // anything changed. _depend (changers, _allow_unordered) { - var self = this; + const self = this; if (Tracker.active) { - var v = new Tracker.Dependency; + const v = new Tracker.Dependency; v.depend(); - var notifyChange = v.changed.bind(v); + const notifyChange = v.changed.bind(v); - var options = { + const options = { _suppress_initial: true, - _allow_unordered: _allow_unordered + _allow_unordered }; - ['added', 'changed', 'removed', 'addedBefore', 'movedBefore'].forEach(function (fnName) { + ['added', 'changed', 'removed', 'addedBefore', 'movedBefore'].forEach(fnName => { if (changers[fnName]) options[fnName] = notifyChange; }); @@ -312,7 +312,7 @@ export class Cursor { } _getCollectionName () { - var self = this; + const self = this; return self.collection.name; } @@ -332,12 +332,12 @@ export class Cursor { // it will just create its own _IdMap). The observeChanges implementation uses // this to remember the distances after this function returns. _getRawObjects (options) { - var self = this; + const self = this; options = options || {}; // XXX use OrderedDict instead of array, and make IdMap and OrderedDict // compatible - var results = options.ordered ? [] : new LocalCollection._IdMap; + const results = options.ordered ? [] : new LocalCollection._IdMap; // fast path for single ID value if (self._selectorId !== undefined) { @@ -347,7 +347,7 @@ export class Cursor { if (self.skip) return results; - var selectedDoc = self.collection._docs.get(self._selectorId); + const selectedDoc = self.collection._docs.get(self._selectorId); if (selectedDoc) { if (options.ordered) results.push(selectedDoc); @@ -362,7 +362,7 @@ export class Cursor { // in the observeChanges case, distances is actually part of the "query" (ie, // live results set) object. in other cases, distances is only used inside // this function. - var distances; + let distances; if (self.matcher.hasGeoQuery() && options.ordered) { if (options.distances) { distances = options.distances; @@ -372,8 +372,8 @@ export class Cursor { } } - self.collection._docs.forEach(function (doc, id) { - var matchResult = self.matcher.documentMatches(doc); + self.collection._docs.forEach((doc, id) => { + const matchResult = self.matcher.documentMatches(doc); if (matchResult.result) { if (options.ordered) { results.push(doc); @@ -395,20 +395,20 @@ export class Cursor { return results; if (self.sorter) { - var comparator = self.sorter.getComparator({distances: distances}); + const comparator = self.sorter.getComparator({distances}); results.sort(comparator); } - var idx_start = self.skip || 0; - var idx_end = self.limit ? (self.limit + idx_start) : results.length; + const idx_start = self.skip || 0; + const idx_end = self.limit ? (self.limit + idx_start) : results.length; return results.slice(idx_start, idx_end); } _publishCursor (sub) { - var self = this; + const self = this; if (! self.collection.name) throw new Error("Can't publish a cursor from a collection without a name."); - var collection = self.collection.name; + const collection = self.collection.name; // XXX minimongo should not depend on mongo-livedata! if (! Package.mongo) { diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index aa98fffc4d..2d6fac61e0 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -11,642 +11,6 @@ import { // LocalCollection: a set of documents that supports queries and modifiers. export class LocalCollection { - static Cursor = Cursor; - - static ObserveHandle = ObserveHandle; - - // XXX maybe move these into another ObserveHelpers package or something - - // _CachingChangeObserver is an object which receives observeChanges callbacks - // and keeps a cache of the current cursor state up to date in self.docs. Users - // of this class should read the docs field but not modify it. You should pass - // the "applyChange" field as the callbacks to the underlying observeChanges - // call. Optionally, you can specify your own observeChanges callbacks which are - // invoked immediately before the docs field is updated; this object is made - // available as `this` to those callbacks. - static _CachingChangeObserver = class _CachingChangeObserver { - constructor (options) { - var self = this; - options = options || {}; - - var orderedFromCallbacks = options.callbacks && - LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); - if (options.hasOwnProperty('ordered')) { - self.ordered = options.ordered; - if (options.callbacks && options.ordered !== orderedFromCallbacks) - throw Error("ordered option doesn't match callbacks"); - } else if (options.callbacks) { - self.ordered = orderedFromCallbacks; - } else { - throw Error("must provide ordered or callbacks"); - } - var callbacks = options.callbacks || {}; - - if (self.ordered) { - self.docs = new OrderedDict(MongoID.idStringify); - self.applyChange = { - addedBefore: function (id, fields, before) { - var doc = EJSON.clone(fields); - doc._id = id; - callbacks.addedBefore && callbacks.addedBefore.call( - self, id, fields, before); - // This line triggers if we provide added with movedBefore. - callbacks.added && callbacks.added.call(self, id, fields); - // XXX could `before` be a falsy ID? Technically - // idStringify seems to allow for them -- though - // OrderedDict won't call stringify on a falsy arg. - self.docs.putBefore(id, doc, before || null); - }, - movedBefore: function (id, before) { - var doc = self.docs.get(id); - callbacks.movedBefore && callbacks.movedBefore.call(self, id, before); - self.docs.moveBefore(id, before || null); - } - }; - } else { - self.docs = new LocalCollection._IdMap; - self.applyChange = { - added: function (id, fields) { - var doc = EJSON.clone(fields); - callbacks.added && callbacks.added.call(self, id, fields); - doc._id = id; - self.docs.set(id, doc); - } - }; - } - - // The methods in _IdMap and OrderedDict used by these callbacks are - // identical. - self.applyChange.changed = function (id, fields) { - var doc = self.docs.get(id); - if (!doc) - throw new Error("Unknown id for changed: " + id); - callbacks.changed && callbacks.changed.call( - self, id, EJSON.clone(fields)); - DiffSequence.applyChanges(doc, fields); - }; - self.applyChange.removed = function (id) { - callbacks.removed && callbacks.removed.call(self, id); - self.docs.remove(id); - }; - } - }; - - static _IdMap = class _IdMap extends IdMap { - constructor () { - super(MongoID.idStringify, MongoID.idParse); - } - }; - - // Wrap a transform function to return objects that have the _id field - // of the untransformed document. This ensures that subsystems such as - // the observe-sequence package that call `observe` can keep track of - // the documents identities. - // - // - Require that it returns objects - // - If the return value has an _id field, verify that it matches the - // original _id field - // - If the return value doesn't have an _id field, add it back. - static wrapTransform = transform => { - if (! transform) - return null; - - // No need to doubly-wrap transforms. - if (transform.__wrappedTransform__) - return transform; - - var wrapped = function (doc) { - if (!doc.hasOwnProperty('_id')) { - // XXX do we ever have a transform on the oplog's collection? because that - // collection has no _id. - throw new Error("can only transform documents with _id"); - } - - var id = doc._id; - // XXX consider making tracker a weak dependency and checking Package.tracker here - var transformed = Tracker.nonreactive(function () { - return transform(doc); - }); - - if (!LocalCollection._isPlainObject(transformed)) { - throw new Error("transform must return object"); - } - - if (transformed.hasOwnProperty('_id')) { - if (!EJSON.equals(transformed._id, id)) { - throw new Error("transformed document can't have different _id"); - } - } else { - transformed._id = id; - } - return transformed; - }; - wrapped.__wrappedTransform__ = true; - return wrapped; - }; - - // XXX the sorted-query logic below is laughably inefficient. we'll - // need to come up with a better datastructure for this. - // - // XXX the logic for observing with a skip or a limit is even more - // laughably inefficient. we recompute the whole results every time! - - // This binary search puts a value between any equal values, and the first - // lesser value. - static _binarySearch = (cmp, array, value) => { - var first = 0, rangeLength = array.length; - - while (rangeLength > 0) { - var halfRange = Math.floor(rangeLength/2); - if (cmp(value, array[first + halfRange]) >= 0) { - first += halfRange + 1; - rangeLength -= halfRange + 1; - } else { - rangeLength = halfRange; - } - } - return first; - }; - - static _checkSupportedProjection = fields => { - if (fields !== Object(fields) || Array.isArray(fields)) - throw MinimongoError("fields option must be an object"); - - Object.keys(fields).forEach(function (keyPath) { - var val = fields[keyPath]; - if (keyPath.split('.').includes('$')) - throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); - if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) - throw MinimongoError("Minimongo doesn't support operators in projections yet."); - if (![1, 0, true, false].includes(val)) - throw MinimongoError("Projection values should be one of 1, 0, true, or false"); - }); - }; - - // Knows how to compile a fields projection to a predicate function. - // @returns - Function: a closure that filters out an object according to the - // fields projection rules: - // @param obj - Object: MongoDB-styled document - // @returns - Object: a document with the fields filtered out - // according to projection rules. Doesn't retain subfields - // of passed argument. - static _compileProjection = fields => { - LocalCollection._checkSupportedProjection(fields); - - var _idProjection = fields._id === undefined ? true : fields._id; - var details = projectionDetails(fields); - - // returns transformed doc according to ruleTree - var transform = function (doc, ruleTree) { - // Special case for "sets" - if (Array.isArray(doc)) - return doc.map(function (subdoc) { return transform(subdoc, ruleTree); }); - - var res = details.including ? {} : EJSON.clone(doc); - Object.keys(ruleTree).forEach(function (key) { - var rule = ruleTree[key]; - if (!doc.hasOwnProperty(key)) - return; - if (rule === Object(rule)) { - // For sub-objects/subsets we branch - if (doc[key] === Object(doc[key])) - res[key] = transform(doc[key], rule); - // Otherwise we don't even touch this subfield - } else if (details.including) - res[key] = EJSON.clone(doc[key]); - else - delete res[key]; - }); - - return res; - }; - - return function (obj) { - var res = transform(obj, details.tree); - - if (_idProjection && obj.hasOwnProperty('_id')) - res._id = obj._id; - if (!_idProjection && res.hasOwnProperty('_id')) - delete res._id; - return res; - }; - }; - - static _diffObjects = (left, right, callbacks) => { - return DiffSequence.diffObjects(left, right, callbacks); - }; - - // ordered: bool. - // old_results and new_results: collections of documents. - // if ordered, they are arrays. - // if unordered, they are IdMaps - static _diffQueryChanges = (ordered, oldResults, newResults, observer, options) => { - return DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options); - }; - - static _diffQueryOrderedChanges = (oldResults, newResults, observer, options) => { - return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options); - }; - - static _diffQueryUnorderedChanges = (oldResults, newResults, observer, options) => { - return DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options); - }; - - static _findInOrderedResults = (query, doc) => { - if (!query.ordered) - throw new Error("Can't call _findInOrderedResults on unordered query"); - for (var i = 0; i < query.results.length; i++) - if (query.results[i] === doc) - return i; - throw Error("object missing from query"); - }; - - // If this is a selector which explicitly constrains the match by ID to a finite - // number of documents, returns a list of their IDs. Otherwise returns - // null. Note that the selector may have other restrictions so it may not even - // match those document! We care about $in and $and since those are generated - // access-controlled update and remove. - static _idsMatchedBySelector = selector => { - // Is the selector just an ID? - if (LocalCollection._selectorIsId(selector)) - return [selector]; - if (!selector) - return null; - - // Do we have an _id clause? - if (selector.hasOwnProperty('_id')) { - // Is the _id clause just an ID? - if (LocalCollection._selectorIsId(selector._id)) - return [selector._id]; - // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? - if (selector._id && selector._id.$in - && Array.isArray(selector._id.$in) - && selector._id.$in.length - && selector._id.$in.every(LocalCollection._selectorIsId)) { - return selector._id.$in; - } - return null; - } - - // If this is a top-level $and, and any of the clauses constrain their - // documents, then the whole selector is constrained by any one clause's - // constraint. (Well, by their intersection, but that seems unlikely.) - if (selector.$and && Array.isArray(selector.$and)) { - for (var i = 0; i < selector.$and.length; ++i) { - var subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); - if (subIds) - return subIds; - } - } - - return null; - }; - - static _insertInResults = (query, doc) => { - var fields = EJSON.clone(doc); - delete fields._id; - if (query.ordered) { - if (!query.sorter) { - query.addedBefore(doc._id, query.projectionFn(fields), null); - query.results.push(doc); - } else { - var i = LocalCollection._insertInSortedList( - query.sorter.getComparator({distances: query.distances}), - query.results, doc); - var next = query.results[i+1]; - if (next) - next = next._id; - else - next = null; - query.addedBefore(doc._id, query.projectionFn(fields), next); - } - query.added(doc._id, query.projectionFn(fields)); - } else { - query.added(doc._id, query.projectionFn(fields)); - query.results.set(doc._id, doc); - } - }; - - static _insertInSortedList = (cmp, array, value) => { - if (array.length === 0) { - array.push(value); - return 0; - } - - var idx = LocalCollection._binarySearch(cmp, array, value); - array.splice(idx, 0, value); - return idx; - }; - - // XXX maybe this should be EJSON.isObject, though EJSON doesn't know about - // RegExp - // XXX note that _type(undefined) === 3!!!! - static _isPlainObject = x => { - return x && LocalCollection._f._type(x) === 3; - }; - - // XXX need a strategy for passing the binding of $ into this - // function, from the compiled selector - // - // maybe just {key.up.to.just.before.dollarsign: array_index} - // - // XXX atomicity: if one modification fails, do we roll back the whole - // change? - // - // options: - // - isInsert is set when _modify is being called to compute the document to - // insert as part of an upsert operation. We use this primarily to figure - // out when to set the fields in $setOnInsert, if present. - static _modify = (doc, mod, options) => { - options = options || {}; - if (!LocalCollection._isPlainObject(mod)) - throw MinimongoError("Modifier must be an object"); - - // Make sure the caller can't mutate our data structures. - mod = EJSON.clone(mod); - - var isModifier = isOperatorObject(mod); - - var newDoc; - - if (!isModifier) { - if (mod._id && !EJSON.equals(doc._id, mod._id)) - throw MinimongoError("Cannot change the _id of a document"); - - // replace the whole document - assertHasValidFieldNames(mod); - newDoc = mod; - } else { - // apply modifiers to the doc. - newDoc = EJSON.clone(doc); - - Object.keys(mod).forEach(function (op) { - var operand = mod[op]; - var modFunc = MODIFIERS[op]; - // Treat $setOnInsert as $set if this is an insert. - if (options.isInsert && op === '$setOnInsert') - modFunc = MODIFIERS['$set']; - if (!modFunc) - throw MinimongoError("Invalid modifier specified " + op); - Object.keys(operand).forEach(function (keypath) { - var arg = operand[keypath]; - if (keypath === '') { - throw MinimongoError("An empty update path is not valid."); - } - - if (keypath === '_id' && op !== '$setOnInsert') { - throw MinimongoError("Mod on _id not allowed"); - } - - var keyparts = keypath.split('.'); - - if (!keyparts.every(Boolean)) { - throw MinimongoError( - "The update path '" + keypath + - "' contains an empty field name, which is not allowed."); - } - - var noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); - var forbidArray = (op === "$rename"); - var target = findModTarget(newDoc, keyparts, { - noCreate: NO_CREATE_MODIFIERS[op], - forbidArray: (op === "$rename"), - arrayIndices: options.arrayIndices - }); - var field = keyparts.pop(); - modFunc(target, field, arg, keypath, newDoc); - }); - }); - } - - // move new document into place. - Object.keys(doc).forEach(function (k) { - // Note: this used to be for (var k in doc) however, this does not - // work right in Opera. Deleting from a doc while iterating over it - // would sometimes cause opera to skip some keys. - if (k !== '_id') - delete doc[k]; - }); - Object.keys(newDoc).forEach(function (k) { - doc[k] = newDoc[k]; - }); - }; - - static _observeFromObserveChanges = (cursor, observeCallbacks) => { - var transform = cursor.getTransform() || function (doc) {return doc;}; - var suppressed = !!observeCallbacks._suppress_initial; - - var observeChangesCallbacks; - if (LocalCollection._observeCallbacksAreOrdered(observeCallbacks)) { - // The "_no_indices" option sets all index arguments to -1 and skips the - // linear scans required to generate them. This lets observers that don't - // need absolute indices benefit from the other features of this API -- - // relative order, transforms, and applyChanges -- without the speed hit. - var indices = !observeCallbacks._no_indices; - observeChangesCallbacks = { - addedBefore: function (id, fields, before) { - var self = this; - if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) - return; - var doc = transform(Object.assign(fields, {_id: id})); - if (observeCallbacks.addedAt) { - var index = indices - ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; - observeCallbacks.addedAt(doc, index, before); - } else { - observeCallbacks.added(doc); - } - }, - changed: function (id, fields) { - var self = this; - if (!(observeCallbacks.changedAt || observeCallbacks.changed)) - return; - var doc = EJSON.clone(self.docs.get(id)); - if (!doc) - throw new Error("Unknown id for changed: " + id); - var oldDoc = transform(EJSON.clone(doc)); - DiffSequence.applyChanges(doc, fields); - doc = transform(doc); - if (observeCallbacks.changedAt) { - var index = indices ? self.docs.indexOf(id) : -1; - observeCallbacks.changedAt(doc, oldDoc, index); - } else { - observeCallbacks.changed(doc, oldDoc); - } - }, - movedBefore: function (id, before) { - var self = this; - if (!observeCallbacks.movedTo) - return; - var from = indices ? self.docs.indexOf(id) : -1; - - var to = indices - ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; - // When not moving backwards, adjust for the fact that removing the - // document slides everything back one slot. - if (to > from) - --to; - observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))), - from, to, before || null); - }, - removed: function (id) { - var self = this; - if (!(observeCallbacks.removedAt || observeCallbacks.removed)) - return; - // technically maybe there should be an EJSON.clone here, but it's about - // to be removed from self.docs! - var doc = transform(self.docs.get(id)); - if (observeCallbacks.removedAt) { - var index = indices ? self.docs.indexOf(id) : -1; - observeCallbacks.removedAt(doc, index); - } else { - observeCallbacks.removed(doc); - } - } - }; - } else { - observeChangesCallbacks = { - added: function (id, fields) { - if (!suppressed && observeCallbacks.added) { - var doc = Object.assign(fields, {_id: id}); - observeCallbacks.added(transform(doc)); - } - }, - changed: function (id, fields) { - var self = this; - if (observeCallbacks.changed) { - var oldDoc = self.docs.get(id); - var doc = EJSON.clone(oldDoc); - DiffSequence.applyChanges(doc, fields); - observeCallbacks.changed(transform(doc), - transform(EJSON.clone(oldDoc))); - } - }, - removed: function (id) { - var self = this; - if (observeCallbacks.removed) { - observeCallbacks.removed(transform(self.docs.get(id))); - } - } - }; - } - - var changeObserver = new LocalCollection._CachingChangeObserver( - {callbacks: observeChangesCallbacks}); - var handle = cursor.observeChanges(changeObserver.applyChange); - suppressed = false; - - return handle; - }; - - static _observeCallbacksAreOrdered = callbacks => { - if (callbacks.addedAt && callbacks.added) - throw new Error("Please specify only one of added() and addedAt()"); - if (callbacks.changedAt && callbacks.changed) - throw new Error("Please specify only one of changed() and changedAt()"); - if (callbacks.removed && callbacks.removedAt) - throw new Error("Please specify only one of removed() and removedAt()"); - - return !!(callbacks.addedAt || callbacks.movedTo || callbacks.changedAt - || callbacks.removedAt); - }; - - static _observeChangesCallbacksAreOrdered = callbacks => { - if (callbacks.added && callbacks.addedBefore) - throw new Error("Please specify only one of added() and addedBefore()"); - return !!(callbacks.addedBefore || callbacks.movedBefore); - }; - - // When performing an upsert, the incoming selector object can be re-used as - // the upsert modifier object, as long as Mongo query and projection - // operators (prefixed with a $ character) are removed from the newly - // created modifier object. This function attempts to strip all $ based Mongo - // operators when creating the upsert modifier object. - // NOTE: There is a known issue here in that some Mongo $ based opeartors - // should not actually be stripped. - // See https://github.com/meteor/meteor/issues/8806. - static _removeDollarOperators = selector => { - let cleansed = {}; - Object.keys(selector).forEach((key) => { - const value = selector[key]; - if (key.charAt(0) !== '$' && !objectOnlyHasDollarKeys(value)) { - if (value !== null - && value.constructor - && Object.getPrototypeOf(value) === Object.prototype) { - cleansed[key] = LocalCollection._removeDollarOperators(value); - } else { - cleansed[key] = value; - } - } - }); - return cleansed; - }; - - static _removeFromResults = (query, doc) => { - if (query.ordered) { - var i = LocalCollection._findInOrderedResults(query, doc); - query.removed(doc._id); - query.results.splice(i, 1); - } else { - var id = doc._id; // in case callback mutates doc - query.removed(doc._id); - query.results.remove(id); - } - }; - - // Is this selector just shorthand for lookup by _id? - static _selectorIsId = selector => { - return (typeof selector === "string") || - (typeof selector === "number") || - selector instanceof MongoID.ObjectID; - }; - - // Is the selector just lookup by _id (shorthand or not)? - static _selectorIsIdPerhapsAsObject = selector => { - return LocalCollection._selectorIsId(selector) || - (selector && typeof selector === "object" && - selector._id && LocalCollection._selectorIsId(selector._id) && - Object.keys(selector).length === 1); - }; - - static _updateInResults = (query, doc, old_doc) => { - if (!EJSON.equals(doc._id, old_doc._id)) - throw new Error("Can't change a doc's _id while updating"); - var projectionFn = query.projectionFn; - var changedFields = DiffSequence.makeChangedFields( - projectionFn(doc), projectionFn(old_doc)); - - if (!query.ordered) { - if (Object.keys(changedFields).length) { - query.changed(doc._id, changedFields); - query.results.set(doc._id, doc); - } - return; - } - - var orig_idx = LocalCollection._findInOrderedResults(query, doc); - - if (Object.keys(changedFields).length) - query.changed(doc._id, changedFields); - if (!query.sorter) - return; - - // just take it out and put it back in again, and see if the index - // changes - query.results.splice(orig_idx, 1); - var new_idx = LocalCollection._insertInSortedList( - query.sorter.getComparator({distances: query.distances}), - query.results, doc); - if (orig_idx !== new_idx) { - var next = query.results[new_idx+1]; - if (next) - next = next._id; - else - next = null; - query.movedBefore && query.movedBefore(doc._id, next); - } - }; - constructor (name) { this.name = name; // _id -> document (also containing id) @@ -722,7 +86,7 @@ export class LocalCollection { // XXX possibly enforce that 'undefined' does not appear (we assume // this in our handling of null and $exists) insert (doc, callback) { - var self = this; + const self = this; doc = EJSON.clone(doc); assertHasValidFieldNames(doc); @@ -733,20 +97,20 @@ export class LocalCollection { doc._id = LocalCollection._useOID ? new MongoID.ObjectID() : Random.id(); } - var id = doc._id; + const id = doc._id; if (self._docs.has(id)) - throw MinimongoError("Duplicate _id '" + id + "'"); + throw MinimongoError(`Duplicate _id '${id}'`); self._saveOriginal(id, undefined); self._docs.set(id, doc); - var queriesToRecompute = []; + const queriesToRecompute = []; // trigger live queries that match - for (var qid in self.queries) { - var query = self.queries[qid]; + for (let qid in self.queries) { + const query = self.queries[qid]; if (query.dirty) continue; - var matchResult = query.matcher.documentMatches(doc); + const matchResult = query.matcher.documentMatches(doc); if (matchResult.result) { if (query.distances && matchResult.distance !== undefined) query.distances.set(id, matchResult.distance); @@ -757,7 +121,7 @@ export class LocalCollection { } } - queriesToRecompute.forEach(function (qid) { + queriesToRecompute.forEach(qid => { if (self.queries[qid]) self._recomputeResults(self.queries[qid]); }); @@ -766,7 +130,7 @@ export class LocalCollection { // Defer because the caller likely doesn't expect the callback to be run // immediately. if (callback) - Meteor.defer(function () { + Meteor.defer(() => { callback(null, id); }); return id; @@ -783,24 +147,24 @@ export class LocalCollection { this.paused = true; // Take a snapshot of the query results for each query. - for (var qid in this.queries) { - var query = this.queries[qid]; + for (let qid in this.queries) { + const query = this.queries[qid]; query.resultsSnapshot = EJSON.clone(query.results); } } remove (selector, callback) { - var self = this; + const self = this; // Easy special case: if we're not calling observeChanges callbacks and we're // not saving originals and we got asked to remove everything, then just empty // everything directly. if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) { - var result = self._docs.size(); + const result = self._docs.size(); self._docs.clear(); - Object.keys(self.queries).forEach(function (qid) { - var query = self.queries[qid]; + Object.keys(self.queries).forEach(qid => { + const query = self.queries[qid]; if (query.ordered) { query.results = []; } else { @@ -808,34 +172,34 @@ export class LocalCollection { } }); if (callback) { - Meteor.defer(function () { + Meteor.defer(() => { callback(null, result); }); } return result; } - var matcher = new Minimongo.Matcher(selector); - var remove = []; - self._eachPossiblyMatchingDoc(selector, function (doc, id) { + const matcher = new Minimongo.Matcher(selector); + const remove = []; + self._eachPossiblyMatchingDoc(selector, (doc, id) => { if (matcher.documentMatches(doc).result) remove.push(id); }); - var queriesToRecompute = []; - var queryRemove = []; - for (var i = 0; i < remove.length; i++) { - var removeId = remove[i]; - var removeDoc = self._docs.get(removeId); - Object.keys(self.queries).forEach(function (qid) { - var query = self.queries[qid]; + const queriesToRecompute = []; + const queryRemove = []; + for (let i = 0; i < remove.length; i++) { + const removeId = remove[i]; + const removeDoc = self._docs.get(removeId); + Object.keys(self.queries).forEach(qid => { + const query = self.queries[qid]; if (query.dirty) return; if (query.matcher.documentMatches(removeDoc).result) { if (query.cursor.skip || query.cursor.limit) queriesToRecompute.push(qid); else - queryRemove.push({qid: qid, doc: removeDoc}); + queryRemove.push({qid, doc: removeDoc}); } }); self._saveOriginal(removeId, removeDoc); @@ -843,22 +207,22 @@ export class LocalCollection { } // run live query callbacks _after_ we've removed the documents. - queryRemove.forEach(function (remove) { - var query = self.queries[remove.qid]; + queryRemove.forEach(remove => { + const query = self.queries[remove.qid]; if (query) { query.distances && query.distances.remove(remove.doc._id); LocalCollection._removeFromResults(query, remove.doc); } }); - queriesToRecompute.forEach(function (qid) { - var query = self.queries[qid]; + queriesToRecompute.forEach(qid => { + const query = self.queries[qid]; if (query) self._recomputeResults(query); }); self._observeQueue.drain(); - result = remove.length; + const result = remove.length; if (callback) - Meteor.defer(function () { + Meteor.defer(() => { callback(null, result); }); return result; @@ -869,7 +233,7 @@ export class LocalCollection { // database. Note that this is not just replaying all the changes that // happened during the pause, it is a smarter 'coalesced' diff. resumeObservers () { - var self = this; + const self = this; // No-op if not paused. if (!this.paused) return; @@ -878,8 +242,8 @@ export class LocalCollection { // observer methods won't actually fire when we trigger them. this.paused = false; - for (var qid in this.queries) { - var query = self.queries[qid]; + for (let qid in this.queries) { + const query = self.queries[qid]; if (query.dirty) { query.dirty = false; // re-compute results will perform `LocalCollection._diffQueryChanges` automatically. @@ -897,11 +261,11 @@ export class LocalCollection { } retrieveOriginals () { - var self = this; + const self = this; if (!self._savedOriginals) throw new Error("Called retrieveOriginals without saveOriginals"); - var originals = self._savedOriginals; + const originals = self._savedOriginals; self._savedOriginals = null; return originals; } @@ -914,7 +278,7 @@ export class LocalCollection { // is the value.) You must alternate between calls to saveOriginals() and // retrieveOriginals(). saveOriginals () { - var self = this; + const self = this; if (self._savedOriginals) throw new Error("Called saveOriginals twice without retrieveOriginals"); self._savedOriginals = new LocalCollection._IdMap; @@ -923,27 +287,27 @@ export class LocalCollection { // XXX atomicity: if multi is true, and one modification fails, do // we rollback the whole operation, or what? update (selector, mod, options, callback) { - var self = this; + const self = this; if (! callback && options instanceof Function) { callback = options; options = null; } if (!options) options = {}; - var matcher = new Minimongo.Matcher(selector, true); + const matcher = new Minimongo.Matcher(selector, true); // Save the original results of any query that we might need to // _recomputeResults on, because _modifyAndNotify will mutate the objects in // it. (We don't need to save the original results of paused queries because // they already have a resultsSnapshot and we won't be diffing in // _recomputeResults.) - var qidToOriginalResults = {}; + const qidToOriginalResults = {}; // We should only clone each document once, even if it appears in multiple queries - var docMap = new LocalCollection._IdMap; - var idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector); + const docMap = new LocalCollection._IdMap; + const idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector); - Object.keys(self.queries).forEach(function (qid) { - var query = self.queries[qid]; + Object.keys(self.queries).forEach(qid => { + const query = self.queries[qid]; if ((query.cursor.skip || query.cursor.limit) && ! self.paused) { // Catch the case of a reactive `count()` on a cursor with skip // or limit, which registers an unordered observe. This is a @@ -963,15 +327,13 @@ export class LocalCollection { // because it may be modified before the new and old result sets // are diffed. But if we know exactly which document IDs we're // going to modify, then we only need to clone those. - var memoizedCloneIfNeeded = function(doc) { + const memoizedCloneIfNeeded = doc => { if (docMap.has(doc._id)) { return docMap.get(doc._id); } else { - var docToMemoize; + let docToMemoize; - if (idsMatchedBySelector && !idsMatchedBySelector.some(function(id) { - return EJSON.equals(id, doc._id); - })) { + if (idsMatchedBySelector && !idsMatchedBySelector.some(id => EJSON.equals(id, doc._id))) { docToMemoize = doc; } else { docToMemoize = EJSON.clone(doc); @@ -985,12 +347,12 @@ export class LocalCollection { qidToOriginalResults[qid] = query.results.map(memoizedCloneIfNeeded); } }); - var recomputeQids = {}; + const recomputeQids = {}; - var updateCount = 0; + let updateCount = 0; - self._eachPossiblyMatchingDoc(selector, function (doc, id) { - var queryResult = matcher.documentMatches(doc); + self._eachPossiblyMatchingDoc(selector, (doc, id) => { + const queryResult = matcher.documentMatches(doc); if (queryResult.result) { // XXX Should we save the original even if mod ends up being a no-op? self._saveOriginal(id, doc); @@ -1002,8 +364,8 @@ export class LocalCollection { return true; }); - Object.keys(recomputeQids).forEach(function (qid) { - var query = self.queries[qid]; + Object.keys(recomputeQids).forEach(qid => { + const query = self.queries[qid]; if (query) self._recomputeResults(query, qidToOriginalResults[qid]); }); @@ -1012,7 +374,7 @@ export class LocalCollection { // If we are doing an upsert, and we didn't modify any documents yet, then // it's time to do an insert. Figure out what document we are inserting, and // generate an id for it. - var insertedId; + let insertedId; if (updateCount === 0 && options.upsert) { let selectorModifier = LocalCollection._selectorIsId(selector) @@ -1041,7 +403,7 @@ export class LocalCollection { // Return the number of affected documents, or in the upsert case, an object // containing the number of affected docs and the id of the doc that was // inserted, if any. - var result; + let result; if (options._returnObject) { result = { numberAffected: updateCount @@ -1053,7 +415,7 @@ export class LocalCollection { } if (callback) - Meteor.defer(function () { + Meteor.defer(() => { callback(null, result); }); return result; @@ -1063,7 +425,7 @@ export class LocalCollection { // equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject: // true }). upsert (selector, mod, options, callback) { - var self = this; + const self = this; if (! callback && typeof options === "function") { callback = options; options = {}; @@ -1079,14 +441,14 @@ export class LocalCollection { // specific _id's, it only looks at those. doc is *not* cloned: it is the // same object that is in _docs. _eachPossiblyMatchingDoc (selector, f) { - var self = this; - var specificIds = LocalCollection._idsMatchedBySelector(selector); + const self = this; + const specificIds = LocalCollection._idsMatchedBySelector(selector); if (specificIds) { - for (var i = 0; i < specificIds.length; ++i) { - var id = specificIds[i]; - var doc = self._docs.get(id); + for (let i = 0; i < specificIds.length; ++i) { + const id = specificIds[i]; + const doc = self._docs.get(id); if (doc) { - var breakIfFalse = f(doc, id); + const breakIfFalse = f(doc, id); if (breakIfFalse === false) break; } @@ -1097,11 +459,11 @@ export class LocalCollection { } _modifyAndNotify (doc, mod, recomputeQids, arrayIndices) { - var self = this; + const self = this; - var matched_before = {}; - for (var qid in self.queries) { - var query = self.queries[qid]; + const matched_before = {}; + for (let qid in self.queries) { + const query = self.queries[qid]; if (query.dirty) continue; if (query.ordered) { @@ -1113,17 +475,17 @@ export class LocalCollection { } } - var old_doc = EJSON.clone(doc); + const old_doc = EJSON.clone(doc); - LocalCollection._modify(doc, mod, {arrayIndices: arrayIndices}); + LocalCollection._modify(doc, mod, {arrayIndices}); - for (qid in self.queries) { - query = self.queries[qid]; + for (let qid in self.queries) { + const query = self.queries[qid]; if (query.dirty) continue; - var before = matched_before[qid]; - var afterMatch = query.matcher.documentMatches(doc); - var after = afterMatch.result; + const before = matched_before[qid]; + const afterMatch = query.matcher.documentMatches(doc); + const after = afterMatch.result; if (after && query.distances && afterMatch.distance !== undefined) query.distances.set(doc._id, afterMatch.distance); @@ -1158,7 +520,7 @@ export class LocalCollection { // // oldResults is guaranteed to be ignored if the query is not paused. _recomputeResults (query, oldResults) { - var self = this; + const self = this; if (self.paused) { // There's no reason to recompute the results now as we're still paused. // By flagging the query as "dirty", the recompute will be performed @@ -1182,7 +544,7 @@ export class LocalCollection { } _saveOriginal (id, doc) { - var self = this; + const self = this; // Are we even trying to save originals? if (!self._savedOriginals) return; @@ -1195,8 +557,641 @@ export class LocalCollection { } } +LocalCollection.Cursor = Cursor; + +LocalCollection.ObserveHandle = ObserveHandle; + +// XXX maybe move these into another ObserveHelpers package or something + +// _CachingChangeObserver is an object which receives observeChanges callbacks +// and keeps a cache of the current cursor state up to date in self.docs. Users +// of this class should read the docs field but not modify it. You should pass +// the "applyChange" field as the callbacks to the underlying observeChanges +// call. Optionally, you can specify your own observeChanges callbacks which are +// invoked immediately before the docs field is updated; this object is made +// available as `this` to those callbacks. +LocalCollection._CachingChangeObserver = class _CachingChangeObserver { + constructor (options) { + const self = this; + options = options || {}; + + const orderedFromCallbacks = options.callbacks && + LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); + if (options.hasOwnProperty('ordered')) { + self.ordered = options.ordered; + if (options.callbacks && options.ordered !== orderedFromCallbacks) + throw Error("ordered option doesn't match callbacks"); + } else if (options.callbacks) { + self.ordered = orderedFromCallbacks; + } else { + throw Error("must provide ordered or callbacks"); + } + const callbacks = options.callbacks || {}; + + if (self.ordered) { + self.docs = new OrderedDict(MongoID.idStringify); + self.applyChange = { + addedBefore(id, fields, before) { + const doc = EJSON.clone(fields); + doc._id = id; + callbacks.addedBefore && callbacks.addedBefore.call( + self, id, fields, before); + // This line triggers if we provide added with movedBefore. + callbacks.added && callbacks.added.call(self, id, fields); + // XXX could `before` be a falsy ID? Technically + // idStringify seems to allow for them -- though + // OrderedDict won't call stringify on a falsy arg. + self.docs.putBefore(id, doc, before || null); + }, + movedBefore(id, before) { + const doc = self.docs.get(id); + callbacks.movedBefore && callbacks.movedBefore.call(self, id, before); + self.docs.moveBefore(id, before || null); + } + }; + } else { + self.docs = new LocalCollection._IdMap; + self.applyChange = { + added(id, fields) { + const doc = EJSON.clone(fields); + callbacks.added && callbacks.added.call(self, id, fields); + doc._id = id; + self.docs.set(id, doc); + } + }; + } + + // The methods in _IdMap and OrderedDict used by these callbacks are + // identical. + self.applyChange.changed = (id, fields) => { + const doc = self.docs.get(id); + if (!doc) + throw new Error(`Unknown id for changed: ${id}`); + callbacks.changed && callbacks.changed.call( + self, id, EJSON.clone(fields)); + DiffSequence.applyChanges(doc, fields); + }; + self.applyChange.removed = id => { + callbacks.removed && callbacks.removed.call(self, id); + self.docs.remove(id); + }; + } +}; + +LocalCollection._IdMap = class _IdMap extends IdMap { + constructor () { + super(MongoID.idStringify, MongoID.idParse); + } +}; + +// Wrap a transform function to return objects that have the _id field +// of the untransformed document. This ensures that subsystems such as +// the observe-sequence package that call `observe` can keep track of +// the documents identities. +// +// - Require that it returns objects +// - If the return value has an _id field, verify that it matches the +// original _id field +// - If the return value doesn't have an _id field, add it back. +LocalCollection.wrapTransform = transform => { + if (! transform) + return null; + + // No need to doubly-wrap transforms. + if (transform.__wrappedTransform__) + return transform; + + const wrapped = doc => { + if (!doc.hasOwnProperty('_id')) { + // XXX do we ever have a transform on the oplog's collection? because that + // collection has no _id. + throw new Error("can only transform documents with _id"); + } + + const id = doc._id; + // XXX consider making tracker a weak dependency and checking Package.tracker here + const transformed = Tracker.nonreactive(() => transform(doc)); + + if (!LocalCollection._isPlainObject(transformed)) { + throw new Error("transform must return object"); + } + + if (transformed.hasOwnProperty('_id')) { + if (!EJSON.equals(transformed._id, id)) { + throw new Error("transformed document can't have different _id"); + } + } else { + transformed._id = id; + } + return transformed; + }; + wrapped.__wrappedTransform__ = true; + return wrapped; +}; + +// XXX the sorted-query logic below is laughably inefficient. we'll +// need to come up with a better datastructure for this. +// +// XXX the logic for observing with a skip or a limit is even more +// laughably inefficient. we recompute the whole results every time! + +// This binary search puts a value between any equal values, and the first +// lesser value. +LocalCollection._binarySearch = (cmp, array, value) => { + let first = 0, rangeLength = array.length; + + while (rangeLength > 0) { + const halfRange = Math.floor(rangeLength/2); + if (cmp(value, array[first + halfRange]) >= 0) { + first += halfRange + 1; + rangeLength -= halfRange + 1; + } else { + rangeLength = halfRange; + } + } + return first; +}; + +LocalCollection._checkSupportedProjection = fields => { + if (fields !== Object(fields) || Array.isArray(fields)) + throw MinimongoError("fields option must be an object"); + + Object.keys(fields).forEach(keyPath => { + const val = fields[keyPath]; + if (keyPath.split('.').includes('$')) + throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); + if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) + throw MinimongoError("Minimongo doesn't support operators in projections yet."); + if (![1, 0, true, false].includes(val)) + throw MinimongoError("Projection values should be one of 1, 0, true, or false"); + }); +}; + +// Knows how to compile a fields projection to a predicate function. +// @returns - Function: a closure that filters out an object according to the +// fields projection rules: +// @param obj - Object: MongoDB-styled document +// @returns - Object: a document with the fields filtered out +// according to projection rules. Doesn't retain subfields +// of passed argument. +LocalCollection._compileProjection = fields => { + LocalCollection._checkSupportedProjection(fields); + + const _idProjection = fields._id === undefined ? true : fields._id; + const details = projectionDetails(fields); + + // returns transformed doc according to ruleTree + const transform = (doc, ruleTree) => { + // Special case for "sets" + if (Array.isArray(doc)) + return doc.map(subdoc => transform(subdoc, ruleTree)); + + const res = details.including ? {} : EJSON.clone(doc); + Object.keys(ruleTree).forEach(key => { + const rule = ruleTree[key]; + if (!doc.hasOwnProperty(key)) + return; + if (rule === Object(rule)) { + // For sub-objects/subsets we branch + if (doc[key] === Object(doc[key])) + res[key] = transform(doc[key], rule); + // Otherwise we don't even touch this subfield + } else if (details.including) + res[key] = EJSON.clone(doc[key]); + else + delete res[key]; + }); + + return res; + }; + + return obj => { + const res = transform(obj, details.tree); + + if (_idProjection && obj.hasOwnProperty('_id')) + res._id = obj._id; + if (!_idProjection && res.hasOwnProperty('_id')) + delete res._id; + return res; + }; +}; + +LocalCollection._diffObjects = (left, right, callbacks) => { + return DiffSequence.diffObjects(left, right, callbacks); +}; + +// ordered: bool. +// old_results and new_results: collections of documents. +// if ordered, they are arrays. +// if unordered, they are IdMaps +LocalCollection._diffQueryChanges = (ordered, oldResults, newResults, observer, options) => { + return DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options); +}; + +LocalCollection._diffQueryOrderedChanges = (oldResults, newResults, observer, options) => { + return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options); +}; + +LocalCollection._diffQueryUnorderedChanges = (oldResults, newResults, observer, options) => { + return DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options); +}; + +LocalCollection._findInOrderedResults = (query, doc) => { + if (!query.ordered) + throw new Error("Can't call _findInOrderedResults on unordered query"); + for (let i = 0; i < query.results.length; i++) + if (query.results[i] === doc) + return i; + throw Error("object missing from query"); +}; + +// If this is a selector which explicitly constrains the match by ID to a finite +// number of documents, returns a list of their IDs. Otherwise returns +// null. Note that the selector may have other restrictions so it may not even +// match those document! We care about $in and $and since those are generated +// access-controlled update and remove. +LocalCollection._idsMatchedBySelector = selector => { + // Is the selector just an ID? + if (LocalCollection._selectorIsId(selector)) + return [selector]; + if (!selector) + return null; + + // Do we have an _id clause? + if (selector.hasOwnProperty('_id')) { + // Is the _id clause just an ID? + if (LocalCollection._selectorIsId(selector._id)) + return [selector._id]; + // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? + if (selector._id && selector._id.$in + && Array.isArray(selector._id.$in) + && selector._id.$in.length + && selector._id.$in.every(LocalCollection._selectorIsId)) { + return selector._id.$in; + } + return null; + } + + // If this is a top-level $and, and any of the clauses constrain their + // documents, then the whole selector is constrained by any one clause's + // constraint. (Well, by their intersection, but that seems unlikely.) + if (selector.$and && Array.isArray(selector.$and)) { + for (let i = 0; i < selector.$and.length; ++i) { + const subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); + if (subIds) + return subIds; + } + } + + return null; +}; + +LocalCollection._insertInResults = (query, doc) => { + const fields = EJSON.clone(doc); + delete fields._id; + if (query.ordered) { + if (!query.sorter) { + query.addedBefore(doc._id, query.projectionFn(fields), null); + query.results.push(doc); + } else { + const i = LocalCollection._insertInSortedList( + query.sorter.getComparator({distances: query.distances}), + query.results, doc); + let next = query.results[i+1]; + if (next) + next = next._id; + else + next = null; + query.addedBefore(doc._id, query.projectionFn(fields), next); + } + query.added(doc._id, query.projectionFn(fields)); + } else { + query.added(doc._id, query.projectionFn(fields)); + query.results.set(doc._id, doc); + } +}; + +LocalCollection._insertInSortedList = (cmp, array, value) => { + if (array.length === 0) { + array.push(value); + return 0; + } + + const idx = LocalCollection._binarySearch(cmp, array, value); + array.splice(idx, 0, value); + return idx; +}; + +// XXX maybe this should be EJSON.isObject, though EJSON doesn't know about +// RegExp +// XXX note that _type(undefined) === 3!!!! +LocalCollection._isPlainObject = x => { + return x && LocalCollection._f._type(x) === 3; +}; + +// XXX need a strategy for passing the binding of $ into this +// function, from the compiled selector +// +// maybe just {key.up.to.just.before.dollarsign: array_index} +// +// XXX atomicity: if one modification fails, do we roll back the whole +// change? +// +// options: +// - isInsert is set when _modify is being called to compute the document to +// insert as part of an upsert operation. We use this primarily to figure +// out when to set the fields in $setOnInsert, if present. +LocalCollection._modify = (doc, mod, options) => { + options = options || {}; + if (!LocalCollection._isPlainObject(mod)) + throw MinimongoError("Modifier must be an object"); + + // Make sure the caller can't mutate our data structures. + mod = EJSON.clone(mod); + + const isModifier = isOperatorObject(mod); + + let newDoc; + + if (!isModifier) { + if (mod._id && !EJSON.equals(doc._id, mod._id)) + throw MinimongoError("Cannot change the _id of a document"); + + // replace the whole document + assertHasValidFieldNames(mod); + newDoc = mod; + } else { + // apply modifiers to the doc. + newDoc = EJSON.clone(doc); + + Object.keys(mod).forEach(op => { + const operand = mod[op]; + let modFunc = MODIFIERS[op]; + // Treat $setOnInsert as $set if this is an insert. + if (options.isInsert && op === '$setOnInsert') + modFunc = MODIFIERS['$set']; + if (!modFunc) + throw MinimongoError(`Invalid modifier specified ${op}`); + Object.keys(operand).forEach(keypath => { + const arg = operand[keypath]; + if (keypath === '') { + throw MinimongoError("An empty update path is not valid."); + } + + if (keypath === '_id' && op !== '$setOnInsert') { + throw MinimongoError("Mod on _id not allowed"); + } + + const keyparts = keypath.split('.'); + + if (!keyparts.every(Boolean)) { + throw MinimongoError( + `The update path '${keypath}' contains an empty field name, which is not allowed.`); + } + + const noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); + const forbidArray = (op === "$rename"); + const target = findModTarget(newDoc, keyparts, { + noCreate: NO_CREATE_MODIFIERS[op], + forbidArray: (op === "$rename"), + arrayIndices: options.arrayIndices + }); + const field = keyparts.pop(); + modFunc(target, field, arg, keypath, newDoc); + }); + }); + } + + // move new document into place. + Object.keys(doc).forEach(k => { + // Note: this used to be for (var k in doc) however, this does not + // work right in Opera. Deleting from a doc while iterating over it + // would sometimes cause opera to skip some keys. + if (k !== '_id') + delete doc[k]; + }); + Object.keys(newDoc).forEach(k => { + doc[k] = newDoc[k]; + }); +}; + +LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { + const transform = cursor.getTransform() || (doc => doc); + let suppressed = !!observeCallbacks._suppress_initial; + + let observeChangesCallbacks; + if (LocalCollection._observeCallbacksAreOrdered(observeCallbacks)) { + // The "_no_indices" option sets all index arguments to -1 and skips the + // linear scans required to generate them. This lets observers that don't + // need absolute indices benefit from the other features of this API -- + // relative order, transforms, and applyChanges -- without the speed hit. + const indices = !observeCallbacks._no_indices; + observeChangesCallbacks = { + addedBefore(id, fields, before) { + const self = this; + if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) + return; + const doc = transform(Object.assign(fields, {_id: id})); + if (observeCallbacks.addedAt) { + const index = indices + ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; + observeCallbacks.addedAt(doc, index, before); + } else { + observeCallbacks.added(doc); + } + }, + changed(id, fields) { + const self = this; + if (!(observeCallbacks.changedAt || observeCallbacks.changed)) + return; + let doc = EJSON.clone(self.docs.get(id)); + if (!doc) + throw new Error(`Unknown id for changed: ${id}`); + const oldDoc = transform(EJSON.clone(doc)); + DiffSequence.applyChanges(doc, fields); + doc = transform(doc); + if (observeCallbacks.changedAt) { + const index = indices ? self.docs.indexOf(id) : -1; + observeCallbacks.changedAt(doc, oldDoc, index); + } else { + observeCallbacks.changed(doc, oldDoc); + } + }, + movedBefore(id, before) { + const self = this; + if (!observeCallbacks.movedTo) + return; + const from = indices ? self.docs.indexOf(id) : -1; + + let to = indices + ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; + // When not moving backwards, adjust for the fact that removing the + // document slides everything back one slot. + if (to > from) + --to; + observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))), + from, to, before || null); + }, + removed(id) { + const self = this; + if (!(observeCallbacks.removedAt || observeCallbacks.removed)) + return; + // technically maybe there should be an EJSON.clone here, but it's about + // to be removed from self.docs! + const doc = transform(self.docs.get(id)); + if (observeCallbacks.removedAt) { + const index = indices ? self.docs.indexOf(id) : -1; + observeCallbacks.removedAt(doc, index); + } else { + observeCallbacks.removed(doc); + } + } + }; + } else { + observeChangesCallbacks = { + added(id, fields) { + if (!suppressed && observeCallbacks.added) { + const doc = Object.assign(fields, {_id: id}); + observeCallbacks.added(transform(doc)); + } + }, + changed(id, fields) { + const self = this; + if (observeCallbacks.changed) { + const oldDoc = self.docs.get(id); + const doc = EJSON.clone(oldDoc); + DiffSequence.applyChanges(doc, fields); + observeCallbacks.changed(transform(doc), + transform(EJSON.clone(oldDoc))); + } + }, + removed(id) { + const self = this; + if (observeCallbacks.removed) { + observeCallbacks.removed(transform(self.docs.get(id))); + } + } + }; + } + + const changeObserver = new LocalCollection._CachingChangeObserver( + {callbacks: observeChangesCallbacks}); + const handle = cursor.observeChanges(changeObserver.applyChange); + suppressed = false; + + return handle; +}; + +LocalCollection._observeCallbacksAreOrdered = callbacks => { + if (callbacks.addedAt && callbacks.added) + throw new Error("Please specify only one of added() and addedAt()"); + if (callbacks.changedAt && callbacks.changed) + throw new Error("Please specify only one of changed() and changedAt()"); + if (callbacks.removed && callbacks.removedAt) + throw new Error("Please specify only one of removed() and removedAt()"); + + return !!(callbacks.addedAt || callbacks.movedTo || callbacks.changedAt + || callbacks.removedAt); +}; + +LocalCollection._observeChangesCallbacksAreOrdered = callbacks => { + if (callbacks.added && callbacks.addedBefore) + throw new Error("Please specify only one of added() and addedBefore()"); + return !!(callbacks.addedBefore || callbacks.movedBefore); +}; + +// When performing an upsert, the incoming selector object can be re-used as +// the upsert modifier object, as long as Mongo query and projection +// operators (prefixed with a $ character) are removed from the newly +// created modifier object. This function attempts to strip all $ based Mongo +// operators when creating the upsert modifier object. +// NOTE: There is a known issue here in that some Mongo $ based opeartors +// should not actually be stripped. +// See https://github.com/meteor/meteor/issues/8806. +LocalCollection._removeDollarOperators = selector => { + let cleansed = {}; + Object.keys(selector).forEach((key) => { + const value = selector[key]; + if (key.charAt(0) !== '$' && !objectOnlyHasDollarKeys(value)) { + if (value !== null + && value.constructor + && Object.getPrototypeOf(value) === Object.prototype) { + cleansed[key] = LocalCollection._removeDollarOperators(value); + } else { + cleansed[key] = value; + } + } + }); + return cleansed; +}; + +LocalCollection._removeFromResults = (query, doc) => { + if (query.ordered) { + const i = LocalCollection._findInOrderedResults(query, doc); + query.removed(doc._id); + query.results.splice(i, 1); + } else { + const id = doc._id; // in case callback mutates doc + query.removed(doc._id); + query.results.remove(id); + } +}; + +// Is this selector just shorthand for lookup by _id? +LocalCollection._selectorIsId = selector => { + return (typeof selector === "string") || + (typeof selector === "number") || + selector instanceof MongoID.ObjectID; +}; + +// Is the selector just lookup by _id (shorthand or not)? +LocalCollection._selectorIsIdPerhapsAsObject = selector => { + return LocalCollection._selectorIsId(selector) || + (selector && typeof selector === "object" && + selector._id && LocalCollection._selectorIsId(selector._id) && + Object.keys(selector).length === 1); +}; + +LocalCollection._updateInResults = (query, doc, old_doc) => { + if (!EJSON.equals(doc._id, old_doc._id)) + throw new Error("Can't change a doc's _id while updating"); + const projectionFn = query.projectionFn; + const changedFields = DiffSequence.makeChangedFields( + projectionFn(doc), projectionFn(old_doc)); + + if (!query.ordered) { + if (Object.keys(changedFields).length) { + query.changed(doc._id, changedFields); + query.results.set(doc._id, doc); + } + return; + } + + const orig_idx = LocalCollection._findInOrderedResults(query, doc); + + if (Object.keys(changedFields).length) + query.changed(doc._id, changedFields); + if (!query.sorter) + return; + + // just take it out and put it back in again, and see if the index + // changes + query.results.splice(orig_idx, 1); + const new_idx = LocalCollection._insertInSortedList( + query.sorter.getComparator({distances: query.distances}), + query.results, doc); + if (orig_idx !== new_idx) { + let next = query.results[new_idx+1]; + if (next) + next = next._id; + else + next = null; + query.movedBefore && query.movedBefore(doc._id, next); + } +}; + const MODIFIERS = { - $currentDate: function (target, field, arg) { + $currentDate(target, field, arg) { if (typeof arg === "object" && arg.hasOwnProperty("$type")) { if (arg.$type !== "date") { throw MinimongoError( @@ -1209,7 +1204,7 @@ const MODIFIERS = { } target[field] = new Date(); }, - $min: function (target, field, arg) { + $min(target, field, arg) { if (typeof arg !== "number") { throw MinimongoError("Modifier $min allowed for numbers only", { field }); } @@ -1225,7 +1220,7 @@ const MODIFIERS = { target[field] = arg; } }, - $max: function (target, field, arg) { + $max(target, field, arg) { if (typeof arg !== "number") { throw MinimongoError("Modifier $max allowed for numbers only", { field }); } @@ -1241,7 +1236,7 @@ const MODIFIERS = { target[field] = arg; } }, - $inc: function (target, field, arg) { + $inc(target, field, arg) { if (typeof arg !== "number") throw MinimongoError("Modifier $inc allowed for numbers only", { field }); if (field in target) { @@ -1253,25 +1248,25 @@ const MODIFIERS = { target[field] = arg; } }, - $set: function (target, field, arg) { + $set(target, field, arg) { if (target !== Object(target)) { // not an array or an object - var e = MinimongoError( + const e = MinimongoError( "Cannot set property on non-object field", { field }); e.setPropertyError = true; throw e; } if (target === null) { - var e = MinimongoError("Cannot set property on null", { field }); + const e = MinimongoError("Cannot set property on null", { field }); e.setPropertyError = true; throw e; } assertHasValidFieldNames(arg); target[field] = arg; }, - $setOnInsert: function (target, field, arg) { + $setOnInsert(target, field, arg) { // converted to `$set` in `_modify` }, - $unset: function (target, field, arg) { + $unset(target, field, arg) { if (target !== undefined) { if (target instanceof Array) { if (field in target) @@ -1280,7 +1275,7 @@ const MODIFIERS = { delete target[field]; } }, - $push: function (target, field, arg) { + $push(target, field, arg) { if (target[field] === undefined) target[field] = []; if (!(target[field] instanceof Array)) @@ -1295,13 +1290,13 @@ const MODIFIERS = { } // Fancy mode: $each (and maybe $slice and $sort and $position) - var toPush = arg.$each; + const toPush = arg.$each; if (!(toPush instanceof Array)) throw MinimongoError("$each must be an array", { field }); assertHasValidFieldNames(toPush); // Parse $position - var position = undefined; + let position = undefined; if ('$position' in arg) { if (typeof arg.$position !== "number") throw MinimongoError("$position must be a numeric value", { field }); @@ -1313,7 +1308,7 @@ const MODIFIERS = { } // Parse $slice. - var slice = undefined; + let slice = undefined; if ('$slice' in arg) { if (typeof arg.$slice !== "number") throw MinimongoError("$slice must be a numeric value", { field }); @@ -1322,7 +1317,7 @@ const MODIFIERS = { } // Parse $sort. - var sortFunction = undefined; + let sortFunction = undefined; if (arg.$sort) { if (slice === undefined) throw MinimongoError("$sort requires $slice to be present", { field }); @@ -1331,7 +1326,7 @@ const MODIFIERS = { // server-side. Could be confusing! // XXX is it correct that we don't do geo-stuff here? sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); - for (var i = 0; i < toPush.length; i++) { + for (let i = 0; i < toPush.length; i++) { if (LocalCollection._f._type(toPush[i]) !== 3) { throw MinimongoError("$push like modifiers using $sort " + "require all elements to be objects", { field }); @@ -1341,11 +1336,11 @@ const MODIFIERS = { // Actually push. if (position === undefined) { - for (var j = 0; j < toPush.length; j++) + for (let j = 0; j < toPush.length; j++) target[field].push(toPush[j]); } else { - var spliceArguments = [position, 0]; - for (var j = 0; j < toPush.length; j++) + const spliceArguments = [position, 0]; + for (let j = 0; j < toPush.length; j++) spliceArguments.push(toPush[j]); Array.prototype.splice.apply(target[field], spliceArguments); } @@ -1364,23 +1359,23 @@ const MODIFIERS = { target[field] = target[field].slice(0, slice); } }, - $pushAll: function (target, field, arg) { + $pushAll(target, field, arg) { if (!(typeof arg === "object" && arg instanceof Array)) throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); assertHasValidFieldNames(arg); - var x = target[field]; + const x = target[field]; if (x === undefined) target[field] = arg; else if (!(x instanceof Array)) throw MinimongoError( "Cannot apply $pushAll modifier to non-array", { field }); else { - for (var i = 0; i < arg.length; i++) + for (let i = 0; i < arg.length; i++) x.push(arg[i]); } }, - $addToSet: function (target, field, arg) { - var isEach = false; + $addToSet(target, field, arg) { + let isEach = false; if (typeof arg === "object") { //check if first key is '$each' const keys = Object.keys(arg); @@ -1388,27 +1383,27 @@ const MODIFIERS = { isEach = true; } } - var values = isEach ? arg["$each"] : [arg]; + const values = isEach ? arg["$each"] : [arg]; assertHasValidFieldNames(values); - var x = target[field]; + const x = target[field]; if (x === undefined) target[field] = values; else if (!(x instanceof Array)) throw MinimongoError( "Cannot apply $addToSet modifier to non-array", { field }); else { - values.forEach(function (value) { - for (var i = 0; i < x.length; i++) + values.forEach(value => { + for (let i = 0; i < x.length; i++) if (LocalCollection._f._equal(value, x[i])) return; x.push(value); }); } }, - $pop: function (target, field, arg) { + $pop(target, field, arg) { if (target === undefined) return; - var x = target[field]; + const x = target[field]; if (x === undefined) return; else if (!(x instanceof Array)) @@ -1421,17 +1416,17 @@ const MODIFIERS = { x.pop(); } }, - $pull: function (target, field, arg) { + $pull(target, field, arg) { if (target === undefined) return; - var x = target[field]; + const x = target[field]; if (x === undefined) return; else if (!(x instanceof Array)) throw MinimongoError( "Cannot apply $pull/pullAll modifier to non-array", { field }); else { - var out = []; + const out = []; if (arg != null && typeof arg === "object" && !(arg instanceof Array)) { // XXX would be much nicer to compile this once, rather than // for each document we modify.. but usually we're not @@ -1442,35 +1437,35 @@ const MODIFIERS = { // to permit stuff like {$pull: {a: {$gt: 4}}}.. something // like {$gt: 4} is not normally a complete selector. // same issue as $elemMatch possibly? - var matcher = new Minimongo.Matcher(arg); - for (var i = 0; i < x.length; i++) + const matcher = new Minimongo.Matcher(arg); + for (let i = 0; i < x.length; i++) if (!matcher.documentMatches(x[i]).result) out.push(x[i]); } else { - for (var i = 0; i < x.length; i++) + for (let i = 0; i < x.length; i++) if (!LocalCollection._f._equal(x[i], arg)) out.push(x[i]); } target[field] = out; } }, - $pullAll: function (target, field, arg) { + $pullAll(target, field, arg) { if (!(typeof arg === "object" && arg instanceof Array)) throw MinimongoError( "Modifier $pushAll/pullAll allowed for arrays only", { field }); if (target === undefined) return; - var x = target[field]; + const x = target[field]; if (x === undefined) return; else if (!(x instanceof Array)) throw MinimongoError( "Cannot apply $pull/pullAll modifier to non-array", { field }); else { - var out = []; - for (var i = 0; i < x.length; i++) { - var exclude = false; - for (var j = 0; j < arg.length; j++) { + const out = []; + for (let i = 0; i < x.length; i++) { + let exclude = false; + for (let j = 0; j < arg.length; j++) { if (LocalCollection._f._equal(x[i], arg[j])) { exclude = true; break; @@ -1482,7 +1477,7 @@ const MODIFIERS = { target[field] = out; } }, - $rename: function (target, field, arg, keypath, doc) { + $rename(target, field, arg, keypath, doc) { if (keypath === arg) // no idea why mongo has this restriction.. throw MinimongoError("$rename source must differ from target", { field }); @@ -1490,7 +1485,7 @@ const MODIFIERS = { throw MinimongoError("$rename source field invalid", { field }); if (typeof arg !== "string") throw MinimongoError("$rename target must be a string", { field }); - if (arg.indexOf('\0') > -1) { + if (arg.includes('\0')) { // Null bytes are not allowed in Mongo field names // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names throw MinimongoError( @@ -1499,17 +1494,17 @@ const MODIFIERS = { } if (target === undefined) return; - var v = target[field]; + const v = target[field]; delete target[field]; - var keyparts = arg.split('.'); - var target2 = findModTarget(doc, keyparts, {forbidArray: true}); + const keyparts = arg.split('.'); + const target2 = findModTarget(doc, keyparts, {forbidArray: true}); if (target2 === null) throw MinimongoError("$rename target field invalid", { field }); - var field2 = keyparts.pop(); + const field2 = keyparts.pop(); target2[field2] = v; }, - $bit: function (target, field, arg) { + $bit(target, field, arg) { // XXX mongo only supports $bit on integers, and we only support // native javascript numbers (doubles) so far, so we can't support $bit throw MinimongoError("$bit is not supported", { field }); @@ -1567,18 +1562,17 @@ function assertIsValidFieldName (key) { // // if options.arrayIndices is set, use its first element for the (first) '$' in // the path. -function findModTarget (doc, keyparts, options) { - options = options || {}; - var usedArrayIndex = false; - for (var i = 0; i < keyparts.length; i++) { - var last = (i === keyparts.length - 1); - var keypart = keyparts[i]; - var indexable = isIndexable(doc); +function findModTarget(doc, keyparts, options = {}) { + let usedArrayIndex = false; + for (let i = 0; i < keyparts.length; i++) { + const last = (i === keyparts.length - 1); + let keypart = keyparts[i]; + const indexable = isIndexable(doc); if (!indexable) { if (options.noCreate) return undefined; - var e = MinimongoError( - "cannot use the part '" + keypart + "' to traverse " + doc); + const e = MinimongoError( + `cannot use the part '${keypart}' to traverse ${doc}`); e.setPropertyError = true; throw e; } @@ -1600,8 +1594,7 @@ function findModTarget (doc, keyparts, options) { if (options.noCreate) return undefined; throw MinimongoError( - "can't append to array using string field name [" - + keypart + "]"); + `can't append to array using string field name [${keypart}]`); } if (last) // handle 'a.01' @@ -1614,8 +1607,7 @@ function findModTarget (doc, keyparts, options) { if (doc.length === keypart) doc.push({}); else if (typeof doc[keypart] !== "object") - throw MinimongoError("can't modify field '" + keyparts[i + 1] + - "' of list value " + JSON.stringify(doc[keypart])); + throw MinimongoError(`can't modify field '${keyparts[i + 1]}' of list value ${JSON.stringify(doc[keypart])}`); } } else { assertIsValidFieldName(keypart); diff --git a/packages/minimongo/main_server.js b/packages/minimongo/main_server.js index 241a0e0f0a..a24849f8a8 100644 --- a/packages/minimongo/main_server.js +++ b/packages/minimongo/main_server.js @@ -7,10 +7,8 @@ import { } from './common.js'; Minimongo._pathsElidingNumericKeys = function (paths) { - var self = this; - return paths.map(function (path) { - return path.split('.').filter(function (part) { return !isNumericKey(part); }).join('.'); - }); + const self = this; + return paths.map(path => path.split('.').filter(part => !isNumericKey(part)).join('.')); }; // Returns true if the modifier applied to some document may change the result @@ -22,17 +20,17 @@ Minimongo._pathsElidingNumericKeys = function (paths) { // - $unset // - 'abc.d': 1 Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { - var self = this; + const self = this; // safe check for $set/$unset being objects modifier = Object.assign({ $set: {}, $unset: {} }, modifier); - var modifiedPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); - var meaningfulPaths = self._getPaths(); + const modifiedPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); + const meaningfulPaths = self._getPaths(); - return modifiedPaths.some(function (path) { - var mod = path.split('.'); - return meaningfulPaths.some(function (meaningfulPath) { - var sel = meaningfulPath.split('.'); - var i = 0, j = 0; + return modifiedPaths.some(path => { + const mod = path.split('.'); + return meaningfulPaths.some(meaningfulPath => { + const sel = meaningfulPath.split('.'); + let i = 0, j = 0; while (i < sel.length && j < mod.length) { if (isNumericKey(sel[i]) && isNumericKey(mod[j])) { @@ -68,12 +66,12 @@ Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { // stay 'false'. // Currently doesn't support $-operators and numeric indices precisely. Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { - var self = this; + const self = this; if (!this.affectedByModifier(modifier)) return false; modifier = Object.assign({$set:{}, $unset:{}}, modifier); - var modifierPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); + const modifierPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); if (!self.isSimple()) return true; @@ -87,13 +85,11 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // NOTE: it is correct since we allow only scalars in $-operators // Example: for selector {'a.b': {$gt: 5}} the modifier {'a.b.c':7} would // definitely set the result to false as 'a.b' appears to be an object. - var expectedScalarIsObject = Object.keys(self._selector).some(function (path) { - var sel = self._selector[path]; + const expectedScalarIsObject = Object.keys(self._selector).some(path => { + const sel = self._selector[path]; if (! isOperatorObject(sel)) return false; - return modifierPaths.some(function (modifierPath) { - return startsWith(modifierPath, path + '.'); - }); + return modifierPaths.some(modifierPath => startsWith(modifierPath, `${path}.`)); }); if (expectedScalarIsObject) @@ -102,7 +98,7 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // See if we can apply the modifier on the ideally matching object. If it // still matches the selector, then the modifier could have turned the real // object in the database into something matching. - var matchingDocument = EJSON.clone(self.matchingDocument()); + const matchingDocument = EJSON.clone(self.matchingDocument()); // The selector is too complex, anything can happen. if (matchingDocument === null) @@ -133,8 +129,8 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // projection taking into account active fields from the passed selector. // @returns Object - projection object (same as fields option of mongo cursor) Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { - var self = this; - var selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); + const self = this; + const selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); // Special case for $where operator in the selector - projection should depend // on all fields of the document. getSelectorPaths returns a list of paths @@ -151,7 +147,7 @@ Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { // { 'a.b': { ans: 42 }, 'foo.bar': null, 'foo.baz': "something" } // => { a: { b: { ans: 42 } }, foo: { bar: null, baz: "something" } } Minimongo.Matcher.prototype.matchingDocument = function () { - var self = this; + const self = this; // check if it was computed before if (self._matchingDocument !== undefined) @@ -159,10 +155,10 @@ Minimongo.Matcher.prototype.matchingDocument = function () { // If the analysis of this selector is too hard for our implementation // fallback to "YES" - var fallback = false; + let fallback = false; self._matchingDocument = pathsToTree(self._getPaths(), - function (path) { - var valueSelector = self._selector[path]; + path => { + const valueSelector = self._selector[path]; if (isOperatorObject(valueSelector)) { // if there is a strict equality, there is a good // chance we can use one of those as "matching" @@ -170,27 +166,25 @@ Minimongo.Matcher.prototype.matchingDocument = function () { if (valueSelector.$eq) { return valueSelector.$eq; } else if (valueSelector.$in) { - var matcher = new Minimongo.Matcher({ placeholder: valueSelector }); + const matcher = new Minimongo.Matcher({ placeholder: valueSelector }); // Return anything from $in that matches the whole selector for this // path. If nothing matches, returns `undefined` as nothing can make // this selector into `true`. - return valueSelector.$in.find(function (x) { - return matcher.documentMatches({ placeholder: x }).result; - }); + return valueSelector.$in.find(x => matcher.documentMatches({ placeholder: x }).result); } else if (onlyContainsKeys(valueSelector, ['$gt', '$gte', '$lt', '$lte'])) { - var lowerBound = -Infinity, upperBound = Infinity; - ['$lte', '$lt'].forEach(function (op) { + let lowerBound = -Infinity, upperBound = Infinity; + ['$lte', '$lt'].forEach(op => { if (valueSelector.hasOwnProperty(op) && valueSelector[op] < upperBound) upperBound = valueSelector[op]; }); - ['$gte', '$gt'].forEach(function (op) { + ['$gte', '$gt'].forEach(op => { if (valueSelector.hasOwnProperty(op) && valueSelector[op] > lowerBound) lowerBound = valueSelector[op]; }); - var middle = (lowerBound + upperBound) / 2; - var matcher = new Minimongo.Matcher({ placeholder: valueSelector }); + const middle = (lowerBound + upperBound) / 2; + const matcher = new Minimongo.Matcher({ placeholder: valueSelector }); if (!matcher.documentMatches({ placeholder: middle }).result && (middle === lowerBound || middle === upperBound)) fallback = true; @@ -207,7 +201,7 @@ Minimongo.Matcher.prototype.matchingDocument = function () { } return self._selector[path]; }, - function (x) { return x; } /*conflict resolution is no resolution*/); + x => x); if (fallback) self._matchingDocument = null; @@ -218,25 +212,25 @@ Minimongo.Matcher.prototype.matchingDocument = function () { // Minimongo.Sorter gets a similar method, which delegates to a Matcher it made // for this exact purpose. Minimongo.Sorter.prototype.affectedByModifier = function (modifier) { - var self = this; + const self = this; return self._selectorForAffectedByModifier.affectedByModifier(modifier); }; Minimongo.Sorter.prototype.combineIntoProjection = function (projection) { - var self = this; - var specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); + const self = this; + const specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); return combineImportantPathsIntoProjection(specPaths, projection); }; function combineImportantPathsIntoProjection (paths, projection) { - var prjDetails = projectionDetails(projection); - var tree = prjDetails.tree; - var mergedProjection = {}; + const prjDetails = projectionDetails(projection); + let tree = prjDetails.tree; + let mergedProjection = {}; // merge the paths to include tree = pathsToTree(paths, - function (path) { return true; }, - function (node, path, fullPath) { return true; }, + path => true, + (node, path, fullPath) => true, tree); mergedProjection = treeToPaths(tree); if (prjDetails.including) { @@ -247,9 +241,9 @@ function combineImportantPathsIntoProjection (paths, projection) { // selector is pointing at fields to include // projection is pointing at fields to exclude // make sure we don't exclude important paths - var mergedExclProjection = {}; - Object.keys(mergedProjection).forEach(function (path) { - var incl = mergedProjection[path]; + const mergedExclProjection = {}; + Object.keys(mergedProjection).forEach(path => { + const incl = mergedProjection[path]; if (!incl) mergedExclProjection[path] = false; }); @@ -260,8 +254,8 @@ function combineImportantPathsIntoProjection (paths, projection) { function getPaths (sel) { return Object.keys(new Minimongo.Matcher(sel)._paths); - return Object.keys(sel).map(function (k) { - var v = sel[k]; + return Object.keys(sel).map(k => { + const v = sel[k]; // we don't know how to handle $where because it can be anything if (k === "$where") return ''; // matches everything @@ -271,15 +265,13 @@ function getPaths (sel) { // the value is a literal or some comparison operator return k; }) - .reduce(function (a, b) { return a.concat(b); }, []) - .filter(function (a, b, c) { return c.indexOf(a) === b; }); + .reduce((a, b) => a.concat(b), []) + .filter((a, b, c) => c.indexOf(a) === b); } // A helper to ensure object has only certain keys function onlyContainsKeys (obj, keys) { - return Object.keys(obj).every(function (k) { - return keys.includes(k); - }); + return Object.keys(obj).every(k => keys.includes(k)); } function pathHasNumericKeys (path) { @@ -294,14 +286,13 @@ function startsWith(str, starts) { // Returns a set of key paths similar to // { 'foo.bar': 1, 'a.b.c': 1 } -function treeToPaths (tree, prefix) { - prefix = prefix || ''; - var result = {}; +function treeToPaths(tree, prefix = '') { + const result = {}; - Object.keys(tree).forEach(function (key) { - var val = tree[key]; + Object.keys(tree).forEach(key => { + const val = tree[key]; if (val === Object(val)) - Object.assign(result, treeToPaths(val, prefix + key + '.')); + Object.assign(result, treeToPaths(val, `${prefix + key}.`)); else result[prefix + key] = val; }); diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index 832db9c1ec..699b2c7ddc 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -124,7 +124,7 @@ export class Matcher { LocalCollection._f = { // XXX for _all and _in, consider building 'inquery' at compile time.. - _type: function (v) { + _type(v) { if (typeof v === "number") return 1; if (typeof v === "string") @@ -158,13 +158,13 @@ LocalCollection._f = { }, // deep equality test: use for literal document and array matches - _equal: function (a, b) { + _equal(a, b) { return EJSON.equals(a, b, {keyOrderSensitive: true}); }, // maps a type code to a value that can be used to sort values of // different types - _typeorder: function (t) { + _typeorder(t) { // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types // XXX what is the correct sort position for Javascript code? // ('100' in the matrix below) @@ -195,15 +195,15 @@ LocalCollection._f = { // semantics. (as an extension, consider 'undefined' to be less than // any other value.) return negative if a is less, positive if b is // less, or 0 if equal - _cmp: function (a, b) { + _cmp(a, b) { if (a === undefined) return b === undefined ? 0 : -1; if (b === undefined) return 1; - var ta = LocalCollection._f._type(a); - var tb = LocalCollection._f._type(b); - var oa = LocalCollection._f._typeorder(ta); - var ob = LocalCollection._f._typeorder(tb); + let ta = LocalCollection._f._type(a); + let tb = LocalCollection._f._type(b); + const oa = LocalCollection._f._typeorder(ta); + const ob = LocalCollection._f._typeorder(tb); if (oa !== ob) return oa < ob ? -1 : 1; if (ta !== tb) @@ -229,9 +229,9 @@ LocalCollection._f = { return a < b ? -1 : (a === b ? 0 : 1); if (ta === 3) { // Object // this could be much more efficient in the expected case ... - var to_array = function (obj) { - var ret = []; - for (var key in obj) { + const to_array = obj => { + const ret = []; + for (let key in obj) { ret.push(key); ret.push(obj[key]); } @@ -240,12 +240,12 @@ LocalCollection._f = { return LocalCollection._f._cmp(to_array(a), to_array(b)); } if (ta === 4) { // Array - for (var i = 0; ; i++) { + for (let i = 0; ; i++) { if (i === a.length) return (i === b.length) ? 0 : -1; if (i === b.length) return 1; - var s = LocalCollection._f._cmp(a[i], b[i]); + const s = LocalCollection._f._cmp(a[i], b[i]); if (s !== 0) return s; } @@ -255,7 +255,7 @@ LocalCollection._f = { // Mongo. if (a.length !== b.length) return a.length - b.length; - for (i = 0; i < a.length; i++) { + for (let i = 0; i < a.length; i++) { if (a[i] < b[i]) return -1; if (a[i] > b[i]) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 3c1f84196a..4c1ea0b187 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1,56 +1,58 @@ -Tinytest.add("minimongo - wrapTransform", function (test) { - var wrap = LocalCollection.wrapTransform; +Tinytest.add("minimongo - wrapTransform", test => { + const wrap = LocalCollection.wrapTransform; // Transforming no function gives falsey. test.isFalse(wrap(undefined)); test.isFalse(wrap(null)); // It's OK if you don't change the ID. - var validTransform = function (doc) { + const validTransform = doc => { delete doc.x; doc.y = 42; - doc.z = function () { return 43; }; + doc.z = () => 43; return doc; }; - var transformed = wrap(validTransform)({_id: "asdf", x: 54}); + const transformed = wrap(validTransform)({_id: "asdf", x: 54}); test.equal(Object.keys(transformed), ['_id', 'y', 'z']); test.equal(transformed.y, 42); test.equal(transformed.z(), 43); // Ensure that ObjectIDs work (even if the _ids in question are not ===-equal) - var oid1 = new MongoID.ObjectID(); - var oid2 = new MongoID.ObjectID(oid1.toHexString()); - test.equal(wrap(function () {return {_id: oid2};})({_id: oid1}), + const oid1 = new MongoID.ObjectID(); + const oid2 = new MongoID.ObjectID(oid1.toHexString()); + test.equal(wrap(() => ({ + _id: oid2 + }))({_id: oid1}), {_id: oid2}); // transform functions must return objects - var invalidObjects = [ + const invalidObjects = [ "asdf", new MongoID.ObjectID(), false, null, true, - 27, [123], /adsf/, new Date, function () {}, undefined + 27, [123], /adsf/, new Date, () => {}, undefined ]; - invalidObjects.forEach(function (invalidObject) { - var wrapped = wrap(function () { return invalidObject; }); - test.throws(function () { + invalidObjects.forEach(invalidObject => { + const wrapped = wrap(() => invalidObject); + test.throws(() => { wrapped({_id: "asdf"}); }); }, /transform must return object/); // transform functions may not change _ids - var wrapped = wrap(function (doc) { doc._id = 'x'; return doc; }); - test.throws(function () { + const wrapped = wrap(doc => { doc._id = 'x'; return doc; }); + test.throws(() => { wrapped({_id: 'y'}); }, /can't have different _id/); // transform functions may remove _ids test.equal({_id: 'a', x: 2}, - wrap(function (d) {delete d._id; return d;})({_id: 'a', x: 2})); + wrap(d => {delete d._id; return d;})({_id: 'a', x: 2})); // test that wrapped transform functions are nonreactive - var unwrapped = function (doc) { + const unwrapped = doc => { test.isFalse(Tracker.active); return doc; }; - var handle = Tracker.autorun(function () { + const handle = Tracker.autorun(() => { test.isTrue(Tracker.active); wrap(unwrapped)({_id: "xxx"}); }); diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index 02d421607d..e39efc79f0 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -4,9 +4,9 @@ LocalCollection._useOID = true; // assert that f is a strcmp-style comparison function that puts // 'values' in the provided order -var assert_ordering = function (test, f, values) { - for (var i = 0; i < values.length; i++) { - var x = f(values[i], values[i]); +const assert_ordering = (test, f, values) => { + for (let i = 0; i < values.length; i++) { + let x = f(values[i], values[i]); if (x !== 0) { // XXX super janky test.fail({type: "minimongo-ordering", @@ -15,9 +15,9 @@ var assert_ordering = function (test, f, values) { should_be_zero_but_got: JSON.stringify(x)}); } if (i + 1 < values.length) { - var less = values[i]; - var more = values[i + 1]; - var x = f(less, more); + const less = values[i]; + const more = values[i + 1]; + x = f(less, more); if (!(x < 0)) { // XXX super janky test.fail({type: "minimongo-ordering", @@ -39,34 +39,35 @@ var assert_ordering = function (test, f, values) { } }; -var log_callbacks = function (operations) { - return { - addedAt: function (obj, idx, before) { - delete obj._id; - operations.push(EJSON.clone(['added', obj, idx, before])); - }, - changedAt: function (obj, old_obj, at) { - delete obj._id; - delete old_obj._id; - operations.push(EJSON.clone(['changed', obj, at, old_obj])); - }, - movedTo: function (obj, old_at, new_at, before) { - delete obj._id; - operations.push(EJSON.clone(['moved', obj, old_at, new_at, before])); - }, - removedAt: function (old_obj, at) { - var id = old_obj._id; - delete old_obj._id; - operations.push(EJSON.clone(['removed', id, at, old_obj])); - } - }; -}; +const log_callbacks = operations => ({ + addedAt(obj, idx, before) { + delete obj._id; + operations.push(EJSON.clone(['added', obj, idx, before])); + }, + + changedAt(obj, old_obj, at) { + delete obj._id; + delete old_obj._id; + operations.push(EJSON.clone(['changed', obj, at, old_obj])); + }, + + movedTo(obj, old_at, new_at, before) { + delete obj._id; + operations.push(EJSON.clone(['moved', obj, old_at, new_at, before])); + }, + + removedAt(old_obj, at) { + const id = old_obj._id; + delete old_obj._id; + operations.push(EJSON.clone(['removed', id, at, old_obj])); + } +}); // XXX test shared structure in all MM entrypoints -Tinytest.add("minimongo - basics", function (test) { - var c = new LocalCollection(), - fluffyKitten_id, - count; +Tinytest.add("minimongo - basics", test => { + const c = new LocalCollection(); + let fluffyKitten_id; + let count; fluffyKitten_id = c.insert({type: "kitten", name: "fluffy"}); c.insert({type: "kitten", name: "snookums"}); @@ -159,10 +160,9 @@ Tinytest.add("minimongo - basics", function (test) { c.insert({foo: {bar: 'baz'}}); test.equal(c.find({foo: {bam: 'baz'}}).count(), 0); test.equal(c.find({foo: {bar: 'baz'}}).count(), 1); - }); -Tinytest.add("minimongo - error - no options", function (test) { +Tinytest.add("minimongo - error - no options", test => { try { throw MinimongoError("Not fun to have errors"); } catch (e) { @@ -170,7 +170,7 @@ Tinytest.add("minimongo - error - no options", function (test) { } }); -Tinytest.add("minimongo - error - with field", function (test) { +Tinytest.add("minimongo - error - with field", test => { try { throw MinimongoError("Cats are no fun", { field: "mice" }); } catch (e) { @@ -178,28 +178,28 @@ Tinytest.add("minimongo - error - with field", function (test) { } }); -Tinytest.add("minimongo - cursors", function (test) { - var c = new LocalCollection(); - var res; +Tinytest.add("minimongo - cursors", test => { + const c = new LocalCollection(); + let res; - for (var i = 0; i < 20; i++) - c.insert({i: i}); + for (let i = 0; i < 20; i++) + c.insert({i}); - var q = c.find(); + const q = c.find(); test.equal(q.count(), 20); // fetch res = q.fetch(); test.length(res, 20); - for (var i = 0; i < 20; i++) { + for (let i = 0; i < 20; i++) { test.equal(res[i].i, i); } // call it again, it still works test.length(q.fetch(), 20); // forEach - var count = 0; - var context = {}; + let count = 0; + const context = {}; q.forEach(function (obj, i, cursor) { test.equal(obj.i, count++); test.equal(obj.i, i); @@ -218,7 +218,7 @@ Tinytest.add("minimongo - cursors", function (test) { return obj.i * 2; }, context); test.length(res, 20); - for (var i = 0; i < 20; i++) + for (let i = 0; i < 20; i++) test.equal(res[i], i * 2); // call it again, it still works test.length(q.fetch(), 20); @@ -226,22 +226,22 @@ Tinytest.add("minimongo - cursors", function (test) { // findOne (and no rewind first) test.equal(c.findOne({i: 0}).i, 0); test.equal(c.findOne({i: 1}).i, 1); - var id = c.findOne({i: 2})._id; + const id = c.findOne({i: 2})._id; test.equal(c.findOne(id).i, 2); }); -Tinytest.add("minimongo - transform", function (test) { - var c = new LocalCollection; +Tinytest.add("minimongo - transform", test => { + const c = new LocalCollection; c.insert({}); // transform functions must return objects - var invalidTransform = function (doc) { return doc._id; }; - test.throws(function () { + const invalidTransform = doc => doc._id; + test.throws(() => { c.findOne({}, {transform: invalidTransform}); }); // transformed documents get _id field transplanted if not present - var transformWithoutId = function (doc) { - var docWithoutId = Object.assign({}, doc); + const transformWithoutId = doc => { + const docWithoutId = Object.assign({}, doc); delete docWithoutId._id; return docWithoutId; }; @@ -249,11 +249,11 @@ Tinytest.add("minimongo - transform", function (test) { c.findOne()._id); }); -Tinytest.add("minimongo - misc", function (test) { +Tinytest.add("minimongo - misc", test => { // deepcopy - var a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]}, + let a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]}, f: null, g: new Date()}; - var b = EJSON.clone(a); + let b = EJSON.clone(a); test.equal(a, b); test.isTrue(LocalCollection._f._equal(a, b)); a.a.push(4); @@ -269,19 +269,19 @@ Tinytest.add("minimongo - misc", function (test) { b.g.setDate(b.g.getDate() + 1); test.notEqual(a.g, b.g); - a = {x: function () {}}; + a = {x() {}}; b = EJSON.clone(a); a.x.a = 14; test.equal(b.x.a, 14); // just to document current behavior }); -Tinytest.add("minimongo - lookup", function (test) { - var lookupA = MinimongoTest.makeLookupFunction('a'); +Tinytest.add("minimongo - lookup", test => { + const lookupA = MinimongoTest.makeLookupFunction('a'); test.equal(lookupA({}), [{value: undefined}]); test.equal(lookupA({a: 1}), [{value: 1}]); test.equal(lookupA({a: [1]}), [{value: [1]}]); - var lookupAX = MinimongoTest.makeLookupFunction('a.x'); + const lookupAX = MinimongoTest.makeLookupFunction('a.x'); test.equal(lookupAX({a: {x: 1}}), [{value: 1}]); test.equal(lookupAX({a: {x: [1]}}), [{value: [1]}]); test.equal(lookupAX({a: 5}), [{value: undefined}]); @@ -290,7 +290,7 @@ Tinytest.add("minimongo - lookup", function (test) { {value: [2], arrayIndices: [1]}, {value: undefined, arrayIndices: [2]}]); - var lookupA0X = MinimongoTest.makeLookupFunction('a.0.x'); + const lookupA0X = MinimongoTest.makeLookupFunction('a.0.x'); test.equal(lookupA0X({a: [{x: 1}]}), [ // From interpreting '0' as "0th array element". {value: 1, arrayIndices: [0, 'x']}, @@ -322,22 +322,21 @@ Tinytest.add("minimongo - lookup", function (test) { ]); }); -Tinytest.add("minimongo - selector_compiler", function (test) { - var matches = function (shouldMatch, selector, doc) { - var doesMatch = new Minimongo.Matcher(selector).documentMatches(doc).result; +Tinytest.add("minimongo - selector_compiler", test => { + const matches = (shouldMatch, selector, doc) => { + const doesMatch = new Minimongo.Matcher(selector).documentMatches(doc).result; if (doesMatch != shouldMatch) { // XXX super janky - test.fail({message: "minimongo match failure: document " + - (shouldMatch ? "should match, but doesn't" : - "shouldn't match, but does"), + test.fail({message: `minimongo match failure: document ${shouldMatch ? "should match, but doesn't" : + "shouldn't match, but does"}`, selector: JSON.stringify(selector), document: JSON.stringify(doc) }); } }; - var match = matches.bind(null, true); - var nomatch = matches.bind(null, false); + const match = matches.bind(null, true); + const nomatch = matches.bind(null, false); // XXX blog post about what I learned while writing these tests (weird // mongo edge cases) @@ -377,8 +376,8 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: 12, b: 13}, {a: [11, 12, 13], b: [14, 15]}); // dates - var date1 = new Date; - var date2 = new Date(date1.getTime() + 1000); + const date1 = new Date; + const date2 = new Date(date1.getTime() + 1000); match({a: date1}, {a: date1}); nomatch({a: date1}, {a: date2}); @@ -482,7 +481,7 @@ Tinytest.add("minimongo - selector_compiler", function (test) { // Members of $all other than regexps are *equality matches*, not document // matches. nomatch({a: {$all: [{b: 3}]}}, {a: [{b: 3, k: 4}]}); - test.throws(function () { + test.throws(() => { match({a: {$all: [{$gt: 4}]}}, {}); }); @@ -523,8 +522,8 @@ Tinytest.add("minimongo - selector_compiler", function (test) { "foo", {bar: 1}, [] - ].forEach(function (badMod) { - test.throws(function () { + ].forEach(badMod => { + test.throws(() => { match({a: {$mod: badMod}}, {a: 11}); }); }); @@ -716,11 +715,11 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({a: {$bitsAnyClear: new Uint8Array([1])}}, {a: 4 }); // taken from: https://github.com/mongodb/mongo/blob/master/jstests/core/bittest.js - var c = new LocalCollection; + const c = new LocalCollection; function matchCount(query, count) { const matches = c.find(query).count() if (matches !== count) { - test.fail({message: "minimongo match count failure: matched " + matches + " times, but should match " + count + " times", + test.fail({message: `minimongo match count failure: matched ${matches} times, but should match ${count} times`, query: JSON.stringify(query), count: JSON.stringify(count) }); @@ -803,8 +802,8 @@ Tinytest.add("minimongo - selector_compiler", function (test) { matchCount({a: {$bitsAnyClear: 127}}, 2) // Tests with array of bit positions. - var allPositions = [] - for (var i = 0; i < 64; i++) { + const allPositions = []; + for (let i = 0; i < 64; i++) { allPositions.push(i) } @@ -882,8 +881,8 @@ Tinytest.add("minimongo - selector_compiler", function (test) { 1.2, "1", [0, -1] - ].forEach(function (badValue) { - test.throws(function () { + ].forEach(badValue => { + test.throws(() => { match({a: {$bitsAllSet: badValue}}, {a: 42}); }); }); @@ -974,7 +973,7 @@ Tinytest.add("minimongo - selector_compiler", function (test) { // Regexps with a global flag ('g') keep a state when tested against the same // string. Selector shouldn't return different result for similar documents // because of this state. - var reusedRegexp = /sh/ig; + const reusedRegexp = /sh/ig; match({a: reusedRegexp}, {a: 'Shorts'}); match({a: reusedRegexp}, {a: 'Shorts'}); match({a: reusedRegexp}, {a: 'Shorts'}); @@ -983,7 +982,7 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({a: {$regex: reusedRegexp}}, {a: 'Shorts'}); match({a: {$regex: reusedRegexp}}, {a: 'Shorts'}); - test.throws(function () { + test.throws(() => { match({a: {$options: 'i'}}, {a: 12}); }); @@ -1001,10 +1000,10 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: /t/}, {a: true}); match({a: /m/i}, {a: ['x', 'xM']}); - test.throws(function () { + test.throws(() => { match({a: {$regex: /a/, $options: 'x'}}, {a: 'cat'}); }); - test.throws(function () { + test.throws(() => { match({a: {$regex: /a/, $options: 's'}}, {a: 'cat'}); }); @@ -1040,7 +1039,7 @@ Tinytest.add("minimongo - selector_compiler", function (test) { // dotted keypaths, nulls, numeric indices, arrays nomatch({"a.b": null}, {a: [1]}); match({"a.b": []}, {a: {b: []}}); - var big = {a: [{b: 1}, 2, {}, {b: [3, 4]}]}; + const big = {a: [{b: 1}, 2, {}, {b: [3, 4]}]}; match({"a.b": 1}, big); match({"a.b": [3, 4]}, big); match({"a.b": 3}, big); @@ -1096,13 +1095,13 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4]}}); // $or - test.throws(function () { + test.throws(() => { match({$or: []}, {}); }); - test.throws(function () { + test.throws(() => { match({$or: [5]}, {}); }); - test.throws(function () { + test.throws(() => { match({$or: []}, {a: 1}); }); match({$or: [{a: 1}]}, {a: 1}); @@ -1186,13 +1185,13 @@ Tinytest.add("minimongo - selector_compiler", function (test) { // this is possibly an open-ended task, so we stop here ... // $nor - test.throws(function () { + test.throws(() => { match({$nor: []}, {}); }); - test.throws(function () { + test.throws(() => { match({$nor: [5]}, {}); }); - test.throws(function () { + test.throws(() => { match({$nor: []}, {a: 1}); }); nomatch({$nor: [{a: 1}]}, {a: 1}); @@ -1267,13 +1266,13 @@ Tinytest.add("minimongo - selector_compiler", function (test) { // $and - test.throws(function () { + test.throws(() => { match({$and: []}, {}); }); - test.throws(function () { + test.throws(() => { match({$and: [5]}, {}); }); - test.throws(function () { + test.throws(() => { match({$and: []}, {a: 1}); }); match({$and: [{a: 1}]}, {a: 1}); @@ -1423,12 +1422,12 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, {a: [{x: 1}, {b: 1}]}); - test.throws(function () { + test.throws(() => { match({a: {$elemMatch: {$gte: 1, $or: [{a: 1}, {b: 1}]}}}, {a: [{x: 1, b: 1}]}); }); - test.throws(function () { + test.throws(() => { match({x: {$elemMatch: {$and: [{$gt: 5, $lt: 9}]}}}, {x: [8]}); }); @@ -1440,20 +1439,20 @@ Tinytest.add("minimongo - selector_compiler", function (test) { // - non-scalar arguments to $gt, $lt, etc }); -Tinytest.add("minimongo - projection_compiler", function (test) { - var testProjection = function (projection, tests) { - var projection_f = LocalCollection._compileProjection(projection); - var equalNonStrict = function (a, b, desc) { +Tinytest.add("minimongo - projection_compiler", test => { + const testProjection = (projection, tests) => { + const projection_f = LocalCollection._compileProjection(projection); + const equalNonStrict = (a, b, desc) => { test.isTrue(EJSON.equals(a, b), desc); }; - tests.forEach(function (testCase) { + tests.forEach(testCase => { equalNonStrict(projection_f(testCase[0]), testCase[1], testCase[2]); }); }; - var testCompileProjectionThrows = function (projection, expectedError) { - test.throws(function () { + const testCompileProjectionThrows = (projection, expectedError) => { + test.throws(() => { LocalCollection._compileProjection(projection); }, expectedError); }; @@ -1571,9 +1570,9 @@ Tinytest.add("minimongo - projection_compiler", function (test) { testCompileProjectionThrows("some string", "fields option must be an object"); }); -Tinytest.add("minimongo - fetch with fields", function (test) { - var c = new LocalCollection(); - Array.from({length: 30}, function (x, i) { +Tinytest.add("minimongo - fetch with fields", test => { + const c = new LocalCollection(); + Array.from({length: 30}, (x, i) => { c.insert({ something: Random.id(), anything: { @@ -1581,25 +1580,23 @@ Tinytest.add("minimongo - fetch with fields", function (test) { cool: "hot" }, nothing: i, - i: i + i }); }); // Test just a regular fetch with some projection - var fetchResults = c.find({}, { fields: { + let fetchResults = c.find({}, { fields: { 'something': 1, 'anything.foo': 1 } }).fetch(); - test.isTrue(fetchResults.every(function (x) { - return x && - x.something && - x.anything && - x.anything.foo && - x.anything.foo === "bar" && - !x.hasOwnProperty('nothing') && - !x.anything.hasOwnProperty('cool'); - })); + test.isTrue(fetchResults.every(x => x && + x.something && + x.anything && + x.anything.foo && + x.anything.foo === "bar" && + !x.hasOwnProperty('nothing') && + !x.anything.hasOwnProperty('cool'))); // Test with a selector, even field used in the selector is excluded in the // projection @@ -1609,16 +1606,14 @@ Tinytest.add("minimongo - fetch with fields", function (test) { fields: { nothing: 0 } }).fetch(); - test.isTrue(fetchResults.every(function (x) { - return x && - x.something && - x.anything && - x.anything.foo === "bar" && - x.anything.cool === "hot" && - !x.hasOwnProperty('nothing') && - x.i && - x.i >= 5; - })); + test.isTrue(fetchResults.every(x => x && + x.something && + x.anything && + x.anything.foo === "bar" && + x.anything.cool === "hot" && + !x.hasOwnProperty('nothing') && + x.i && + x.i >= 5)); test.isTrue(fetchResults.length === 25); @@ -1637,36 +1632,34 @@ Tinytest.add("minimongo - fetch with fields", function (test) { } }).fetch(); - test.isTrue(fetchResults.every(function (x) { - return x && - x.something && - x.i >= 10 && x.i < 20; - })); + test.isTrue(fetchResults.every(x => x && + x.something && + x.i >= 10 && x.i < 20)); - fetchResults.forEach(function (x, i, arr) { + fetchResults.forEach((x, i, arr) => { if (!i) return; test.isTrue(x.i === arr[i-1].i + 1); }); // Temporary unsupported operators // queries are taken from MongoDB docs examples - test.throws(function () { + test.throws(() => { c.find({}, { fields: { 'grades.$': 1 } }); }); - test.throws(function () { + test.throws(() => { c.find({}, { fields: { grades: { $elemMatch: { mean: 70 } } } }); }); - test.throws(function () { + test.throws(() => { c.find({}, { fields: { grades: { $slice: [20, 10] } } }); }); }); -Tinytest.add("minimongo - fetch with projection, subarrays", function (test) { +Tinytest.add("minimongo - fetch with projection, subarrays", test => { // Apparently projection of type 'foo.bar.x' for // { foo: [ { bar: { x: 42 } }, { bar: { x: 3 } } ] } // should return exactly this object. More precisely, arrays are considered as // sets and are queried separately and then merged back to result set - var c = new LocalCollection(); + const c = new LocalCollection(); // Insert a test object with two set fields c.insert({ @@ -1687,14 +1680,13 @@ Tinytest.add("minimongo - fetch with projection, subarrays", function (test) { }] }); - var equalNonStrict = function (a, b, desc) { + const equalNonStrict = (a, b, desc) => { test.isTrue(EJSON.equals(a, b), desc); }; - var testForProjection = function (projection, expected) { - var fetched = c.find({}, { fields: projection }).fetch()[0]; - equalNonStrict(fetched, expected, "failed sub-set projection: " + - JSON.stringify(projection)); + const testForProjection = (projection, expected) => { + const fetched = c.find({}, { fields: projection }).fetch()[0]; + equalNonStrict(fetched, expected, `failed sub-set projection: ${JSON.stringify(projection)}`); }; testForProjection({ 'setA.fieldA': 1, 'setB.anotherB': 1, _id: 0 }, @@ -1718,10 +1710,10 @@ Tinytest.add("minimongo - fetch with projection, subarrays", function (test) { {a: [ [ { c: 2 }, { c: 4 } ], { c: 5 }, [ { c: 9 } ] ] }); }); -Tinytest.add("minimongo - fetch with projection, deep copy", function (test) { +Tinytest.add("minimongo - fetch with projection, deep copy", test => { // Compiled fields projection defines the contract: returned document doesn't // retain anything from the passed argument. - var doc = { + const doc = { a: { x: 42 }, b: { y: { z: 33 } @@ -1729,13 +1721,13 @@ Tinytest.add("minimongo - fetch with projection, deep copy", function (test) { c: "asdf" }; - var fields = { + let fields = { 'a': 1, 'b.y': 1 }; - var projectionFn = LocalCollection._compileProjection(fields); - var filteredDoc = projectionFn(doc); + let projectionFn = LocalCollection._compileProjection(fields); + let filteredDoc = projectionFn(doc); doc.a.x++; doc.b.y.z--; test.equal(filteredDoc.a.x, 42, "projection returning deep copy - including"); @@ -1749,14 +1741,14 @@ Tinytest.add("minimongo - fetch with projection, deep copy", function (test) { test.equal(filteredDoc.a.x, 43, "projection returning deep copy - excluding"); }); -Tinytest.add("minimongo - observe ordered with projection", function (test) { +Tinytest.add("minimongo - observe ordered with projection", test => { // These tests are copy-paste from "minimongo -observe ordered", // slightly modified to test projection - var operations = []; - var cbs = log_callbacks(operations); - var handle; + const operations = []; + const cbs = log_callbacks(operations); + let handle; - var c = new LocalCollection(); + const c = new LocalCollection(); handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(cbs); test.isTrue(handle.collection === c); @@ -1780,16 +1772,16 @@ Tinytest.add("minimongo - observe ordered with projection", function (test) { // test stop handle.stop(); - var idA2 = Random.id(); + const idA2 = Random.id(); c.insert({_id: idA2, a:2}); test.equal(operations.shift(), undefined); - var cursor = c.find({}, {fields: {a: 1, _id: 0}}); - test.throws(function () { - cursor.observeChanges({added: function () {}}); + const cursor = c.find({}, {fields: {a: 1, _id: 0}}); + test.throws(() => { + cursor.observeChanges({added() {}}); }); - test.throws(function () { - cursor.observe({added: function () {}}); + test.throws(() => { + cursor.observe({added() {}}); }); // test initial inserts (and backwards sort) @@ -1850,16 +1842,16 @@ Tinytest.add("minimongo - observe ordered with projection", function (test) { }); -Tinytest.add("minimongo - ordering", function (test) { - var shortBinary = EJSON.newBinary(1); +Tinytest.add("minimongo - ordering", test => { + const shortBinary = EJSON.newBinary(1); shortBinary[0] = 128; - var longBinary1 = EJSON.newBinary(2); + const longBinary1 = EJSON.newBinary(2); longBinary1[1] = 42; - var longBinary2 = EJSON.newBinary(2); + const longBinary2 = EJSON.newBinary(2); longBinary2[1] = 50; - var date1 = new Date; - var date2 = new Date(date1.getTime() + 1000); + const date1 = new Date; + const date2 = new Date(date1.getTime() + 1000); // value ordering assert_ordering(test, LocalCollection._f._cmp, [ @@ -1877,9 +1869,9 @@ Tinytest.add("minimongo - ordering", function (test) { ]); // document ordering under a sort specification - var verify = function (sorts, docs) { - (Array.isArray(sorts) ? sorts : [sorts]).forEach(function (sort) { - var sorter = new Minimongo.Sorter(sort); + const verify = (sorts, docs) => { + (Array.isArray(sorts) ? sorts : [sorts]).forEach(sort => { + const sorter = new Minimongo.Sorter(sort); assert_ordering(test, sorter.getComparator(), docs); }); }; @@ -1906,17 +1898,17 @@ Tinytest.add("minimongo - ordering", function (test) { [["a", "asc"], ["b", "asc"]]], [{c: 1}, {a: 1, b: 2}, {a: 1, b: 3}, {a: 2, b: 0}]); - test.throws(function () { + test.throws(() => { new Minimongo.Sorter("a"); }); - test.throws(function () { + test.throws(() => { new Minimongo.Sorter(123); }); // We don't support $natural:1 (since we don't actually have Mongo's on-disk // ordering available!) - test.throws(function () { + test.throws(() => { new Minimongo.Sorter({$natural: 1}); }); @@ -2014,11 +2006,11 @@ Tinytest.add("minimongo - ordering", function (test) { ]); }); -Tinytest.add("minimongo - sort", function (test) { - var c = new LocalCollection(); - for (var i = 0; i < 50; i++) - for (var j = 0; j < 2; j++) - c.insert({a: i, b: j, _id: i + "_" + j}); +Tinytest.add("minimongo - sort", test => { + const c = new LocalCollection(); + for (let i = 0; i < 50; i++) + for (let j = 0; j < 2; j++) + c.insert({a: i, b: j, _id: `${i}_${j}`}); test.equal( c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, limit: 5}).fetch(), [ @@ -2045,41 +2037,41 @@ Tinytest.add("minimongo - sort", function (test) { {a: 47, b: 1, _id: "47_1"}]); }); -Tinytest.add("minimongo - subkey sort", function (test) { - var c = new LocalCollection(); +Tinytest.add("minimongo - subkey sort", test => { + const c = new LocalCollection(); // normal case c.insert({a: {b: 2}}); c.insert({a: {b: 1}}); c.insert({a: {b: 3}}); test.equal( - c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), + c.find({}, {sort: {'a.b': -1}}).fetch().map(doc => doc.a), [{b: 3}, {b: 2}, {b: 1}]); // isn't an object c.insert({a: 1}); test.equal( - c.find({}, {sort: {'a.b': 1}}).fetch().map(function (doc) { return doc.a; }), + c.find({}, {sort: {'a.b': 1}}).fetch().map(doc => doc.a), [1, {b: 1}, {b: 2}, {b: 3}]); // complex object c.insert({a: {b: {c: 1}}}); test.equal( - c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), + c.find({}, {sort: {'a.b': -1}}).fetch().map(doc => doc.a), [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1]); // no such top level prop c.insert({c: 1}); test.equal( - c.find({}, {sort: {'a.b': -1}}).fetch().map(function (doc) { return doc.a; }), + c.find({}, {sort: {'a.b': -1}}).fetch().map(doc => doc.a), [{b: {c: 1}}, {b: 3}, {b: 2}, {b: 1}, 1, undefined]); // no such mid level prop. just test that it doesn't throw. test.equal(c.find({}, {sort: {'a.nope.c': -1}}).count(), 6); }); -Tinytest.add("minimongo - array sort", function (test) { - var c = new LocalCollection(); +Tinytest.add("minimongo - array sort", test => { + const c = new LocalCollection(); // "up" and "down" are the indices that the docs should have when sorted // ascending and descending by "a.x" respectively. They are not reverses of @@ -2099,14 +2091,14 @@ Tinytest.add("minimongo - array sort", function (test) { // Test that the the documents in "cursor" contain values with the name // "field" running from 0 to the max value of that name in the collection. - var testCursorMatchesField = function (cursor, field) { - var fieldValues = []; - c.find().forEach(function (doc) { + const testCursorMatchesField = (cursor, field) => { + const fieldValues = []; + c.find().forEach(doc => { if (doc.hasOwnProperty(field)) fieldValues.push(doc[field]); }); - test.equal(cursor.fetch().map(function (doc) { return doc[field]; }), - Array.from({length: Math.max.apply(null, fieldValues) + 1}, function (x, i) { return i; })); + test.equal(cursor.fetch().map(doc => doc[field]), + Array.from({length: Math.max.apply(null, fieldValues) + 1}, (x, i) => i)); }; testCursorMatchesField(c.find({}, {sort: {'a.x': 1}}), 'up'); @@ -2115,31 +2107,31 @@ Tinytest.add("minimongo - array sort", function (test) { 'selected'); }); -Tinytest.add("minimongo - sort keys", function (test) { - var keyListToObject = function (keyList) { - var obj = {}; - keyList.forEach(function (key) { +Tinytest.add("minimongo - sort keys", test => { + const keyListToObject = keyList => { + const obj = {}; + keyList.forEach(key => { obj[EJSON.stringify(key)] = true; }); return obj; }; - var testKeys = function (sortSpec, doc, expectedKeyList) { - var expectedKeys = keyListToObject(expectedKeyList); - var sorter = new Minimongo.Sorter(sortSpec); + const testKeys = (sortSpec, doc, expectedKeyList) => { + const expectedKeys = keyListToObject(expectedKeyList); + const sorter = new Minimongo.Sorter(sortSpec); - var actualKeyList = []; - sorter._generateKeysFromDoc(doc, function (key) { + const actualKeyList = []; + sorter._generateKeysFromDoc(doc, key => { actualKeyList.push(key); }); - var actualKeys = keyListToObject(actualKeyList); + const actualKeys = keyListToObject(actualKeyList); test.equal(actualKeys, expectedKeys); }; - var testParallelError = function (sortSpec, doc) { - var sorter = new Minimongo.Sorter(sortSpec); - test.throws(function () { - sorter._generateKeysFromDoc(doc, function (){}); + const testParallelError = (sortSpec, doc) => { + const sorter = new Minimongo.Sorter(sortSpec); + test.throws(() => { + sorter._generateKeysFromDoc(doc, () => {}); }, /parallel arrays/); }; @@ -2178,12 +2170,12 @@ Tinytest.add("minimongo - sort keys", function (test) { {x: 2, y: [4, 5]}]}); }); -Tinytest.add("minimongo - sort key filter", function (test) { - var testOrder = function (sortSpec, selector, doc1, doc2) { - var matcher = new Minimongo.Matcher(selector); - var sorter = new Minimongo.Sorter(sortSpec, {matcher: matcher}); - var comparator = sorter.getComparator(); - var comparison = comparator(doc1, doc2); +Tinytest.add("minimongo - sort key filter", test => { + const testOrder = (sortSpec, selector, doc1, doc2) => { + const matcher = new Minimongo.Matcher(selector); + const sorter = new Minimongo.Sorter(sortSpec, {matcher}); + const comparator = sorter.getComparator(); + const comparison = comparator(doc1, doc2); test.isTrue(comparison < 0); }; @@ -2194,10 +2186,10 @@ Tinytest.add("minimongo - sort key filter", function (test) { {a: {x: [1, 4]}}, {a: {x: 3}}); - var keyCompatible = function (sortSpec, selector, key, compatible) { - var matcher = new Minimongo.Matcher(selector); - var sorter = new Minimongo.Sorter(sortSpec, {matcher: matcher}); - var actual = sorter._keyCompatibleWithSelector(key); + const keyCompatible = (sortSpec, selector, key, compatible) => { + const matcher = new Minimongo.Matcher(selector); + const sorter = new Minimongo.Sorter(sortSpec, {matcher}); + const actual = sorter._keyCompatibleWithSelector(key); test.equal(actual, compatible); }; @@ -2259,8 +2251,8 @@ Tinytest.add("minimongo - sort key filter", function (test) { {c: {$lt: 3}}, [3, "bla", 4], true); }); -Tinytest.add("minimongo - sort function", function (test) { - var c = new LocalCollection(); +Tinytest.add("minimongo - sort function", test => { + const c = new LocalCollection(); c.insert({a: 1}); c.insert({a: 10}); @@ -2270,38 +2262,31 @@ Tinytest.add("minimongo - sort function", function (test) { c.insert({a: 4}); c.insert({a: 3}); - var sortFunction = function (doc1, doc2) { - return doc2.a - doc1.a; - }; + const sortFunction = (doc1, doc2) => doc2.a - doc1.a; test.equal(c.find({}, {sort: sortFunction}).fetch(), c.find({}).fetch().sort(sortFunction)); test.notEqual(c.find({}).fetch(), c.find({}).fetch().sort(sortFunction)); test.equal(c.find({}, {sort: {a: -1}}).fetch(), c.find({}).fetch().sort(sortFunction)); }); -Tinytest.add("minimongo - binary search", function (test) { - var forwardCmp = function (a, b) { - return a - b; - }; +Tinytest.add("minimongo - binary search", test => { + const forwardCmp = (a, b) => a - b; - var backwardCmp = function (a, b) { - return -1 * forwardCmp(a, b); - }; + const backwardCmp = (a, b) => -1 * forwardCmp(a, b); - var checkSearch = function (cmp, array, value, expected, message) { - var actual = LocalCollection._binarySearch(cmp, array, value); + const checkSearch = (cmp, array, value, expected, message) => { + const actual = LocalCollection._binarySearch(cmp, array, value); if (expected != actual) { test.fail({type: "minimongo-binary-search", - message: message + " : Expected index " + expected + - " but had " + actual + message: `${message} : Expected index ${expected} but had ${actual}` }); } }; - var checkSearchForward = function (array, value, expected, message) { + const checkSearchForward = (array, value, expected, message) => { checkSearch(forwardCmp, array, value, expected, message); }; - var checkSearchBackward = function (array, value, expected, message) { + const checkSearchBackward = (array, value, expected, message) => { checkSearch(backwardCmp, array, value, expected, message); }; @@ -2336,41 +2321,41 @@ Tinytest.add("minimongo - binary search", function (test) { checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 3, 0, "Backward: Highly degenerate array, upper"); }); -Tinytest.add("minimongo - modify", function (test) { - var modifyWithQuery = function (doc, query, mod, expected) { - var coll = new LocalCollection; +Tinytest.add("minimongo - modify", test => { + const modifyWithQuery = (doc, query, mod, expected) => { + const coll = new LocalCollection; coll.insert(doc); // The query is relevant for 'a.$.b'. coll.update(query, mod); - var actual = coll.findOne(); + const actual = coll.findOne(); delete actual._id; // added by insert if (typeof expected === "function") { - expected(actual, EJSON.stringify({input: doc, mod: mod})); + expected(actual, EJSON.stringify({input: doc, mod})); } else { - test.equal(actual, expected, EJSON.stringify({input: doc, mod: mod})); + test.equal(actual, expected, EJSON.stringify({input: doc, mod})); } }; - var modify = function (doc, mod, expected) { + const modify = (doc, mod, expected) => { modifyWithQuery(doc, {}, mod, expected); }; - var exceptionWithQuery = function (doc, query, mod) { - var coll = new LocalCollection; + const exceptionWithQuery = (doc, query, mod) => { + const coll = new LocalCollection; coll.insert(doc); - test.throws(function () { + test.throws(() => { coll.update(query, mod); }); }; - var exception = function (doc, mod) { + const exception = (doc, mod) => { exceptionWithQuery(doc, {}, mod); }; - var upsert = function (query, mod, expected) { - var coll = new LocalCollection; + const upsert = (query, mod, expected) => { + const coll = new LocalCollection; - var result = coll.upsert(query, mod); + const result = coll.upsert(query, mod); - var actual = coll.findOne(); + const actual = coll.findOne(); if (expected._id) { test.equal(result.insertedId, expected._id); @@ -2382,9 +2367,9 @@ Tinytest.add("minimongo - modify", function (test) { test.equal(actual, expected); }; - var upsertException = function (query, mod) { - var coll = new LocalCollection; - test.throws(function(){ + const upsertException = (query, mod) => { + const coll = new LocalCollection; + test.throws(() => { coll.upsert(query, mod); }); }; @@ -2925,12 +2910,12 @@ Tinytest.add("minimongo - modify", function (test) { // XXX test update() (selecting docs, multi, upsert..) -Tinytest.add("minimongo - observe ordered", function (test) { - var operations = []; - var cbs = log_callbacks(operations); - var handle; +Tinytest.add("minimongo - observe ordered", test => { + const operations = []; + const cbs = log_callbacks(operations); + let handle; - var c = new LocalCollection(); + const c = new LocalCollection(); handle = c.find({}, {sort: {a: 1}}).observe(cbs); test.isTrue(handle.collection === c); @@ -2953,7 +2938,7 @@ Tinytest.add("minimongo - observe ordered", function (test) { // test stop handle.stop(); - var idA2 = Random.id(); + const idA2 = Random.id(); c.insert({_id: idA2, a:2}); test.equal(operations.shift(), undefined); @@ -3030,22 +3015,22 @@ Tinytest.add("minimongo - observe ordered", function (test) { handle.stop(); }); -[true, false].forEach(function (ordered) { - Tinytest.add("minimongo - observe ordered: " + ordered, function (test) { - var c = new LocalCollection(); +[true, false].forEach(ordered => { + Tinytest.add(`minimongo - observe ordered: ${ordered}`, test => { + const c = new LocalCollection(); - var ev = ""; - var makecb = function (tag) { - var ret = {}; - ["added", "changed", "removed"].forEach(function (fn) { - var fnName = ordered ? fn + "At" : fn; - ret[fnName] = function (doc) { - ev = (ev + fn.substr(0, 1) + tag + doc._id + "_"); + let ev = ""; + const makecb = tag => { + const ret = {}; + ["added", "changed", "removed"].forEach(fn => { + const fnName = ordered ? `${fn}At` : fn; + ret[fnName] = doc => { + ev = (`${ev + fn.substr(0, 1) + tag + doc._id}_`); }; }); return ret; }; - var expect = function (x) { + const expect = x => { test.equal(ev, x); ev = ""; }; @@ -3057,7 +3042,7 @@ Tinytest.add("minimongo - observe ordered", function (test) { // This should work equally well for ordered and unordered observations // (because the callbacks don't look at indices and there's no 'moved' // callback). - var handle = c.find({tags: "flower"}).observe(makecb('a')); + let handle = c.find({tags: "flower"}).observe(makecb('a')); expect("aa3_"); c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}}); expect("ra3_"); @@ -3092,10 +3077,11 @@ Tinytest.add("minimongo - observe ordered", function (test) { }); -Tinytest.add("minimongo - saveOriginals", function (test) { +Tinytest.add("minimongo - saveOriginals", test => { // set up some data - var c = new LocalCollection(), - count; + const c = new LocalCollection(); + + let count; c.insert({_id: 'foo', x: 'untouched'}); c.insert({_id: 'bar', x: 'updateme'}); c.insert({_id: 'baz', x: 'updateme'}); @@ -3113,10 +3099,10 @@ Tinytest.add("minimongo - saveOriginals", function (test) { test.equal(count, 2); // Verify the originals. - var originals = c.retrieveOriginals(); - var affected = ['bar', 'baz', 'quux', 'whoa', 'hooray']; + let originals = c.retrieveOriginals(); + const affected = ['bar', 'baz', 'quux', 'whoa', 'hooray']; test.equal(originals.size(), affected.length); - affected.forEach(function (id) { + affected.forEach(id => { test.isTrue(originals.has(id)); }); test.equal(originals.get('bar'), {_id: 'bar', x: 'updateme'}); @@ -3148,20 +3134,20 @@ Tinytest.add("minimongo - saveOriginals", function (test) { test.equal(originals.get('temp'), undefined); }); -Tinytest.add("minimongo - saveOriginals errors", function (test) { - var c = new LocalCollection(); +Tinytest.add("minimongo - saveOriginals errors", test => { + const c = new LocalCollection(); // Can't call retrieve before save. - test.throws(function () { c.retrieveOriginals(); }); + test.throws(() => { c.retrieveOriginals(); }); c.saveOriginals(); // Can't call save twice. - test.throws(function () { c.saveOriginals(); }); + test.throws(() => { c.saveOriginals(); }); }); -Tinytest.add("minimongo - objectid transformation", function (test) { - var testId = function (item) { +Tinytest.add("minimongo - objectid transformation", test => { + const testId = item => { test.equal(item, MongoID.idParse(MongoID.idStringify(item))); }; - var randomOid = new MongoID.ObjectID(); + const randomOid = new MongoID.ObjectID(); testId(randomOid); testId("FOO"); testId("ffffffffffff"); @@ -3173,21 +3159,21 @@ Tinytest.add("minimongo - objectid transformation", function (test) { }); -Tinytest.add("minimongo - objectid", function (test) { - var randomOid = new MongoID.ObjectID(); - var anotherRandomOid = new MongoID.ObjectID(); +Tinytest.add("minimongo - objectid", test => { + const randomOid = new MongoID.ObjectID(); + const anotherRandomOid = new MongoID.ObjectID(); test.notEqual(randomOid, anotherRandomOid); - test.throws(function() { new MongoID.ObjectID("qqqqqqqqqqqqqqqqqqqqqqqq");}); - test.throws(function() { new MongoID.ObjectID("ABCDEF"); }); + test.throws(() => { new MongoID.ObjectID("qqqqqqqqqqqqqqqqqqqqqqqq");}); + test.throws(() => { new MongoID.ObjectID("ABCDEF"); }); test.equal(randomOid, new MongoID.ObjectID(randomOid.valueOf())); }); -Tinytest.add("minimongo - pause", function (test) { - var operations = []; - var cbs = log_callbacks(operations); +Tinytest.add("minimongo - pause", test => { + const operations = []; + const cbs = log_callbacks(operations); - var c = new LocalCollection(); - var h = c.find({}).observe(cbs); + const c = new LocalCollection(); + const h = c.find({}).observe(cbs); // remove and add cancel out. c.insert({_id: 1, a: 1}); @@ -3225,15 +3211,15 @@ Tinytest.add("minimongo - pause", function (test) { h.stop(); }); -Tinytest.add("minimongo - ids matched by selector", function (test) { - var check = function (selector, ids) { - var idsFromSelector = LocalCollection._idsMatchedBySelector(selector); +Tinytest.add("minimongo - ids matched by selector", test => { + const check = (selector, ids) => { + const idsFromSelector = LocalCollection._idsMatchedBySelector(selector); // XXX normalize order, in a way that also works for ObjectIDs? test.equal(idsFromSelector, ids); }; check("foo", ["foo"]); check({_id: "foo"}, ["foo"]); - var oid1 = new MongoID.ObjectID(); + const oid1 = new MongoID.ObjectID(); check(oid1, [oid1]); check({_id: oid1}, [oid1]); check({_id: "foo", x: 42}, ["foo"]); @@ -3246,30 +3232,30 @@ Tinytest.add("minimongo - ids matched by selector", function (test) { check({$and: [{x: 42}, {_id: {$in: [oid1]}}]}, [oid1]); }); -Tinytest.add("minimongo - reactive stop", function (test) { - var coll = new LocalCollection(); +Tinytest.add("minimongo - reactive stop", test => { + const coll = new LocalCollection(); coll.insert({_id: 'A'}); coll.insert({_id: 'B'}); coll.insert({_id: 'C'}); - var addBefore = function (str, newChar, before) { - var idx = str.indexOf(before); + const addBefore = (str, newChar, before) => { + const idx = str.indexOf(before); if (idx === -1) return str + newChar; return str.slice(0, idx) + newChar + str.slice(idx); }; - var x, y; - var sortOrder = ReactiveVar(1); + let x, y; + const sortOrder = ReactiveVar(1); - var c = Tracker.autorun(function () { - var q = coll.find({}, {sort: {_id: sortOrder.get()}}); + const c = Tracker.autorun(() => { + const q = coll.find({}, {sort: {_id: sortOrder.get()}}); x = ""; - q.observe({ addedAt: function (doc, atIndex, before) { + q.observe({ addedAt(doc, atIndex, before) { x = addBefore(x, doc._id, before); }}); y = ""; - q.observeChanges({ addedBefore: function (id, fields, before) { + q.observeChanges({ addedBefore(id, fields, before) { y = addBefore(y, id, before); }}); }); @@ -3296,8 +3282,8 @@ Tinytest.add("minimongo - reactive stop", function (test) { test.equal(y, "EDCBA"); }); -Tinytest.add("minimongo - immediate invalidate", function (test) { - var coll = new LocalCollection(); +Tinytest.add("minimongo - immediate invalidate", test => { + const coll = new LocalCollection(); coll.insert({_id: 'A'}); // This has two separate findOnes. findOne() uses skip/limit, which means @@ -3306,7 +3292,7 @@ Tinytest.add("minimongo - immediate invalidate", function (test) { // recomputed, then recompute them one by one, without checking to see if the // callbacks from recomputing one query stopped the second query, which // crashed. - var c = Tracker.autorun(function () { + const c = Tracker.autorun(() => { coll.findOne('A'); coll.findOne('A'); }); @@ -3317,16 +3303,17 @@ Tinytest.add("minimongo - immediate invalidate", function (test) { }); -Tinytest.add("minimongo - count on cursor with limit", function(test){ - var coll = new LocalCollection(), count; +Tinytest.add("minimongo - count on cursor with limit", test => { + const coll = new LocalCollection(); + let count; coll.insert({_id: 'A'}); coll.insert({_id: 'B'}); coll.insert({_id: 'C'}); coll.insert({_id: 'D'}); - var c = Tracker.autorun(function (c) { - var cursor = coll.find({_id: {$exists: true}}, {sort: {_id: 1}, limit: 3}); + const c = Tracker.autorun(c => { + const cursor = coll.find({_id: {$exists: true}}, {sort: {_id: 1}, limit: 3}); count = cursor.count(); }); @@ -3352,14 +3339,14 @@ Tinytest.add("minimongo - count on cursor with limit", function(test){ c.stop(); }); -Tinytest.add("minimongo - reactive count with cached cursor", function (test) { - var coll = new LocalCollection; - var cursor = coll.find({}); - var firstAutorunCount, secondAutorunCount; - Tracker.autorun(function(){ +Tinytest.add("minimongo - reactive count with cached cursor", test => { + const coll = new LocalCollection; + const cursor = coll.find({}); + let firstAutorunCount, secondAutorunCount; + Tracker.autorun(() => { firstAutorunCount = cursor.count(); }); - Tracker.autorun(function(){ + Tracker.autorun(() => { secondAutorunCount = coll.find({}).count(); }); test.equal(firstAutorunCount, 0); @@ -3372,44 +3359,44 @@ Tinytest.add("minimongo - reactive count with cached cursor", function (test) { test.equal(secondAutorunCount, 3); }); -Tinytest.add("minimongo - $near operator tests", function (test) { - var coll = new LocalCollection(); +Tinytest.add("minimongo - $near operator tests", test => { + let coll = new LocalCollection(); coll.insert({ rest: { loc: [2, 3] } }); coll.insert({ rest: { loc: [-3, 3] } }); coll.insert({ rest: { loc: [5, 5] } }); test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 30 } }).count(), 3); test.equal(coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 1); - var points = coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 6 } }).fetch(); - points.forEach(function (point, i, points) { + const points = coll.find({ 'rest.loc': { $near: [0, 0], $maxDistance: 6 } }).fetch(); + points.forEach((point, i, points) => { test.isTrue(!i || distance([0, 0], point.rest.loc) >= distance([0, 0], points[i - 1].rest.loc)); }); function distance(a, b) { - var x = a[0] - b[0]; - var y = a[1] - b[1]; + const x = a[0] - b[0]; + const y = a[1] - b[1]; return Math.sqrt(x * x + y * y); } // GeoJSON tests coll = new LocalCollection(); - var data = [{ "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] } }, + const data = [{ "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] } }, { "category" : "WEAPON LAWS", "descript" : "POSS OF PROHIBITED WEAPON", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] } }, { "category" : "LARCENY/THEFT", "descript" : "GRAND THEFT OF PROPERTY", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.41538270191, 37.774683628213 ] } }, { "category" : "LARCENY/THEFT", "descript" : "PETTY THEFT FROM LOCKED AUTO", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415396041221, 37.7747879744156 ] } }, { "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879734156 ] } } ]; - data.forEach(function (x, i) { coll.insert(Object.assign(x, { x: i })); }); + data.forEach((x, i) => { coll.insert(Object.assign(x, { x: i })); }); - var close15 = coll.find({ location: { $near: { + const close15 = coll.find({ location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 15 } } }).fetch(); test.length(close15, 1); test.equal(close15[0].descript, "GRAND THEFT OF PROPERTY"); - var close20 = coll.find({ location: { $near: { + const close20 = coll.find({ location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 } } }).fetch(); @@ -3420,7 +3407,7 @@ Tinytest.add("minimongo - $near operator tests", function (test) { test.equal(close20[3].descript, "POSS OF PROHIBITED WEAPON"); // Any combinations of $near with $or/$and/$nor/$not should throw an error - test.throws(function () { + test.throws(() => { coll.find({ location: { $not: { $near: { @@ -3429,25 +3416,25 @@ Tinytest.add("minimongo - $near operator tests", function (test) { coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 } } } }); }); - test.throws(function () { + test.throws(() => { coll.find({ $and: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, { x: 0 }] }); }); - test.throws(function () { + test.throws(() => { coll.find({ $or: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, { x: 0 }] }); }); - test.throws(function () { + test.throws(() => { coll.find({ $nor: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 1 }}}, { x: 0 }] }); }); - test.throws(function () { + test.throws(() => { coll.find({ $and: [{ $and: [{ @@ -3479,9 +3466,9 @@ Tinytest.add("minimongo - $near operator tests", function (test) { _id: "y", k: 9, a: {b: [5, 5]}}); - var testNear = function (near, md, expected) { + const testNear = (near, md, expected) => { test.equal( - coll.find({'a.b': {$near: near, $maxDistance: md}}).fetch().map(function (doc) { return doc._id }), + coll.find({'a.b': {$near: near, $maxDistance: md}}).fetch().map(doc => doc._id), expected); }; testNear([149, 149], 4, ['x']); @@ -3494,15 +3481,15 @@ Tinytest.add("minimongo - $near operator tests", function (test) { // issue #3599 // Ensure that distance is not used as a tie-breaker for sort. test.equal( - coll.find({'a.b': {$near: [1, 1]}}, {sort: {k: 1}}).fetch().map(function (doc) { return doc._id; }), + coll.find({'a.b': {$near: [1, 1]}}, {sort: {k: 1}}).fetch().map(doc => doc._id), ['x', 'y']); test.equal( - coll.find({'a.b': {$near: [5, 5]}}, {sort: {k: 1}}).fetch().map(function (doc) { return doc._id; }), + coll.find({'a.b': {$near: [5, 5]}}, {sort: {k: 1}}).fetch().map(doc => doc._id), ['x', 'y']); - var operations = []; - var cbs = log_callbacks(operations); - var handle = coll.find({'a.b': {$near: [7,7]}}).observe(cbs); + const operations = []; + const cbs = log_callbacks(operations); + const handle = coll.find({'a.b': {$near: [7,7]}}).observe(cbs); test.length(operations, 2); test.equal(operations.shift(), ['added', {k:9, a:{b:[5,5]}}, 0, null]); @@ -3518,8 +3505,8 @@ Tinytest.add("minimongo - $near operator tests", function (test) { }); // issue #2077 -Tinytest.add("minimongo - $near and $geometry for legacy coordinates", function(test){ - var coll = new LocalCollection(); +Tinytest.add("minimongo - $near and $geometry for legacy coordinates", test => { + const coll = new LocalCollection(); coll.insert({ loc: { @@ -3548,30 +3535,30 @@ Tinytest.add("minimongo - $near and $geometry for legacy coordinates", function( // Regression test for #4377. Previously, "replace" updates didn't clone the // argument. -Tinytest.add("minimongo - update should clone", function (test) { - var x = []; - var coll = new LocalCollection; - var id = coll.insert({}); - coll.update(id, {x: x}); +Tinytest.add("minimongo - update should clone", test => { + const x = []; + const coll = new LocalCollection; + const id = coll.insert({}); + coll.update(id, {x}); x.push(1); test.equal(coll.findOne(id), {_id: id, x: []}); }); // See #2275. -Tinytest.add("minimongo - fetch in observe", function (test) { - var coll = new LocalCollection; - var callbackInvoked = false; - var observe = coll.find().observeChanges({ - added: function (id, fields) { +Tinytest.add("minimongo - fetch in observe", test => { + const coll = new LocalCollection; + let callbackInvoked = false; + const observe = coll.find().observeChanges({ + added(id, fields) { callbackInvoked = true; test.equal(fields, {foo: 1}); - var doc = coll.findOne({foo: 1}); + const doc = coll.findOne({foo: 1}); test.isTrue(doc); test.equal(doc.foo, 1); } }); test.isFalse(callbackInvoked); - var computation = Tracker.autorun(function (computation) { + const computation = Tracker.autorun(computation => { if (computation.firstRun) { coll.insert({foo: 1}); } @@ -3582,14 +3569,14 @@ Tinytest.add("minimongo - fetch in observe", function (test) { }); // See #2254 -Tinytest.add("minimongo - fine-grained reactivity of observe with fields projection", function (test) { - var X = new LocalCollection; - var id = "asdf"; +Tinytest.add("minimongo - fine-grained reactivity of observe with fields projection", test => { + const X = new LocalCollection; + const id = "asdf"; X.insert({_id: id, foo: {bar: 123}}); - var callbackInvoked = false; - var obs = X.find(id, {fields: {'foo.bar': 1}}).observeChanges({ - changed: function (id, fields) { + let callbackInvoked = false; + const obs = X.find(id, {fields: {'foo.bar': 1}}).observeChanges({ + changed(id, fields) { callbackInvoked = true; } }); @@ -3600,13 +3587,13 @@ Tinytest.add("minimongo - fine-grained reactivity of observe with fields project obs.stop(); }); -Tinytest.add("minimongo - fine-grained reactivity of query with fields projection", function (test) { - var X = new LocalCollection; - var id = "asdf"; +Tinytest.add("minimongo - fine-grained reactivity of query with fields projection", test => { + const X = new LocalCollection; + const id = "asdf"; X.insert({_id: id, foo: {bar: 123}}); - var callbackInvoked = false; - var computation = Tracker.autorun(function () { + let callbackInvoked = false; + const computation = Tracker.autorun(() => { callbackInvoked = true; return X.findOne(id, { fields: { 'foo.bar': 1 } }); }); @@ -3624,11 +3611,11 @@ Tinytest.add("minimongo - fine-grained reactivity of query with fields projectio // Tests that the logic in `LocalCollection.prototype.update` // correctly deals with count() on a cursor with skip or limit (since // then the result set is an IdMap, not an array) -Tinytest.add("minimongo - reactive skip/limit count while updating", function(test) { - var X = new LocalCollection; - var count = -1; +Tinytest.add("minimongo - reactive skip/limit count while updating", test => { + const X = new LocalCollection; + let count = -1; - var c = Tracker.autorun(function() { + const c = Tracker.autorun(() => { count = X.find({}, {skip: 1, limit: 1}).count(); }); @@ -3657,7 +3644,7 @@ Tinytest.add("minimongo - reactive skip/limit count while updating", function(te // Makes sure inserts cannot be performed using field names that have // Mongo restricted characters in them ('.', '$', '\0'): // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot insert using invalid field names", function (test) { +Tinytest.add("minimongo - cannot insert using invalid field names", test => { const collection = new LocalCollection(); // Quick test to make sure non-dot field inserts are working @@ -3668,42 +3655,42 @@ Tinytest.add("minimongo - cannot insert using invalid field names", function (te // Verify top level dot-field inserts are prohibited ['a.b', '.b', 'a.', 'a.b.c'].forEach((field) => { - test.throws(function () { + test.throws(() => { collection.insert({ [field]: 'c' }); }, `Key ${field} must not contain '.'`); }); // Verify nested dot-field inserts are prohibited - test.throws(function () { + test.throws(() => { collection.insert({ a: { b: { 'c.d': 'e' } } }); }, "Key c.d must not contain '.'"); // Verify field names starting with $ are prohibited - test.throws(function () { + test.throws(() => { collection.insert({ '$a': 'b' }); }, "Key $a must not start with '$'"); // Verify nested field names starting with $ are prohibited - test.throws(function () { + test.throws(() => { collection.insert({ a: { b: { '$c': 'd' } } }); }, "Key $c must not start with '$'"); // Verify top level fields with null characters are prohibited ['\0a', 'a\0', 'a\0b', '\u0000a', 'a\u0000', 'a\u0000b'].forEach((field) => { - test.throws(function () { + test.throws(() => { collection.insert({ [field]: 'c' }); }, `Key ${field} must not contain null bytes`); }); // Verify nested field names with null characters are prohibited - test.throws(function () { + test.throws(() => { collection.insert({ a: { b: { '\0c': 'd' } } }); }, 'Key \0c must not contain null bytes'); }); // Makes sure $set's cannot be performed using null bytes // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot $set with null bytes", function (test) { +Tinytest.add("minimongo - cannot $set with null bytes", test => { const collection = new LocalCollection(); // Quick test to make sure non-null byte $set's are working @@ -3718,7 +3705,7 @@ Tinytest.add("minimongo - cannot $set with null bytes", function (test) { // Makes sure $rename's cannot be performed using null bytes // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot $rename with null bytes", function (test) { +Tinytest.add("minimongo - cannot $rename with null bytes", test => { const collection = new LocalCollection(); // Quick test to make sure non-null byte $rename's are working diff --git a/packages/minimongo/minimongo_tests_server.js b/packages/minimongo/minimongo_tests_server.js index 38cc1476da..b7c59ee8b6 100644 --- a/packages/minimongo/minimongo_tests_server.js +++ b/packages/minimongo/minimongo_tests_server.js @@ -1,6 +1,6 @@ -Tinytest.add("minimongo - modifier affects selector", function (test) { +Tinytest.add("minimongo - modifier affects selector", test => { function testSelectorPaths (sel, paths, desc) { - var matcher = new Minimongo.Matcher(sel); + const matcher = new Minimongo.Matcher(sel); test.equal(matcher._getPaths(), paths, desc); } @@ -59,7 +59,7 @@ Tinytest.add("minimongo - modifier affects selector", function (test) { }, ['x', 'y'], "$or and elemMatch"); function testSelectorAffectedByModifier (sel, mod, yes, desc) { - var matcher = new Minimongo.Matcher(sel); + const matcher = new Minimongo.Matcher(sel); test.equal(matcher.affectedByModifier(mod), yes, desc); } @@ -96,9 +96,9 @@ Tinytest.add("minimongo - modifier affects selector", function (test) { affected({foo: {$elemMatch: {bar: 5}}}, {$set: {'foo.4.bar': 5}}, "$elemMatch"); }); -Tinytest.add("minimongo - selector and projection combination", function (test) { +Tinytest.add("minimongo - selector and projection combination", test => { function testSelProjectionComb (sel, proj, expected, desc) { - var matcher = new Minimongo.Matcher(sel); + const matcher = new Minimongo.Matcher(sel); test.equal(matcher.combineIntoProjection(proj), expected, desc); } @@ -194,7 +194,7 @@ Tinytest.add("minimongo - selector and projection combination", function (test) testSelProjectionComb({ 'a.b.c': 42, - $where: function () { return true; } + $where() { return true; } }, { 'a.b': 1, 'z.z': 1 @@ -203,7 +203,7 @@ Tinytest.add("minimongo - selector and projection combination", function (test) testSelProjectionComb({ $or: [ {'a.b.c': 42}, - {$where: function () { return true; } } + {$where() { return true; } } ] }, { 'a.b': 1, @@ -316,7 +316,7 @@ Tinytest.add("minimongo - selector and projection combination", function (test) testSelProjectionComb({ 'a.b.c': 42, - $where: function () { return true; } + $where() { return true; } }, { 'a.b': 0, 'z.z': 0 @@ -325,7 +325,7 @@ Tinytest.add("minimongo - selector and projection combination", function (test) testSelProjectionComb({ $or: [ {'a.b.c': 42}, - {$where: function () { return true; } } + {$where() { return true; } } ] }, { 'a.b': 0, @@ -334,9 +334,9 @@ Tinytest.add("minimongo - selector and projection combination", function (test) }); -Tinytest.add("minimongo - sorter and projection combination", function (test) { +Tinytest.add("minimongo - sorter and projection combination", test => { function testSorterProjectionComb (sortSpec, proj, expected, desc) { - var sorter = new Minimongo.Sorter(sortSpec); + const sorter = new Minimongo.Sorter(sortSpec); test.equal(sorter.combineIntoProjection(proj), expected, desc); } @@ -359,7 +359,7 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { }); -(function () { +((() => { // TODO: Tests for "can selector become true by modifier" are incomplete, // absent or test the functionality of "not ideal" implementation (test checks // that certain case always returns true as implementation is incomplete) @@ -377,11 +377,11 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { // * gives up on a combination of $gt/$gte/$lt/$lte and $ne/$nin // * doesn't support $eq properly - var test = null; // set this global in the beginning of every test + let test = null; // set this global in the beginning of every test // T - should return true // F - should return false - var oneTest = function (sel, mod, expected, desc) { - var matcher = new Minimongo.Matcher(sel); + const oneTest = (sel, mod, expected, desc) => { + const matcher = new Minimongo.Matcher(sel); test.equal(matcher.canBecomeTrueByModifier(mod), expected, desc); }; function T (sel, mod, desc) { @@ -391,10 +391,10 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { oneTest(sel, mod, false, desc); } - Tinytest.add("minimongo - can selector become true by modifier - literals (structured tests)", function (t) { + Tinytest.add("minimongo - can selector become true by modifier - literals (structured tests)", t => { test = t; - var selector = { + const selector = { 'a.b.c': 2, 'foo.bar': { z: { y: 1 } @@ -431,7 +431,7 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { F(selector, {$set:{ 'empty.field.a': 3 }}); }); - Tinytest.add("minimongo - can selector become true by modifier - literals (adhoc tests)", function (t) { + Tinytest.add("minimongo - can selector become true by modifier - literals (adhoc tests)", t => { test = t; T({x:1}, {$set:{x:1}}, "simple set scalar"); T({x:"a"}, {$set:{x:"a"}}, "simple set scalar"); @@ -446,7 +446,7 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { F({'foo.bar.baz': 1}, {$unset:{'foo.bar.bar': 1}}, "simple unset of the interesting path prefix"); }); - Tinytest.add("minimongo - can selector become true by modifier - regexps", function (t) { + Tinytest.add("minimongo - can selector become true by modifier - regexps", t => { test = t; // Regexp @@ -458,7 +458,7 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { T({ 'foo.bar': /^[0-9]+$/i, x: 1 }, { $set: { x: 1 } }, "don't touch regexp"); }); - Tinytest.add("minimongo - can selector become true by modifier - undefined/null", function (t) { + Tinytest.add("minimongo - can selector become true by modifier - undefined/null", t => { test = t; // Nulls / Undefined T({ 'foo.bar': null }, {$set:{'foo.bar': null}}, "set of null looking for null"); @@ -474,7 +474,7 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { }); - Tinytest.add("minimongo - can selector become true by modifier - literals with arrays", function (t) { + Tinytest.add("minimongo - can selector become true by modifier - literals with arrays", t => { test = t; // These tests are incomplete and in theory they all should return true as we // don't support any case with numeric fields yet. @@ -490,7 +490,7 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { T({'a.b': 1}, {$unset:{'a.1.b': 1}}, "unset of array element's field"); }); - Tinytest.add("minimongo - can selector become true by modifier - set an object literal whose fields are selected", function (t) { + Tinytest.add("minimongo - can selector become true by modifier - set an object literal whose fields are selected", t => { test = t; T({ 'a.b.c': 1 }, { $set: { 'a.b': { c: 1 } } }, "a simple scalar selector and simple set"); F({ 'a.b.c': 1 }, { $set: { 'a.b': { c: 2 } } }, "a simple scalar selector and simple set to false"); @@ -498,7 +498,7 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { F({ 'a.b.c': 1 }, { $set: { 'a.b': 222 } }, "a simple scalar selector and simple set a wrong type"); }); - Tinytest.add("minimongo - can selector become true by modifier - $-scalar selectors and simple tests", function (t) { + Tinytest.add("minimongo - can selector become true by modifier - $-scalar selectors and simple tests", t => { test = t; T({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { c: 4 } } }, "nested $lt"); F({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { c: 5 } } }, "nested $lt"); @@ -551,7 +551,7 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { F({ 'a.b': { $gt: 5, $lt: 7}, x: 1 }, { $set: { x: 1 }, $unset: { 'a.b.c': 1 } }, "unset sub-field of $gt,$lt operator (scalar expected)"); }); - Tinytest.add("minimongo - can selector become true by modifier - $-nonscalar selectors and simple tests", function (t) { + Tinytest.add("minimongo - can selector become true by modifier - $-nonscalar selectors and simple tests", t => { test = t; T({ a: { $eq: { x: 5 } } }, { $set: { 'a.x': 5 } }, "set of $eq"); // XXX this test should be F, but it is not implemented yet @@ -568,4 +568,4 @@ Tinytest.add("minimongo - sorter and projection combination", function (test) { // XXX this test should be F, but it is not implemented yet T({ a: { $ne: { a: 2 } } }, { $set: { a: { a: 2 } } }, "$ne object"); }); -})(); +}))(); diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index b998055f64..a332a5bfaa 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -22,26 +22,26 @@ import { export class Sorter { constructor (spec, options) { - var self = this; + const self = this; options = options || {}; self._sortSpecParts = []; self._sortFunction = null; - var addSpecPart = function (path, ascending) { + const addSpecPart = (path, ascending) => { if (!path) throw Error("sort keys must be non-empty"); if (path.charAt(0) === '$') - throw Error("unsupported sort key: " + path); + throw Error(`unsupported sort key: ${path}`); self._sortSpecParts.push({ - path: path, + path, lookup: makeLookupFunction(path, {forSort: true}), - ascending: ascending + ascending }); }; if (spec instanceof Array) { - for (var i = 0; i < spec.length; i++) { + for (let i = 0; i < spec.length; i++) { if (typeof spec[i] === "string") { addSpecPart(spec[i], true); } else { @@ -49,14 +49,14 @@ export class Sorter { } } } else if (typeof spec === "object") { - Object.keys(spec).forEach(function (key) { - var value = spec[key]; + Object.keys(spec).forEach(key => { + const value = spec[key]; addSpecPart(key, value >= 0); }); } else if (typeof spec === "function") { self._sortFunction = spec; } else { - throw Error("Bad sort specification: " + JSON.stringify(spec)); + throw Error(`Bad sort specification: ${JSON.stringify(spec)}`); } // If a function is specified for sorting, we skip the rest. @@ -67,17 +67,15 @@ export class Sorter { // affectedByModifier code; we create a selector that is affected by the same // modifiers as this sort order. This is only implemented on the server. if (self.affectedByModifier) { - var selector = {}; - self._sortSpecParts.forEach(function (spec) { + const selector = {}; + self._sortSpecParts.forEach(spec => { selector[spec.path] = 1; }); self._selectorForAffectedByModifier = new Minimongo.Matcher(selector); } self._keyComparator = composeComparators( - self._sortSpecParts.map(function (spec, i) { - return self._keyFieldComparator(i); - })); + self._sortSpecParts.map((spec, i) => self._keyFieldComparator(i))); // If you specify a matcher for this Sorter, _keyFilter may be set to a // function which selects whether or not a given "sort key" (tuple of values @@ -87,7 +85,7 @@ export class Sorter { } getComparator (options) { - var self = this; + const self = this; // If sort is specified or have no distances, just use the comparator from // the source specification (which defaults to "everything is equal". @@ -98,14 +96,14 @@ export class Sorter { return self._getBaseComparator(); } - var distances = options.distances; + const distances = options.distances; // Return a comparator which compares using $near distances. - return function (a, b) { + return (a, b) => { if (!distances.has(a._id)) - throw Error("Missing distance for " + a._id); + throw Error(`Missing distance for ${a._id}`); if (!distances.has(b._id)) - throw Error("Missing distance for " + b._id); + throw Error(`Missing distance for ${b._id}`); return distances.get(a._id) - distances.get(b._id); }; } @@ -114,7 +112,7 @@ export class Sorter { // parts. Returns negative, 0, or positive based on using the sort spec to // compare fields. _compareKeys (key1, key2) { - var self = this; + const self = this; if (key1.length !== self._sortSpecParts.length || key2.length !== self._sortSpecParts.length) { throw Error("Key has wrong length"); @@ -126,33 +124,31 @@ export class Sorter { // Iterates over each possible "key" from doc (ie, over each branch), calling // 'cb' with the key. _generateKeysFromDoc (doc, cb) { - var self = this; + const self = this; if (self._sortSpecParts.length === 0) throw new Error("can't generate keys without a spec"); // maps index -> ({'' -> value} or {path -> value}) - var valuesByIndexAndPath = []; + const valuesByIndexAndPath = []; - var pathFromIndices = function (indices) { - return indices.join(',') + ','; - }; + const pathFromIndices = indices => `${indices.join(',')},`; - var knownPaths = null; + let knownPaths = null; - self._sortSpecParts.forEach(function (spec, whichField) { + self._sortSpecParts.forEach((spec, whichField) => { // Expand any leaf arrays that we find, and ignore those arrays // themselves. (We never sort based on an array itself.) - var branches = expandArraysInBranches(spec.lookup(doc), true); + let branches = expandArraysInBranches(spec.lookup(doc), true); // If there are no values for a key (eg, key goes to an empty array), // pretend we found one null value. if (!branches.length) branches = [{value: null}]; - var usedPaths = false; + let usedPaths = false; valuesByIndexAndPath[whichField] = {}; - branches.forEach(function (branch) { + branches.forEach(branch => { if (!branch.arrayIndices) { // If there are no array indices for a branch, then it must be the // only branch, because the only thing that produces multiple branches @@ -164,9 +160,9 @@ export class Sorter { } usedPaths = true; - var path = pathFromIndices(branch.arrayIndices); + const path = pathFromIndices(branch.arrayIndices); if (valuesByIndexAndPath[whichField].hasOwnProperty(path)) - throw Error("duplicate path: " + path); + throw Error(`duplicate path: ${path}`); valuesByIndexAndPath[whichField][path] = branch.value; // If two sort fields both go into arrays, they have to go into the @@ -193,7 +189,7 @@ export class Sorter { } } else if (usedPaths) { knownPaths = {}; - Object.keys(valuesByIndexAndPath[whichField]).forEach(function (path) { + Object.keys(valuesByIndexAndPath[whichField]).forEach(path => { knownPaths[path] = true; }); } @@ -201,7 +197,7 @@ export class Sorter { if (!knownPaths) { // Easy case: no use of arrays. - var soleKey = valuesByIndexAndPath.map(function (values) { + const soleKey = valuesByIndexAndPath.map(values => { if (!values.hasOwnProperty('')) throw Error("no value in sole key case?"); return values['']; @@ -210,8 +206,8 @@ export class Sorter { return; } - Object.keys(knownPaths).forEach(function (path) { - var key = valuesByIndexAndPath.map(function (values) { + Object.keys(knownPaths).forEach(path => { + const key = valuesByIndexAndPath.map(values => { if (values.hasOwnProperty('')) return values['']; if (!values.hasOwnProperty(path)) @@ -225,7 +221,7 @@ export class Sorter { // Returns a comparator that represents the sort specification (but not // including a possible geoquery distance tie-breaker). _getBaseComparator () { - var self = this; + const self = this; if (self._sortFunction) return self._sortFunction; @@ -233,14 +229,12 @@ export class Sorter { // If we're only sorting on geoquery distance and no specs, just say // everything is equal. if (!self._sortSpecParts.length) { - return function (doc1, doc2) { - return 0; - }; + return (doc1, doc2) => 0; } - return function (doc1, doc2) { - var key1 = self._getMinKeyFromDoc(doc1); - var key2 = self._getMinKeyFromDoc(doc2); + return (doc1, doc2) => { + const key1 = self._getMinKeyFromDoc(doc1); + const key2 = self._getMinKeyFromDoc(doc2); return self._compareKeys(key1, key2); }; } @@ -256,10 +250,10 @@ export class Sorter { // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key. _getMinKeyFromDoc (doc) { - var self = this; - var minKey = null; + const self = this; + let minKey = null; - self._generateKeysFromDoc(doc, function (key) { + self._generateKeysFromDoc(doc, key => { if (!self._keyCompatibleWithSelector(key)) return; @@ -280,22 +274,22 @@ export class Sorter { } _getPaths () { - var self = this; - return self._sortSpecParts.map(function (part) { return part.path; }); + const self = this; + return self._sortSpecParts.map(part => part.path); } _keyCompatibleWithSelector (key) { - var self = this; + const self = this; return !self._keyFilter || self._keyFilter(key); } // Given an index 'i', returns a comparator that compares two key arrays based // on field 'i'. _keyFieldComparator (i) { - var self = this; - var invert = !self._sortSpecParts[i].ascending; - return function (key1, key2) { - var compare = LocalCollection._f._cmp(key1[i], key2[i]); + const self = this; + const invert = !self._sortSpecParts[i].ascending; + return (key1, key2) => { + let compare = LocalCollection._f._cmp(key1[i], key2[i]); if (invert) compare = -compare; return compare; @@ -322,7 +316,7 @@ export class Sorter { // subtle and undocumented; we've gotten as close as we can figure out based // on our understanding of Mongo's behavior. _useWithMatcher (matcher) { - var self = this; + const self = this; if (self._keyFilter) throw Error("called _useWithMatcher twice?"); @@ -333,23 +327,23 @@ export class Sorter { if (!self._sortSpecParts.length) return; - var selector = matcher._selector; + const selector = matcher._selector; // If the user just passed a literal function to find(), then we can't get a // key filter from it. if (selector instanceof Function) return; - var constraintsByPath = {}; - self._sortSpecParts.forEach(function (spec, i) { + const constraintsByPath = {}; + self._sortSpecParts.forEach((spec, i) => { constraintsByPath[spec.path] = []; }); - Object.keys(selector).forEach(function (key) { - var subSelector = selector[key]; + Object.keys(selector).forEach(key => { + const subSelector = selector[key]; // XXX support $and and $or - var constraints = constraintsByPath[key]; + const constraints = constraintsByPath[key]; if (!constraints) return; @@ -371,8 +365,8 @@ export class Sorter { } if (isOperatorObject(subSelector)) { - Object.keys(subSelector).forEach(function (operator) { - var operand = subSelector[operator]; + Object.keys(subSelector).forEach(operator => { + const operand = subSelector[operator]; if (['$lt', '$lte', '$gt', '$gte'].includes(operator)) { // XXX this depends on us knowing that these operators don't use any // of the arguments to compileElementSelector other than operand. @@ -403,13 +397,7 @@ export class Sorter { if (!constraintsByPath[self._sortSpecParts[0].path].length) return; - self._keyFilter = function (key) { - return self._sortSpecParts.every(function (specPart, index) { - return constraintsByPath[specPart.path].every(function (f) { - return f(key[index]); - }); - }); - }; + self._keyFilter = key => self._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(f => f(key[index]))); } } @@ -418,9 +406,9 @@ export class Sorter { // comparator which uses each comparator in order and returns the first // non-zero value. function composeComparators (comparatorArray) { - return function (a, b) { - for (var i = 0; i < comparatorArray.length; ++i) { - var compare = comparatorArray[i](a, b); + return (a, b) => { + for (let i = 0; i < comparatorArray.length; ++i) { + const compare = comparatorArray[i](a, b); if (compare !== 0) return compare; } From e1ef1e298401bb0927b9da2aca039c2a7fead99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 00:18:20 +0200 Subject: [PATCH 10/28] Got rid of self (sic!). --- packages/minimongo/cursor.js | 163 ++++++++---------- packages/minimongo/local_collection.js | 218 +++++++++++-------------- packages/minimongo/main_server.js | 44 ++--- packages/minimongo/sorter.js | 90 +++++----- 4 files changed, 225 insertions(+), 290 deletions(-) diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index 364ad26adb..05dfb1aa6e 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -4,39 +4,36 @@ import {LocalCollection} from './local_collection.js'; // a defined order, limit, and offset. creating a Cursor with LocalCollection.find(), export class Cursor { // don't call this ctor directly. use LocalCollection.find(). - constructor (collection, selector, options) { - const self = this; - if (!options) options = {}; - - self.collection = collection; - self.sorter = null; - self.matcher = new Minimongo.Matcher(selector); + constructor (collection, selector, options = {}) { +this.collection = collection; + this.sorter = null; + this.matcher = new Minimongo.Matcher(selector); if (LocalCollection._selectorIsId(selector)) { // stash for fast path - self._selectorId = selector; + this._selectorId = selector; } else if (LocalCollection._selectorIsIdPerhapsAsObject(selector)) { // also do the fast path for { _id: idString } - self._selectorId = selector._id; + this._selectorId = selector._id; } else { - self._selectorId = undefined; - if (self.matcher.hasGeoQuery() || options.sort) { - self.sorter = new Minimongo.Sorter(options.sort || [], - { matcher: self.matcher }); + this._selectorId = undefined; + if (this.matcher.hasGeoQuery() || options.sort) { + this.sorter = new Minimongo.Sorter(options.sort || [], + { matcher: this.matcher }); } } - self.skip = options.skip; - self.limit = options.limit; - self.fields = options.fields; + this.skip = options.skip; + this.limit = options.limit; + this.fields = options.fields; - self._projectionFn = LocalCollection._compileProjection(self.fields || {}); + this._projectionFn = LocalCollection._compileProjection(this.fields || {}); - self._transform = LocalCollection.wrapTransform(options.transform); + this._transform = LocalCollection.wrapTransform(options.transform); // by default, queries register w/ Tracker when it is available. if (typeof Tracker !== "undefined") - self.reactive = (options.reactive === undefined) ? true : options.reactive; + this.reactive = (options.reactive === undefined) ? true : options.reactive; } /** @@ -48,13 +45,11 @@ export class Cursor { * @returns {Number} */ count () { - const self = this; - - if (self.reactive) - self._depend({added: true, removed: true}, + if (this.reactive) + this._depend({added: true, removed: true}, true /* allow the observe to be unordered */); - return self._getRawObjects({ordered: true}).length; + return this._getRawObjects({ordered: true}).length; } /** @@ -66,9 +61,8 @@ export class Cursor { * @returns {Object[]} */ fetch () { - const self = this; const res = []; - self.forEach(doc => { + this.forEach(doc => { res.push(doc); }); return res; @@ -89,12 +83,10 @@ export class Cursor { * @param {Any} [thisArg] An object which will be the value of `this` inside `callback`. */ forEach (callback, thisArg) { - const self = this; + const objects = this._getRawObjects({ordered: true}); - const objects = self._getRawObjects({ordered: true}); - - if (self.reactive) { - self._depend({ + if (this.reactive) { + this._depend({ addedBefore: true, removed: true, changed: true, @@ -103,11 +95,11 @@ export class Cursor { objects.forEach((elt, i) => { // This doubles as a clone operation. - elt = self._projectionFn(elt); + elt = this._projectionFn(elt); - if (self._transform) - elt = self._transform(elt); - callback.call(thisArg, elt, i, self); + if (this._transform) + elt = this._transform(elt); + callback.call(thisArg, elt, i, this); }); } @@ -125,10 +117,9 @@ export class Cursor { * @param {Any} [thisArg] An object which will be the value of `this` inside `callback`. */ map (callback, thisArg) { - const self = this; const res = []; - self.forEach((doc, index) => { - res.push(callback.call(thisArg, doc, index, self)); + this.forEach((doc, index) => { + res.push(callback.call(thisArg, doc, index, this)); }); return res; } @@ -162,8 +153,7 @@ export class Cursor { * @param {Object} callbacks Functions to call to deliver the result set as it changes */ observe (options) { - const self = this; - return LocalCollection._observeFromObserveChanges(self, options); + return LocalCollection._observeFromObserveChanges(this, options); } /** @@ -174,42 +164,40 @@ export class Cursor { * @param {Object} callbacks Functions to call to deliver the result set as it changes */ observeChanges (options) { - const self = this; - const ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); // there are several places that assume you aren't combining skip/limit with // unordered observe. eg, update's EJSON.clone, and the "there are several" // comment in _modifyAndNotify // XXX allow skip/limit with unordered observe - if (!options._allow_unordered && !ordered && (self.skip || self.limit)) + if (!options._allow_unordered && !ordered && (this.skip || this.limit)) throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); - if (self.fields && (self.fields._id === 0 || self.fields._id === false)) + if (this.fields && (this.fields._id === 0 || this.fields._id === false)) throw Error("You may not observe a cursor with {fields: {_id: 0}}"); const query = { dirty: false, - matcher: self.matcher, // not fast pathed - sorter: ordered && self.sorter, + matcher: this.matcher, // not fast pathed + sorter: ordered && this.sorter, distances: ( - self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), + this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), resultsSnapshot: null, ordered, - cursor: self, - projectionFn: self._projectionFn + cursor: this, + projectionFn: this._projectionFn }; let qid; // Non-reactive queries call added[Before] and then never call anything // else. - if (self.reactive) { - qid = self.collection.next_qid++; - self.collection.queries[qid] = query; + if (this.reactive) { + qid = this.collection.next_qid++; + this.collection.queries[qid] = query; } - query.results = self._getRawObjects({ + query.results = this._getRawObjects({ ordered, distances: query.distances}); - if (self.collection.paused) + if (this.collection.paused) query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); // wrap callbacks we were passed. callbacks only fire when not paused and @@ -222,15 +210,15 @@ export class Cursor { const wrapCallback = f => { if (!f) return () => {}; + const self = this; return function (/*args*/) { - const context = this; const args = arguments; if (self.collection.paused) return; self.collection._observeQueue.queueTask(() => { - f.apply(context, args); + f.apply(this, args); }); }; }; @@ -242,7 +230,7 @@ export class Cursor { query.movedBefore = wrapCallback(options.movedBefore); } - if (!options._suppress_initial && !self.collection.paused) { + if (!options._suppress_initial && !this.collection.paused) { const results = query.results._map || query.results; Object.keys(results).forEach(key => { const doc = results[key]; @@ -250,21 +238,21 @@ export class Cursor { delete fields._id; if (ordered) - query.addedBefore(doc._id, self._projectionFn(fields), null); - query.added(doc._id, self._projectionFn(fields)); + query.addedBefore(doc._id, this._projectionFn(fields), null); + query.added(doc._id, this._projectionFn(fields)); }); } const handle = new LocalCollection.ObserveHandle; Object.assign(handle, { - collection: self.collection, - stop() { - if (self.reactive) - delete self.collection.queries[qid]; + collection: this.collection, + stop: () => { + if (this.reactive) + delete this.collection.queries[qid]; } }); - if (self.reactive && Tracker.active) { + if (this.reactive && Tracker.active) { // XXX in many cases, the same observe will be recreated when // the current autorun is rerun. we could save work by // letting it linger across rerun and potentially get @@ -276,7 +264,7 @@ export class Cursor { } // run the observe callbacks resulting from the initial contents // before we leave the observe. - self.collection._observeQueue.drain(); + this.collection._observeQueue.drain(); return handle; } @@ -290,8 +278,6 @@ export class Cursor { // XXX Maybe we need a version of observe that just calls a callback if // anything changed. _depend (changers, _allow_unordered) { - const self = this; - if (Tracker.active) { const v = new Tracker.Dependency; v.depend(); @@ -307,13 +293,12 @@ export class Cursor { }); // observeChanges will stop() when this computation is invalidated - self.observeChanges(options); + this.observeChanges(options); } } _getCollectionName () { - const self = this; - return self.collection.name; + return this.collection.name; } // Returns a collection of matching objects, but doesn't deep copy them. @@ -331,28 +316,25 @@ export class Cursor { // argument, this function will clear it and use it for this purpose (otherwise // it will just create its own _IdMap). The observeChanges implementation uses // this to remember the distances after this function returns. - _getRawObjects (options) { - const self = this; - options = options || {}; - + _getRawObjects (options = {}) { // XXX use OrderedDict instead of array, and make IdMap and OrderedDict // compatible const results = options.ordered ? [] : new LocalCollection._IdMap; // fast path for single ID value - if (self._selectorId !== undefined) { + if (this._selectorId !== undefined) { // If you have non-zero skip and ask for a single id, you get // nothing. This is so it matches the behavior of the '{_id: foo}' // path. - if (self.skip) + if (this.skip) return results; - const selectedDoc = self.collection._docs.get(self._selectorId); + const selectedDoc = this.collection._docs.get(this._selectorId); if (selectedDoc) { if (options.ordered) results.push(selectedDoc); else - results.set(self._selectorId, selectedDoc); + results.set(this._selectorId, selectedDoc); } return results; } @@ -363,7 +345,7 @@ export class Cursor { // live results set) object. in other cases, distances is only used inside // this function. let distances; - if (self.matcher.hasGeoQuery() && options.ordered) { + if (this.matcher.hasGeoQuery() && options.ordered) { if (options.distances) { distances = options.distances; distances.clear(); @@ -372,8 +354,8 @@ export class Cursor { } } - self.collection._docs.forEach((doc, id) => { - const matchResult = self.matcher.documentMatches(doc); + this.collection._docs.forEach((doc, id) => { + const matchResult = this.matcher.documentMatches(doc); if (matchResult.result) { if (options.ordered) { results.push(doc); @@ -385,8 +367,8 @@ export class Cursor { } // Fast path for limited unsorted queries. // XXX 'length' check here seems wrong for ordered - if (self.limit && !self.skip && !self.sorter && - results.length === self.limit) + if (this.limit && !this.skip && !this.sorter && + results.length === this.limit) return false; // break return true; // continue }); @@ -394,27 +376,26 @@ export class Cursor { if (!options.ordered) return results; - if (self.sorter) { - const comparator = self.sorter.getComparator({distances}); + if (this.sorter) { + const comparator = this.sorter.getComparator({distances}); results.sort(comparator); } - const idx_start = self.skip || 0; - const idx_end = self.limit ? (self.limit + idx_start) : results.length; + const idx_start = this.skip || 0; + const idx_end = this.limit ? (this.limit + idx_start) : results.length; return results.slice(idx_start, idx_end); } _publishCursor (sub) { - const self = this; - if (! self.collection.name) + if (! this.collection.name) throw new Error("Can't publish a cursor from a collection without a name."); - const collection = self.collection.name; + const collection = this.collection.name; // XXX minimongo should not depend on mongo-livedata! if (! Package.mongo) { throw new Error("Can't publish from Minimongo without the `mongo` package."); } - return Package.mongo.Mongo.Collection._publishCursor(self, sub, collection); + return Package.mongo.Mongo.Collection._publishCursor(this, sub, collection); } } diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 2d6fac61e0..baa0ee8be7 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -86,7 +86,6 @@ export class LocalCollection { // XXX possibly enforce that 'undefined' does not appear (we assume // this in our handling of null and $exists) insert (doc, callback) { - const self = this; doc = EJSON.clone(doc); assertHasValidFieldNames(doc); @@ -99,16 +98,16 @@ export class LocalCollection { } const id = doc._id; - if (self._docs.has(id)) + if (this._docs.has(id)) throw MinimongoError(`Duplicate _id '${id}'`); - self._saveOriginal(id, undefined); - self._docs.set(id, doc); + this._saveOriginal(id, undefined); + this._docs.set(id, doc); const queriesToRecompute = []; // trigger live queries that match - for (let qid in self.queries) { - const query = self.queries[qid]; + for (let qid in this.queries) { + const query = this.queries[qid]; if (query.dirty) continue; const matchResult = query.matcher.documentMatches(doc); if (matchResult.result) { @@ -122,10 +121,10 @@ export class LocalCollection { } queriesToRecompute.forEach(qid => { - if (self.queries[qid]) - self._recomputeResults(self.queries[qid]); + if (this.queries[qid]) + this._recomputeResults(this.queries[qid]); }); - self._observeQueue.drain(); + this._observeQueue.drain(); // Defer because the caller likely doesn't expect the callback to be run // immediately. @@ -155,16 +154,14 @@ export class LocalCollection { } remove (selector, callback) { - const self = this; - // Easy special case: if we're not calling observeChanges callbacks and we're // not saving originals and we got asked to remove everything, then just empty // everything directly. - if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) { - const result = self._docs.size(); - self._docs.clear(); - Object.keys(self.queries).forEach(qid => { - const query = self.queries[qid]; + if (this.paused && !this._savedOriginals && EJSON.equals(selector, {})) { + const result = this._docs.size(); + this._docs.clear(); + Object.keys(this.queries).forEach(qid => { + const query = this.queries[qid]; if (query.ordered) { query.results = []; } else { @@ -181,7 +178,7 @@ export class LocalCollection { const matcher = new Minimongo.Matcher(selector); const remove = []; - self._eachPossiblyMatchingDoc(selector, (doc, id) => { + this._eachPossiblyMatchingDoc(selector, (doc, id) => { if (matcher.documentMatches(doc).result) remove.push(id); }); @@ -190,9 +187,9 @@ export class LocalCollection { const queryRemove = []; for (let i = 0; i < remove.length; i++) { const removeId = remove[i]; - const removeDoc = self._docs.get(removeId); - Object.keys(self.queries).forEach(qid => { - const query = self.queries[qid]; + const removeDoc = this._docs.get(removeId); + Object.keys(this.queries).forEach(qid => { + const query = this.queries[qid]; if (query.dirty) return; if (query.matcher.documentMatches(removeDoc).result) { @@ -202,24 +199,24 @@ export class LocalCollection { queryRemove.push({qid, doc: removeDoc}); } }); - self._saveOriginal(removeId, removeDoc); - self._docs.remove(removeId); + this._saveOriginal(removeId, removeDoc); + this._docs.remove(removeId); } // run live query callbacks _after_ we've removed the documents. queryRemove.forEach(remove => { - const query = self.queries[remove.qid]; + const query = this.queries[remove.qid]; if (query) { query.distances && query.distances.remove(remove.doc._id); LocalCollection._removeFromResults(query, remove.doc); } }); queriesToRecompute.forEach(qid => { - const query = self.queries[qid]; + const query = this.queries[qid]; if (query) - self._recomputeResults(query); + this._recomputeResults(query); }); - self._observeQueue.drain(); + this._observeQueue.drain(); const result = remove.length; if (callback) Meteor.defer(() => { @@ -233,7 +230,6 @@ export class LocalCollection { // database. Note that this is not just replaying all the changes that // happened during the pause, it is a smarter 'coalesced' diff. resumeObservers () { - const self = this; // No-op if not paused. if (!this.paused) return; @@ -243,11 +239,11 @@ export class LocalCollection { this.paused = false; for (let qid in this.queries) { - const query = self.queries[qid]; + const query = this.queries[qid]; if (query.dirty) { query.dirty = false; // re-compute results will perform `LocalCollection._diffQueryChanges` automatically. - self._recomputeResults(query, query.resultsSnapshot); + this._recomputeResults(query, query.resultsSnapshot); } else { // Diff the current results against the snapshot and send to observers. // pass the query object for its observer callbacks. @@ -257,16 +253,15 @@ export class LocalCollection { } query.resultsSnapshot = null; } - self._observeQueue.drain(); + this._observeQueue.drain(); } retrieveOriginals () { - const self = this; - if (!self._savedOriginals) + if (!this._savedOriginals) throw new Error("Called retrieveOriginals without saveOriginals"); - const originals = self._savedOriginals; - self._savedOriginals = null; + const originals = this._savedOriginals; + this._savedOriginals = null; return originals; } @@ -278,16 +273,14 @@ export class LocalCollection { // is the value.) You must alternate between calls to saveOriginals() and // retrieveOriginals(). saveOriginals () { - const self = this; - if (self._savedOriginals) + if (this._savedOriginals) throw new Error("Called saveOriginals twice without retrieveOriginals"); - self._savedOriginals = new LocalCollection._IdMap; + this._savedOriginals = new LocalCollection._IdMap; } // XXX atomicity: if multi is true, and one modification fails, do // we rollback the whole operation, or what? update (selector, mod, options, callback) { - const self = this; if (! callback && options instanceof Function) { callback = options; options = null; @@ -306,9 +299,9 @@ export class LocalCollection { const docMap = new LocalCollection._IdMap; const idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector); - Object.keys(self.queries).forEach(qid => { - const query = self.queries[qid]; - if ((query.cursor.skip || query.cursor.limit) && ! self.paused) { + Object.keys(this.queries).forEach(qid => { + const query = this.queries[qid]; + if ((query.cursor.skip || query.cursor.limit) && ! this.paused) { // Catch the case of a reactive `count()` on a cursor with skip // or limit, which registers an unordered observe. This is a // pretty rare case, so we just clone the entire result set with @@ -351,12 +344,12 @@ export class LocalCollection { let updateCount = 0; - self._eachPossiblyMatchingDoc(selector, (doc, id) => { + this._eachPossiblyMatchingDoc(selector, (doc, id) => { const queryResult = matcher.documentMatches(doc); if (queryResult.result) { // XXX Should we save the original even if mod ends up being a no-op? - self._saveOriginal(id, doc); - self._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices); + this._saveOriginal(id, doc); + this._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices); ++updateCount; if (!options.multi) return false; // break @@ -365,11 +358,11 @@ export class LocalCollection { }); Object.keys(recomputeQids).forEach(qid => { - const query = self.queries[qid]; + const query = this.queries[qid]; if (query) - self._recomputeResults(query, qidToOriginalResults[qid]); + this._recomputeResults(query, qidToOriginalResults[qid]); }); - self._observeQueue.drain(); + this._observeQueue.drain(); // If we are doing an upsert, and we didn't modify any documents yet, then // it's time to do an insert. Figure out what document we are inserting, and @@ -396,7 +389,7 @@ export class LocalCollection { if (! newDoc._id && options.insertedId) newDoc._id = options.insertedId; - insertedId = self.insert(newDoc); + insertedId = this.insert(newDoc); updateCount = 1; } @@ -425,12 +418,11 @@ export class LocalCollection { // equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject: // true }). upsert (selector, mod, options, callback) { - const self = this; if (! callback && typeof options === "function") { callback = options; options = {}; } - return self.update(selector, mod, Object.assign({}, options, { + return this.update(selector, mod, Object.assign({}, options, { upsert: true, _returnObject: true }), callback); @@ -441,12 +433,11 @@ export class LocalCollection { // specific _id's, it only looks at those. doc is *not* cloned: it is the // same object that is in _docs. _eachPossiblyMatchingDoc (selector, f) { - const self = this; const specificIds = LocalCollection._idsMatchedBySelector(selector); if (specificIds) { for (let i = 0; i < specificIds.length; ++i) { const id = specificIds[i]; - const doc = self._docs.get(id); + const doc = this._docs.get(id); if (doc) { const breakIfFalse = f(doc, id); if (breakIfFalse === false) @@ -454,16 +445,14 @@ export class LocalCollection { } } } else { - self._docs.forEach(f); + this._docs.forEach(f); } } _modifyAndNotify (doc, mod, recomputeQids, arrayIndices) { - const self = this; - const matched_before = {}; - for (let qid in self.queries) { - const query = self.queries[qid]; + for (let qid in this.queries) { + const query = this.queries[qid]; if (query.dirty) continue; if (query.ordered) { @@ -479,8 +468,8 @@ export class LocalCollection { LocalCollection._modify(doc, mod, {arrayIndices}); - for (let qid in self.queries) { - const query = self.queries[qid]; + for (let qid in this.queries) { + const query = this.queries[qid]; if (query.dirty) continue; const before = matched_before[qid]; @@ -520,8 +509,7 @@ export class LocalCollection { // // oldResults is guaranteed to be ignored if the query is not paused. _recomputeResults (query, oldResults) { - const self = this; - if (self.paused) { + if (this.paused) { // There's no reason to recompute the results now as we're still paused. // By flagging the query as "dirty", the recompute will be performed // when resumeObservers is called. @@ -529,14 +517,14 @@ export class LocalCollection { return; } - if (! self.paused && ! oldResults) + if (! this.paused && ! oldResults) oldResults = query.results; if (query.distances) query.distances.clear(); query.results = query.cursor._getRawObjects({ ordered: query.ordered, distances: query.distances}); - if (! self.paused) { + if (! this.paused) { LocalCollection._diffQueryChanges( query.ordered, oldResults, query.results, query, { projectionFn: query.projectionFn }); @@ -544,16 +532,15 @@ export class LocalCollection { } _saveOriginal (id, doc) { - const self = this; // Are we even trying to save originals? - if (!self._savedOriginals) + if (!this._savedOriginals) return; // Have we previously mutated the original (and so 'doc' is not actually // original)? (Note the 'has' check rather than truth: we store undefined // here for inserted docs!) - if (self._savedOriginals.has(id)) + if (this._savedOriginals.has(id)) return; - self._savedOriginals.set(id, EJSON.clone(doc)); + this._savedOriginals.set(id, EJSON.clone(doc)); } } @@ -564,76 +551,73 @@ LocalCollection.ObserveHandle = ObserveHandle; // XXX maybe move these into another ObserveHelpers package or something // _CachingChangeObserver is an object which receives observeChanges callbacks -// and keeps a cache of the current cursor state up to date in self.docs. Users +// and keeps a cache of the current cursor state up to date in this.docs. Users // of this class should read the docs field but not modify it. You should pass // the "applyChange" field as the callbacks to the underlying observeChanges // call. Optionally, you can specify your own observeChanges callbacks which are // invoked immediately before the docs field is updated; this object is made // available as `this` to those callbacks. LocalCollection._CachingChangeObserver = class _CachingChangeObserver { - constructor (options) { - const self = this; - options = options || {}; - + constructor (options = {}) { const orderedFromCallbacks = options.callbacks && LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); if (options.hasOwnProperty('ordered')) { - self.ordered = options.ordered; + this.ordered = options.ordered; if (options.callbacks && options.ordered !== orderedFromCallbacks) throw Error("ordered option doesn't match callbacks"); } else if (options.callbacks) { - self.ordered = orderedFromCallbacks; + this.ordered = orderedFromCallbacks; } else { throw Error("must provide ordered or callbacks"); } const callbacks = options.callbacks || {}; - if (self.ordered) { - self.docs = new OrderedDict(MongoID.idStringify); - self.applyChange = { - addedBefore(id, fields, before) { + if (this.ordered) { + this.docs = new OrderedDict(MongoID.idStringify); + this.applyChange = { + addedBefore: (id, fields, before) => { const doc = EJSON.clone(fields); doc._id = id; callbacks.addedBefore && callbacks.addedBefore.call( - self, id, fields, before); + this, id, fields, before); // This line triggers if we provide added with movedBefore. - callbacks.added && callbacks.added.call(self, id, fields); + callbacks.added && callbacks.added.call(this, id, fields); // XXX could `before` be a falsy ID? Technically // idStringify seems to allow for them -- though // OrderedDict won't call stringify on a falsy arg. - self.docs.putBefore(id, doc, before || null); + this.docs.putBefore(id, doc, before || null); }, - movedBefore(id, before) { - const doc = self.docs.get(id); - callbacks.movedBefore && callbacks.movedBefore.call(self, id, before); - self.docs.moveBefore(id, before || null); + movedBefore: (id, before) => { + const doc = this.docs.get(id); + callbacks.movedBefore && callbacks.movedBefore.call(this, id, before); + this.docs.moveBefore(id, before || null); } }; } else { - self.docs = new LocalCollection._IdMap; - self.applyChange = { - added(id, fields) { + this.docs = new LocalCollection._IdMap; + this.applyChange = { + added: (id, fields) => { const doc = EJSON.clone(fields); - callbacks.added && callbacks.added.call(self, id, fields); + callbacks.added && callbacks.added.call(this, id, fields); doc._id = id; - self.docs.set(id, doc); + this.docs.set(id, doc); } }; } // The methods in _IdMap and OrderedDict used by these callbacks are // identical. - self.applyChange.changed = (id, fields) => { - const doc = self.docs.get(id); + this.applyChange.changed = (id, fields) => { + const doc = this.docs.get(id); if (!doc) throw new Error(`Unknown id for changed: ${id}`); callbacks.changed && callbacks.changed.call( - self, id, EJSON.clone(fields)); + this, id, EJSON.clone(fields)); DiffSequence.applyChanges(doc, fields); }; - self.applyChange.removed = id => { - callbacks.removed && callbacks.removed.call(self, id); - self.docs.remove(id); + this.applyChange.removed = id => { + callbacks.removed && callbacks.removed.call(this, id); + this.docs.remove(id); }; } }; @@ -987,60 +971,56 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { // relative order, transforms, and applyChanges -- without the speed hit. const indices = !observeCallbacks._no_indices; observeChangesCallbacks = { - addedBefore(id, fields, before) { - const self = this; + addedBefore (id, fields, before) { if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) return; const doc = transform(Object.assign(fields, {_id: id})); if (observeCallbacks.addedAt) { const index = indices - ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; + ? (before ? this.docs.indexOf(before) : this.docs.size()) : -1; observeCallbacks.addedAt(doc, index, before); } else { observeCallbacks.added(doc); } }, - changed(id, fields) { - const self = this; + changed (id, fields) { if (!(observeCallbacks.changedAt || observeCallbacks.changed)) return; - let doc = EJSON.clone(self.docs.get(id)); + let doc = EJSON.clone(this.docs.get(id)); if (!doc) throw new Error(`Unknown id for changed: ${id}`); const oldDoc = transform(EJSON.clone(doc)); DiffSequence.applyChanges(doc, fields); doc = transform(doc); if (observeCallbacks.changedAt) { - const index = indices ? self.docs.indexOf(id) : -1; + const index = indices ? this.docs.indexOf(id) : -1; observeCallbacks.changedAt(doc, oldDoc, index); } else { observeCallbacks.changed(doc, oldDoc); } }, - movedBefore(id, before) { - const self = this; + movedBefore (id, before) { if (!observeCallbacks.movedTo) return; - const from = indices ? self.docs.indexOf(id) : -1; + const from = indices ? this.docs.indexOf(id) : -1; let to = indices - ? (before ? self.docs.indexOf(before) : self.docs.size()) : -1; + ? (before ? this.docs.indexOf(before) : this.docs.size()) : -1; // When not moving backwards, adjust for the fact that removing the // document slides everything back one slot. if (to > from) --to; - observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))), + observeCallbacks.movedTo(transform(EJSON.clone(this.docs.get(id))), from, to, before || null); }, - removed(id) { - const self = this; + removed (id) { if (!(observeCallbacks.removedAt || observeCallbacks.removed)) return; // technically maybe there should be an EJSON.clone here, but it's about - // to be removed from self.docs! - const doc = transform(self.docs.get(id)); + // to be removed from this.docs! + const doc = transform(this.docs.get(id)); if (observeCallbacks.removedAt) { - const index = indices ? self.docs.indexOf(id) : -1; + const index = indices ? this.docs.indexOf(id) : -1; observeCallbacks.removedAt(doc, index); } else { observeCallbacks.removed(doc); @@ -1049,26 +1029,24 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { }; } else { observeChangesCallbacks = { - added(id, fields) { + added (id, fields) { if (!suppressed && observeCallbacks.added) { const doc = Object.assign(fields, {_id: id}); observeCallbacks.added(transform(doc)); } }, - changed(id, fields) { - const self = this; + changed (id, fields) { if (observeCallbacks.changed) { - const oldDoc = self.docs.get(id); + const oldDoc = this.docs.get(id); const doc = EJSON.clone(oldDoc); DiffSequence.applyChanges(doc, fields); observeCallbacks.changed(transform(doc), transform(EJSON.clone(oldDoc))); } }, - removed(id) { - const self = this; + removed (id) { if (observeCallbacks.removed) { - observeCallbacks.removed(transform(self.docs.get(id))); + observeCallbacks.removed(transform(this.docs.get(id))); } } }; diff --git a/packages/minimongo/main_server.js b/packages/minimongo/main_server.js index a24849f8a8..7b3fb9992c 100644 --- a/packages/minimongo/main_server.js +++ b/packages/minimongo/main_server.js @@ -7,7 +7,6 @@ import { } from './common.js'; Minimongo._pathsElidingNumericKeys = function (paths) { - const self = this; return paths.map(path => path.split('.').filter(part => !isNumericKey(part)).join('.')); }; @@ -20,11 +19,10 @@ Minimongo._pathsElidingNumericKeys = function (paths) { // - $unset // - 'abc.d': 1 Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { - const self = this; // safe check for $set/$unset being objects modifier = Object.assign({ $set: {}, $unset: {} }, modifier); const modifiedPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); - const meaningfulPaths = self._getPaths(); + const meaningfulPaths = this._getPaths(); return modifiedPaths.some(path => { const mod = path.split('.'); @@ -66,17 +64,16 @@ Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { // stay 'false'. // Currently doesn't support $-operators and numeric indices precisely. Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { - const self = this; if (!this.affectedByModifier(modifier)) return false; modifier = Object.assign({$set:{}, $unset:{}}, modifier); const modifierPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); - if (!self.isSimple()) + if (!this.isSimple()) return true; - if (self._getPaths().some(pathHasNumericKeys) || + if (this._getPaths().some(pathHasNumericKeys) || modifierPaths.some(pathHasNumericKeys)) return true; @@ -85,8 +82,8 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // NOTE: it is correct since we allow only scalars in $-operators // Example: for selector {'a.b': {$gt: 5}} the modifier {'a.b.c':7} would // definitely set the result to false as 'a.b' appears to be an object. - const expectedScalarIsObject = Object.keys(self._selector).some(path => { - const sel = self._selector[path]; + const expectedScalarIsObject = Object.keys(this._selector).some(path => { + const sel = this._selector[path]; if (! isOperatorObject(sel)) return false; return modifierPaths.some(modifierPath => startsWith(modifierPath, `${path}.`)); @@ -98,7 +95,7 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // See if we can apply the modifier on the ideally matching object. If it // still matches the selector, then the modifier could have turned the real // object in the database into something matching. - const matchingDocument = EJSON.clone(self.matchingDocument()); + const matchingDocument = EJSON.clone(this.matchingDocument()); // The selector is too complex, anything can happen. if (matchingDocument === null) @@ -122,15 +119,14 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { throw e; } - return self.documentMatches(matchingDocument).result; + return this.documentMatches(matchingDocument).result; }; // Knows how to combine a mongo selector and a fields projection to a new fields // projection taking into account active fields from the passed selector. // @returns Object - projection object (same as fields option of mongo cursor) Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { - const self = this; - const selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); + const selectorPaths = Minimongo._pathsElidingNumericKeys(this._getPaths()); // Special case for $where operator in the selector - projection should depend // on all fields of the document. getSelectorPaths returns a list of paths @@ -147,18 +143,16 @@ Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { // { 'a.b': { ans: 42 }, 'foo.bar': null, 'foo.baz': "something" } // => { a: { b: { ans: 42 } }, foo: { bar: null, baz: "something" } } Minimongo.Matcher.prototype.matchingDocument = function () { - const self = this; - // check if it was computed before - if (self._matchingDocument !== undefined) - return self._matchingDocument; + if (this._matchingDocument !== undefined) + return this._matchingDocument; // If the analysis of this selector is too hard for our implementation // fallback to "YES" let fallback = false; - self._matchingDocument = pathsToTree(self._getPaths(), + this._matchingDocument = pathsToTree(this._getPaths(), path => { - const valueSelector = self._selector[path]; + const valueSelector = this._selector[path]; if (isOperatorObject(valueSelector)) { // if there is a strict equality, there is a good // chance we can use one of those as "matching" @@ -191,7 +185,7 @@ Minimongo.Matcher.prototype.matchingDocument = function () { return middle; } else if (onlyContainsKeys(valueSelector, ['$nin', '$ne'])) { - // Since self._isSimple makes sure $nin and $ne are not combined with + // Since this._isSimple makes sure $nin and $ne are not combined with // objects or arrays, we can confidently return an empty object as it // never matches any scalar. return {}; @@ -199,26 +193,24 @@ Minimongo.Matcher.prototype.matchingDocument = function () { fallback = true; } } - return self._selector[path]; + return this._selector[path]; }, x => x); if (fallback) - self._matchingDocument = null; + this._matchingDocument = null; - return self._matchingDocument; + return this._matchingDocument; }; // Minimongo.Sorter gets a similar method, which delegates to a Matcher it made // for this exact purpose. Minimongo.Sorter.prototype.affectedByModifier = function (modifier) { - const self = this; - return self._selectorForAffectedByModifier.affectedByModifier(modifier); + return this._selectorForAffectedByModifier.affectedByModifier(modifier); }; Minimongo.Sorter.prototype.combineIntoProjection = function (projection) { - const self = this; - const specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths()); + const specPaths = Minimongo._pathsElidingNumericKeys(this._getPaths()); return combineImportantPathsIntoProjection(specPaths, projection); }; diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index a332a5bfaa..5a4ba04061 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -21,19 +21,16 @@ import { // first, or 0 if neither object comes before the other. export class Sorter { - constructor (spec, options) { - const self = this; - options = options || {}; - - self._sortSpecParts = []; - self._sortFunction = null; + constructor (spec, options = {}) { + this._sortSpecParts = []; + this._sortFunction = null; const addSpecPart = (path, ascending) => { if (!path) throw Error("sort keys must be non-empty"); if (path.charAt(0) === '$') throw Error(`unsupported sort key: ${path}`); - self._sortSpecParts.push({ + this._sortSpecParts.push({ path, lookup: makeLookupFunction(path, {forSort: true}), ascending @@ -54,46 +51,44 @@ export class Sorter { addSpecPart(key, value >= 0); }); } else if (typeof spec === "function") { - self._sortFunction = spec; + this._sortFunction = spec; } else { throw Error(`Bad sort specification: ${JSON.stringify(spec)}`); } // If a function is specified for sorting, we skip the rest. - if (self._sortFunction) + if (this._sortFunction) return; // To implement affectedByModifier, we piggy-back on top of Matcher's // affectedByModifier code; we create a selector that is affected by the same // modifiers as this sort order. This is only implemented on the server. - if (self.affectedByModifier) { + if (this.affectedByModifier) { const selector = {}; - self._sortSpecParts.forEach(spec => { + this._sortSpecParts.forEach(spec => { selector[spec.path] = 1; }); - self._selectorForAffectedByModifier = new Minimongo.Matcher(selector); + this._selectorForAffectedByModifier = new Minimongo.Matcher(selector); } - self._keyComparator = composeComparators( - self._sortSpecParts.map((spec, i) => self._keyFieldComparator(i))); + this._keyComparator = composeComparators( + this._sortSpecParts.map((spec, i) => this._keyFieldComparator(i))); // If you specify a matcher for this Sorter, _keyFilter may be set to a // function which selects whether or not a given "sort key" (tuple of values // for the different sort spec fields) is compatible with the selector. - self._keyFilter = null; - options.matcher && self._useWithMatcher(options.matcher); + this._keyFilter = null; + options.matcher && this._useWithMatcher(options.matcher); } getComparator (options) { - const self = this; - // If sort is specified or have no distances, just use the comparator from // the source specification (which defaults to "everything is equal". // issue #3599 // https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation // sort effectively overrides $near - if (self._sortSpecParts.length || !options || !options.distances) { - return self._getBaseComparator(); + if (this._sortSpecParts.length || !options || !options.distances) { + return this._getBaseComparator(); } const distances = options.distances; @@ -112,21 +107,18 @@ export class Sorter { // parts. Returns negative, 0, or positive based on using the sort spec to // compare fields. _compareKeys (key1, key2) { - const self = this; - if (key1.length !== self._sortSpecParts.length || - key2.length !== self._sortSpecParts.length) { + if (key1.length !== this._sortSpecParts.length || + key2.length !== this._sortSpecParts.length) { throw Error("Key has wrong length"); } - return self._keyComparator(key1, key2); + return this._keyComparator(key1, key2); } // Iterates over each possible "key" from doc (ie, over each branch), calling // 'cb' with the key. _generateKeysFromDoc (doc, cb) { - const self = this; - - if (self._sortSpecParts.length === 0) + if (this._sortSpecParts.length === 0) throw new Error("can't generate keys without a spec"); // maps index -> ({'' -> value} or {path -> value}) @@ -136,7 +128,7 @@ export class Sorter { let knownPaths = null; - self._sortSpecParts.forEach((spec, whichField) => { + this._sortSpecParts.forEach((spec, whichField) => { // Expand any leaf arrays that we find, and ignore those arrays // themselves. (We never sort based on an array itself.) let branches = expandArraysInBranches(spec.lookup(doc), true); @@ -221,21 +213,19 @@ export class Sorter { // Returns a comparator that represents the sort specification (but not // including a possible geoquery distance tie-breaker). _getBaseComparator () { - const self = this; - - if (self._sortFunction) - return self._sortFunction; + if (this._sortFunction) + return this._sortFunction; // If we're only sorting on geoquery distance and no specs, just say // everything is equal. - if (!self._sortSpecParts.length) { + if (!this._sortSpecParts.length) { return (doc1, doc2) => 0; } return (doc1, doc2) => { - const key1 = self._getMinKeyFromDoc(doc1); - const key2 = self._getMinKeyFromDoc(doc2); - return self._compareKeys(key1, key2); + const key1 = this._getMinKeyFromDoc(doc1); + const key2 = this._getMinKeyFromDoc(doc2); + return this._compareKeys(key1, key2); }; } @@ -250,18 +240,17 @@ export class Sorter { // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key. _getMinKeyFromDoc (doc) { - const self = this; let minKey = null; - self._generateKeysFromDoc(doc, key => { - if (!self._keyCompatibleWithSelector(key)) + this._generateKeysFromDoc(doc, key => { + if (!this._keyCompatibleWithSelector(key)) return; if (minKey === null) { minKey = key; return; } - if (self._compareKeys(key, minKey) < 0) { + if (this._compareKeys(key, minKey) < 0) { minKey = key; } }); @@ -274,20 +263,17 @@ export class Sorter { } _getPaths () { - const self = this; - return self._sortSpecParts.map(part => part.path); + return this._sortSpecParts.map(part => part.path); } _keyCompatibleWithSelector (key) { - const self = this; - return !self._keyFilter || self._keyFilter(key); + return !this._keyFilter || this._keyFilter(key); } // Given an index 'i', returns a comparator that compares two key arrays based // on field 'i'. _keyFieldComparator (i) { - const self = this; - const invert = !self._sortSpecParts[i].ascending; + const invert = !this._sortSpecParts[i].ascending; return (key1, key2) => { let compare = LocalCollection._f._cmp(key1[i], key2[i]); if (invert) @@ -316,15 +302,13 @@ export class Sorter { // subtle and undocumented; we've gotten as close as we can figure out based // on our understanding of Mongo's behavior. _useWithMatcher (matcher) { - const self = this; - - if (self._keyFilter) + if (this._keyFilter) throw Error("called _useWithMatcher twice?"); // If we are only sorting by distance, then we're not going to bother to // build a key filter. // XXX figure out how geoqueries interact with this stuff - if (!self._sortSpecParts.length) + if (!this._sortSpecParts.length) return; const selector = matcher._selector; @@ -335,7 +319,7 @@ export class Sorter { return; const constraintsByPath = {}; - self._sortSpecParts.forEach((spec, i) => { + this._sortSpecParts.forEach((spec, i) => { constraintsByPath[spec.path] = []; }); @@ -394,10 +378,10 @@ export class Sorter { // others; we shouldn't create a key filter unless the first sort field is // restricted, though after that point we can restrict the other sort fields // or not as we wish. - if (!constraintsByPath[self._sortSpecParts[0].path].length) + if (!constraintsByPath[this._sortSpecParts[0].path].length) return; - self._keyFilter = key => self._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(f => f(key[index]))); + this._keyFilter = key => this._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(f => f(key[index]))); } } From 521ca42006edd151883a462ae043b5f6d27dec89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 00:38:21 +0200 Subject: [PATCH 11/28] ESLint fix magic. --- packages/minimongo/common.js | 327 ++- packages/minimongo/cursor.js | 99 +- packages/minimongo/local_collection.js | 614 +++--- packages/minimongo/main_server.js | 121 +- packages/minimongo/matcher.js | 142 +- packages/minimongo/minimongo_tests.js | 18 +- packages/minimongo/minimongo_tests_client.js | 2040 +++++++++--------- packages/minimongo/minimongo_tests_server.js | 532 +++-- packages/minimongo/package.js | 6 +- packages/minimongo/sorter.js | 109 +- 10 files changed, 1867 insertions(+), 2141 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index 6399f762b3..5806a91122 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -20,38 +20,31 @@ export const ELEMENT_OPERATORS = { $mod: { compileElementSelector(operand) { if (!(Array.isArray(operand) && operand.length === 2 - && typeof(operand[0]) === 'number' - && typeof(operand[1]) === 'number')) { - throw Error("argument to $mod must be an array of two numbers"); + && typeof operand[0] === 'number' + && typeof operand[1] === 'number')) { + throw Error('argument to $mod must be an array of two numbers'); } // XXX could require to be ints or round or something const divisor = operand[0]; const remainder = operand[1]; return value => typeof value === 'number' && value % divisor === remainder; - } + }, }, $in: { compileElementSelector(operand) { - if (!Array.isArray(operand)) - throw Error("$in needs an array"); + if (!Array.isArray(operand)) {throw Error('$in needs an array');} const elementMatchers = []; operand.forEach(option => { - if (option instanceof RegExp) - elementMatchers.push(regexpElementMatcher(option)); - else if (isOperatorObject(option)) - throw Error("cannot nest $ under $in"); - else - elementMatchers.push(equalityElementMatcher(option)); + if (option instanceof RegExp) {elementMatchers.push(regexpElementMatcher(option));} else if (isOperatorObject(option)) {throw Error('cannot nest $ under $in');} else {elementMatchers.push(equalityElementMatcher(option));} }); return value => { // Allow {a: {$in: [null]}} to match when 'a' does not exist. - if (value === undefined) - value = null; + if (value === undefined) {value = null;} return elementMatchers.some(e => e(value)); }; - } + }, }, $size: { // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we @@ -64,10 +57,10 @@ export const ELEMENT_OPERATORS = { // does. operand = 0; } else if (typeof operand !== 'number') { - throw Error("$size needs a number"); + throw Error('$size needs a number'); } return value => Array.isArray(value) && value.length === operand; - } + }, }, $type: { // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should @@ -76,11 +69,10 @@ export const ELEMENT_OPERATORS = { // should *not* include it itself. dontIncludeLeafArrays: true, compileElementSelector(operand) { - if (typeof operand !== 'number') - throw Error("$type needs a number"); + if (typeof operand !== 'number') {throw Error('$type needs a number');} return value => value !== undefined && LocalCollection._f._type(value) === operand; - } + }, }, $bitsAllSet: { compileElementSelector(operand) { @@ -89,7 +81,7 @@ export const ELEMENT_OPERATORS = { const bitmask = getValueBitmask(value, op.length); return bitmask && op.every((byte, idx) => (bitmask[idx] & byte) == byte); }; - } + }, }, $bitsAnySet: { compileElementSelector(operand) { @@ -98,7 +90,7 @@ export const ELEMENT_OPERATORS = { const bitmask = getValueBitmask(value, query.length); return bitmask && query.some((byte, idx) => (~bitmask[idx] & byte) !== byte); }; - } + }, }, $bitsAllClear: { compileElementSelector(operand) { @@ -107,7 +99,7 @@ export const ELEMENT_OPERATORS = { const bitmask = getValueBitmask(value, query.length); return bitmask && query.every((byte, idx) => !(bitmask[idx] & byte)); }; - } + }, }, $bitsAnyClear: { compileElementSelector(operand) { @@ -116,12 +108,11 @@ export const ELEMENT_OPERATORS = { const bitmask = getValueBitmask(value, query.length); return bitmask && query.some((byte, idx) => (bitmask[idx] & byte) !== byte); }; - } + }, }, $regex: { compileElementSelector(operand, valueSelector) { - if (!(typeof operand === 'string' || operand instanceof RegExp)) - throw Error("$regex has to be a string or RegExp"); + if (!(typeof operand === 'string' || operand instanceof RegExp)) {throw Error('$regex has to be a string or RegExp');} let regexp; if (valueSelector.$options !== undefined) { @@ -132,8 +123,7 @@ export const ELEMENT_OPERATORS = { // Be clear that we only support the JS-supported options, not extended // ones (eg, Mongo supports x and s). Ideally we would implement x and s // by transforming the regexp, but not today... - if (/[^gim]/.test(valueSelector.$options)) - throw new Error("Only the i, m, and g regexp options are supported"); + if (/[^gim]/.test(valueSelector.$options)) {throw new Error('Only the i, m, and g regexp options are supported');} const regexSource = operand instanceof RegExp ? operand.source : operand; regexp = new RegExp(regexSource, valueSelector.$options); @@ -143,18 +133,17 @@ export const ELEMENT_OPERATORS = { regexp = new RegExp(operand); } return regexpElementMatcher(regexp); - } + }, }, $elemMatch: { dontExpandLeafArrays: true, compileElementSelector(operand, valueSelector, matcher) { - if (!LocalCollection._isPlainObject(operand)) - throw Error("$elemMatch need an object"); + if (!LocalCollection._isPlainObject(operand)) {throw Error('$elemMatch need an object');} let subMatcher, isDocMatcher; if (isOperatorObject(Object.keys(operand) - .filter(key => !Object.keys(LOGICAL_OPERATORS).includes(key)) - .reduce((a, b) => Object.assign(a, {[b]: operand[b]}), {}), true)) { + .filter(key => !Object.keys(LOGICAL_OPERATORS).includes(key)) + .reduce((a, b) => Object.assign(a, {[b]: operand[b]}), {}), true)) { subMatcher = compileValueSelector(operand, matcher); isDocMatcher = false; } else { @@ -163,13 +152,12 @@ export const ELEMENT_OPERATORS = { // {$elemMatch: {x: 3}} means "an element has a field x:3", not // "consists only of a field x:3". Also, regexps and sub-$ are allowed. subMatcher = compileDocumentSelector(operand, matcher, - {inElemMatch: true}); + {inElemMatch: true}); isDocMatcher = true; } return value => { - if (!Array.isArray(value)) - return false; + if (!Array.isArray(value)) {return false;} for (let i = 0; i < value.length; ++i) { const arrayElement = value[i]; let arg; @@ -177,8 +165,7 @@ export const ELEMENT_OPERATORS = { // We can only match {$elemMatch: {b: 3}} against objects. // (We can also match against arrays, if there's numeric indices, // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) - if (!isIndexable(arrayElement)) - return false; + if (!isIndexable(arrayElement)) {return false;} arg = arrayElement; } else { // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches @@ -186,13 +173,12 @@ export const ELEMENT_OPERATORS = { arg = [{value: arrayElement, dontIterate: true}]; } // XXX support $near in $elemMatch by propagating $distance? - if (subMatcher(arg).result) - return i; // specially understood to mean "use as arrayIndices" + if (subMatcher(arg).result) {return i;} // specially understood to mean "use as arrayIndices" } return false; }; - } - } + }, + }, }; // Operators that appear at the top level of a document selector. @@ -209,8 +195,7 @@ const LOGICAL_OPERATORS = { // Special case: if there is only one matcher, use it directly, *preserving* // any arrayIndices it returns. - if (matchers.length === 1) - return matchers[0]; + if (matchers.length === 1) {return matchers[0];} return doc => { const result = matchers.some(f => f(doc).result); @@ -242,18 +227,18 @@ const LOGICAL_OPERATORS = { } return doc => // We make the document available as both `this` and `obj`. // XXX not sure what we should do if this throws - ({ - result: selectorValue.call(doc, doc) - }); + ({ + result: selectorValue.call(doc, doc), + }); }, // This is just used as a comment in the query (in MongoDB, it also ends up in // query logs); it has no effect on the actual selection. $comment() { return () => ({ - result: true + result: true, }); - } + }, }; // Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a @@ -282,28 +267,23 @@ const VALUE_OPERATORS = { }, // $options just provides options for $regex; its logic is inside $regex $options(operand, valueSelector) { - if (!valueSelector.hasOwnProperty('$regex')) - throw Error("$options needs a $regex"); + if (!valueSelector.hasOwnProperty('$regex')) {throw Error('$options needs a $regex');} return everythingMatcher; }, // $maxDistance is basically an argument to $near $maxDistance(operand, valueSelector) { - if (!valueSelector.$near) - throw Error("$maxDistance needs a $near"); + if (!valueSelector.$near) {throw Error('$maxDistance needs a $near');} return everythingMatcher; }, $all(operand, valueSelector, matcher) { - if (!Array.isArray(operand)) - throw Error("$all requires array"); + if (!Array.isArray(operand)) {throw Error('$all requires array');} // Not sure why, but this seems to be what MongoDB does. - if (operand.length === 0) - return nothingMatcher; + if (operand.length === 0) {return nothingMatcher;} const branchedMatchers = []; operand.forEach(criterion => { // XXX handle $all/$elemMatch combination - if (isOperatorObject(criterion)) - throw Error("no $ expressions in $all"); + if (isOperatorObject(criterion)) {throw Error('no $ expressions in $all');} // This is always a regexp or equality selector. branchedMatchers.push(compileValueSelector(criterion, matcher)); }); @@ -312,8 +292,7 @@ const VALUE_OPERATORS = { return andBranchedMatchers(branchedMatchers); }, $near(operand, valueSelector, matcher, isRoot) { - if (!isRoot) - throw Error("$near can't be inside another $ operator"); + if (!isRoot) {throw Error("$near can't be inside another $ operator");} matcher._hasGeoQuery = true; // There are two kinds of geodata in MongoDB: legacy coordinate pairs and @@ -330,26 +309,23 @@ const VALUE_OPERATORS = { // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get // a priority. - if (!value) - return null; - if(!value.type) + if (!value) {return null;} + if (!value.type) { return GeoJSON.pointDistance(point, - { type: "Point", coordinates: pointToArray(value) }); - if (value.type === "Point") { - return GeoJSON.pointDistance(point, value); - } else { - return GeoJSON.geometryWithinRadius(value, point, maxDistance) - ? 0 : maxDistance + 1; + { type: 'Point', coordinates: pointToArray(value) }); } + if (value.type === 'Point') { + return GeoJSON.pointDistance(point, value); + } + return GeoJSON.geometryWithinRadius(value, point, maxDistance) + ? 0 : maxDistance + 1; }; } else { maxDistance = valueSelector.$maxDistance; - if (!isIndexable(operand)) - throw Error("$near argument must be coordinate pair or GeoJSON"); + if (!isIndexable(operand)) {throw Error('$near argument must be coordinate pair or GeoJSON');} point = pointToArray(operand); distance = value => { - if (!isIndexable(value)) - return null; + if (!isIndexable(value)) {return null;} return distanceCoordinatePairs(point, value); }; } @@ -368,42 +344,34 @@ const VALUE_OPERATORS = { branchedValues.every(branch => { // if operation is an update, don't skip branches, just return the first one (#3599) let curDistance; - if (!matcher._isUpdate){ - if (!(typeof branch.value === "object")){ + if (!matcher._isUpdate) { + if (!(typeof branch.value === 'object')) { return true; } curDistance = distance(branch.value); // Skip branches that aren't real points or are too far away. - if (curDistance === null || curDistance > maxDistance) - return true; + if (curDistance === null || curDistance > maxDistance) {return true;} // Skip anything that's a tie. - if (result.distance !== undefined && result.distance <= curDistance) - return true; + if (result.distance !== undefined && result.distance <= curDistance) {return true;} } result.result = true; result.distance = curDistance; - if (!branch.arrayIndices) - delete result.arrayIndices; - else - result.arrayIndices = branch.arrayIndices; - if (matcher._isUpdate) - return false; + if (!branch.arrayIndices) {delete result.arrayIndices;} else {result.arrayIndices = branch.arrayIndices;} + if (matcher._isUpdate) {return false;} return true; }); return result; }; - } + }, }; // NB: We are cheating and using this function to implement 'AND' for both // 'document matchers' and 'branched matchers'. They both return result objects // but the argument is different: for the former it's a whole doc, whereas for // the latter it's an array of 'branched values'. -function andSomeMatchers (subMatchers) { - if (subMatchers.length === 0) - return everythingMatcher; - if (subMatchers.length === 1) - return subMatchers[0]; +function andSomeMatchers(subMatchers) { + if (subMatchers.length === 0) {return everythingMatcher;} + if (subMatchers.length === 1) {return subMatchers[0];} return docOrBranches => { const ret = {}; @@ -438,12 +406,10 @@ function andSomeMatchers (subMatchers) { const andDocumentMatchers = andSomeMatchers; const andBranchedMatchers = andSomeMatchers; -function compileArrayOfDocumentSelectors (selectors, matcher, inElemMatch) { - if (!Array.isArray(selectors) || selectors.length === 0) - throw Error('$and/$or/$nor must be nonempty array'); +function compileArrayOfDocumentSelectors(selectors, matcher, inElemMatch) { + if (!Array.isArray(selectors) || selectors.length === 0) {throw Error('$and/$or/$nor must be nonempty array');} return selectors.map(subSelector => { - if (!LocalCollection._isPlainObject(subSelector)) - throw Error('$or/$and/$nor entries need to be full objects'); + if (!LocalCollection._isPlainObject(subSelector)) {throw Error('$or/$and/$nor entries need to be full objects');} return compileDocumentSelector( subSelector, matcher, {inElemMatch}); }); @@ -456,24 +422,22 @@ function compileArrayOfDocumentSelectors (selectors, matcher, inElemMatch) { // // If this is the root document selector (ie, not wrapped in $and or the like), // then isRoot is true. (This is used by $near.) -export function compileDocumentSelector (docSelector, matcher, options = {}) { +export function compileDocumentSelector(docSelector, matcher, options = {}) { let docMatchers = []; Object.keys(docSelector).forEach(key => { let subSelector = docSelector[key]; if (key.substr(0, 1) === '$') { // Outer operators are either logical operators (they recurse back into // this function), or $where. - if (!LOGICAL_OPERATORS.hasOwnProperty(key)) - throw new Error(`Unrecognized logical operator: ${key}`); + if (!LOGICAL_OPERATORS.hasOwnProperty(key)) {throw new Error(`Unrecognized logical operator: ${key}`);} matcher._isSimple = false; docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, - options.inElemMatch)); + options.inElemMatch)); } else { // Record this path, but only if we aren't in an elemMatcher, since in an // elemMatch this is a path inside an object in an array, not in the doc // root. - if (!options.inElemMatch) - matcher._recordPathUsed(key); + if (!options.inElemMatch) {matcher._recordPathUsed(key);} let lookUpByIndex = makeLookupFunction(key); let valueMatcher = compileValueSelector(subSelector, matcher, options.isRoot); @@ -491,17 +455,16 @@ export function compileDocumentSelector (docSelector, matcher, options = {}) { // {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to // indicate equality). Returns a branched matcher: a function mapping // [branched value]->result object. -function compileValueSelector (valueSelector, matcher, isRoot) { +function compileValueSelector(valueSelector, matcher, isRoot) { if (valueSelector instanceof RegExp) { matcher._isSimple = false; return convertElementMatcherToBranchedMatcher( regexpElementMatcher(valueSelector)); } else if (isOperatorObject(valueSelector)) { return operatorBranchedMatcher(valueSelector, matcher, isRoot); - } else { - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(valueSelector)); } + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(valueSelector)); } // Given an element matcher (which evaluates a single value), returns a branched @@ -524,15 +487,13 @@ function convertElementMatcherToBranchedMatcher(elementMatcher, options = {}) { // XXX This code dates from when we only stored a single array index // (for the outermost array). Should we be also including deeper array // indices from the $elemMatch match? - if (!element.arrayIndices) - element.arrayIndices = [matched]; + if (!element.arrayIndices) {element.arrayIndices = [matched];} matched = true; } // If some element matched, and it's tagged with array indices, include // those indices in our result object. - if (matched && element.arrayIndices) - ret.arrayIndices = element.arrayIndices; + if (matched && element.arrayIndices) {ret.arrayIndices = element.arrayIndices;} return matched; }); @@ -541,38 +502,36 @@ function convertElementMatcherToBranchedMatcher(elementMatcher, options = {}) { } // Helpers for $near. -function distanceCoordinatePairs (a, b) { +function distanceCoordinatePairs(a, b) { a = pointToArray(a); b = pointToArray(b); const x = a[0] - b[0]; const y = a[1] - b[1]; - if (Number.isNaN(x) || Number.isNaN(y)) - return null; + if (Number.isNaN(x) || Number.isNaN(y)) {return null;} return Math.sqrt(x * x + y * y); } // Takes something that is not an operator object and returns an element matcher // for equality with that thing. -export function equalityElementMatcher (elementSelector) { - if (isOperatorObject(elementSelector)) - throw Error("Can't create equalityValueSelector for operator object"); +export function equalityElementMatcher(elementSelector) { + if (isOperatorObject(elementSelector)) {throw Error("Can't create equalityValueSelector for operator object");} // Special-case: null and undefined are equal (if you got undefined in there // somewhere, or if you got it due to some branch being non-existent in the // weird special case), even though they aren't with EJSON.equals. if (elementSelector == null) { // undefined or null return value => // undefined or null - value == null; + value == null; } return value => LocalCollection._f._equal(elementSelector, value); } -function everythingMatcher (docOrBranchedValues) { +function everythingMatcher(docOrBranchedValues) { return {result: true}; } -export function expandArraysInBranches (branches, skipTheArrays) { +export function expandArraysInBranches(branches, skipTheArrays) { const branchesOut = []; branches.forEach(branch => { const thisIsArray = Array.isArray(branch.value); @@ -583,14 +542,14 @@ export function expandArraysInBranches (branches, skipTheArrays) { if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { branchesOut.push({ value: branch.value, - arrayIndices: branch.arrayIndices + arrayIndices: branch.arrayIndices, }); } if (thisIsArray && !branch.dontIterate) { branch.value.forEach((leaf, i) => { branchesOut.push({ value: leaf, - arrayIndices: (branch.arrayIndices || []).concat(i) + arrayIndices: (branch.arrayIndices || []).concat(i), }); }); } @@ -599,17 +558,17 @@ export function expandArraysInBranches (branches, skipTheArrays) { } // Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear. -function getOperandBitmask (operand, selector) { +function getOperandBitmask(operand, selector) { // numeric bitmask // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. // Otherwise, $bitsAllSet will return an error. if (Number.isInteger(operand) && operand >= 0) { - return new Uint8Array(new Int32Array([operand]).buffer) + return new Uint8Array(new Int32Array([operand]).buffer); } // bindata bitmask // You can also use an arbitrarily large BinData instance as a bitmask. else if (EJSON.isBinary(operand)) { - return new Uint8Array(operand.buffer) + return new Uint8Array(operand.buffer); } // position list // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. @@ -617,17 +576,16 @@ function getOperandBitmask (operand, selector) { const buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1); const view = new Uint8Array(buffer); operand.forEach(x => { - view[x >> 3] |= (1 << (x & 0x7)) - }) - return view + view[x >> 3] |= 1 << (x & 0x7); + }); + return view; } // bad operand - else { - throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`) - } + + throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`); } -function getValueBitmask (value, length) { +function getValueBitmask(value, length) { // The field value must be either numerical or a BinData instance. Otherwise, $bits... will not match the current document. // numerical if (Number.isSafeInteger(value)) { @@ -635,29 +593,29 @@ function getValueBitmask (value, length) { // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. const buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); let view = new Uint32Array(buffer, 0, 2); - view[0] = (value % ((1 << 16) * (1 << 16))) | 0 - view[1] = (value / ((1 << 16) * (1 << 16))) | 0 + view[0] = value % ((1 << 16) * (1 << 16)) | 0; + view[1] = value / ((1 << 16) * (1 << 16)) | 0; // sign extension if (value < 0) { - view = new Uint8Array(buffer, 2) + view = new Uint8Array(buffer, 2); view.forEach((byte, idx) => { - view[idx] = 0xff - }) + view[idx] = 0xff; + }); } - return new Uint8Array(buffer) + return new Uint8Array(buffer); } // bindata else if (EJSON.isBinary(value)) { - return new Uint8Array(value.buffer) + return new Uint8Array(value.buffer); } // no match - return false + return false; } // Returns a branched matcher that matches iff the given matcher does not. // Note that this implicitly "deMorganizes" the wrapped function. ie, it // means that ALL branch values need to fail to match innerBranchedMatcher. -function invertBranchedMatcher (branchedMatcher) { +function invertBranchedMatcher(branchedMatcher) { return branchValues => { const invertMe = branchedMatcher(branchValues); // We explicitly choose to strip arrayIndices here: it doesn't make sense to @@ -667,20 +625,19 @@ function invertBranchedMatcher (branchedMatcher) { }; } -export function isIndexable (obj) { +export function isIndexable(obj) { return Array.isArray(obj) || LocalCollection._isPlainObject(obj); } -export function isNumericKey (s) { +export function isNumericKey(s) { return /^[0-9]+$/.test(s); } // Returns true if this is an object with at least one key and all keys begin // with $. Unless inconsistentOK is set, throws if some keys begin with $ and // others don't. -export function isOperatorObject (valueSelector, inconsistentOK) { - if (!LocalCollection._isPlainObject(valueSelector)) - return false; +export function isOperatorObject(valueSelector, inconsistentOK) { + if (!LocalCollection._isPlainObject(valueSelector)) {return false;} let theseAreOperators = undefined; Object.keys(valueSelector).forEach(selKey => { @@ -688,8 +645,7 @@ export function isOperatorObject (valueSelector, inconsistentOK) { if (theseAreOperators === undefined) { theseAreOperators = thisIsOperator; } else if (theseAreOperators !== thisIsOperator) { - if (!inconsistentOK) - throw new Error(`Inconsistent operator: ${JSON.stringify(valueSelector)}`); + if (!inconsistentOK) {throw new Error(`Inconsistent operator: ${JSON.stringify(valueSelector)}`);} theseAreOperators = false; } }); @@ -697,7 +653,7 @@ export function isOperatorObject (valueSelector, inconsistentOK) { } // Helper for $lt/$gt/$lte/$gte. -function makeInequality (cmpValueComparator) { +function makeInequality(cmpValueComparator) { return { compileElementSelector(operand) { // Arrays never compare false with non-arrays for any inequality. @@ -710,21 +666,18 @@ function makeInequality (cmpValueComparator) { // Special case: consider undefined and null the same (so true with // $gte/$lte). - if (operand === undefined) - operand = null; + if (operand === undefined) {operand = null;} const operandType = LocalCollection._f._type(operand); return value => { - if (value === undefined) - value = null; + if (value === undefined) {value = null;} // Comparisons are never true among things of different type (except // null vs undefined). - if (LocalCollection._f._type(value) !== operandType) - return false; + if (LocalCollection._f._type(value) !== operandType) {return false;} return cmpValueComparator(LocalCollection._f._cmp(value, operand)); }; - } + }, }; } @@ -791,25 +744,21 @@ export function makeLookupFunction(key, options = {}) { } const omitUnnecessaryFields = retVal => { - if (!retVal.dontIterate) - delete retVal.dontIterate; - if (retVal.arrayIndices && !retVal.arrayIndices.length) - delete retVal.arrayIndices; + if (!retVal.dontIterate) {delete retVal.dontIterate;} + if (retVal.arrayIndices && !retVal.arrayIndices.length) {delete retVal.arrayIndices;} return retVal; }; // Doc will always be a plain object or an array. // apply an explicit numeric index, an array. return (doc, arrayIndices) => { - if (!arrayIndices) - arrayIndices = []; + if (!arrayIndices) {arrayIndices = [];} if (Array.isArray(doc)) { // If we're being asked to do an invalid lookup into an array (non-integer // or out-of-bounds), return no results (which is different from returning // a single undefined result, in that `null` equality checks won't match). - if (!(firstPartIsNumeric && firstPart < doc.length)) - return []; + if (!(firstPartIsNumeric && firstPart < doc.length)) {return [];} // Remember that we used this array index. Include an 'x' to indicate that // the previous index came from being considered as an explicit array @@ -846,10 +795,9 @@ export function makeLookupFunction(key, options = {}) { // return a single `undefined` (which can, for example, match via equality // with `null`). if (!isIndexable(firstLevel)) { - if (Array.isArray(doc)) - return []; + if (Array.isArray(doc)) {return [];} return [omitUnnecessaryFields({value: undefined, - arrayIndices})]; + arrayIndices})]; } const result = []; @@ -895,22 +843,22 @@ export function makeLookupFunction(key, options = {}) { // Use it to export private functions to test in Tinytest. MinimongoTest = {makeLookupFunction}; MinimongoError = (message, options = {}) => { - if (typeof message === "string" && options.field) { + if (typeof message === 'string' && options.field) { message += ` for field '${options.field}'`; } const e = new Error(message); - e.name = "MinimongoError"; + e.name = 'MinimongoError'; return e; }; -export function nothingMatcher (docOrBranchedValues) { +export function nothingMatcher(docOrBranchedValues) { return {result: false}; } // Takes an operator object (an object with $ keys) and returns a branched // matcher for it. -function operatorBranchedMatcher (valueSelector, matcher, isRoot) { +function operatorBranchedMatcher(valueSelector, matcher, isRoot) { // Each valueSelector works separately on the various branches. So one // operator can match one branch and another can match another branch. This // is OK. @@ -962,15 +910,12 @@ export function pathsToTree(paths, newLeafFn, conflictFn, tree = {}) { // use .every just for iteration with break const success = pathArr.slice(0, -1).every((key, idx) => { - if (!treePos.hasOwnProperty(key)) - treePos[key] = {}; - else if (treePos[key] !== Object(treePos[key])) { + if (!treePos.hasOwnProperty(key)) {treePos[key] = {};} else if (treePos[key] !== Object(treePos[key])) { treePos[key] = conflictFn(treePos[key], - pathArr.slice(0, idx + 1).join('.'), - keyPath); + pathArr.slice(0, idx + 1).join('.'), + keyPath); // break out of loop if we are failing for this path - if (treePos[key] !== Object(treePos[key])) - return false; + if (treePos[key] !== Object(treePos[key])) {return false;} } treePos = treePos[key]; @@ -979,10 +924,7 @@ export function pathsToTree(paths, newLeafFn, conflictFn, tree = {}) { if (success) { const lastKey = pathArr[pathArr.length - 1]; - if (!treePos.hasOwnProperty(lastKey)) - treePos[lastKey] = newLeafFn(keyPath); - else - treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath); + if (!treePos.hasOwnProperty(lastKey)) {treePos[lastKey] = newLeafFn(keyPath);} else {treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath);} } }); @@ -992,7 +934,7 @@ export function pathsToTree(paths, newLeafFn, conflictFn, tree = {}) { // Makes sure we get 2 elements array and assume the first one to be x and // the second one to y no matter what user passes. // In case user passes { lon: x, lat: y } returns [x, y] -function pointToArray (point) { +function pointToArray(point) { return Array.isArray(point) ? point.slice() : [point.x, point.y]; } @@ -1002,7 +944,7 @@ function pointToArray (point) { // - tree - Object - tree representation of keys involved in projection // (exception for '_id' as it is a special case handled separately) // - including - Boolean - "take only certain fields" type of projection -export function projectionDetails (fields) { +export function projectionDetails(fields) { // Find the non-_id keys (_id is handled specially because it is included unless // explicitly excluded). Sort the keys, so that our code to detect overlaps // like 'foo' and 'foo.bar' can assume that 'foo' comes first. @@ -1016,18 +958,16 @@ export function projectionDetails (fields) { // special case, since exclusive _id is always allowed. if (fieldsKeys.length > 0 && !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && - !(fieldsKeys.includes('_id') && fields['_id'])) - fieldsKeys = fieldsKeys.filter(key => key !== '_id'); + !(fieldsKeys.includes('_id') && fields._id)) {fieldsKeys = fieldsKeys.filter(key => key !== '_id');} let including = null; // Unknown fieldsKeys.forEach(keyPath => { const rule = !!fields[keyPath]; - if (including === null) - including = rule; + if (including === null) {including = rule;} if (including !== rule) - // This error message is copied from MongoDB shell - throw MinimongoError("You cannot currently mix including and excluding fields."); + // This error message is copied from MongoDB shell + {throw MinimongoError('You cannot currently mix including and excluding fields.');} }); @@ -1059,19 +999,18 @@ export function projectionDetails (fields) { return { tree: projectionRulesTree, - including + including, }; } // Takes a RegExp object and returns an element matcher. -export function regexpElementMatcher (regexp) { +export function regexpElementMatcher(regexp) { return value => { if (value instanceof RegExp) { return value.toString() === regexp.toString(); } // Regexps only work against strings. - if (typeof value !== 'string') - return false; + if (typeof value !== 'string') {return false;} // Reset regexp's state to avoid inconsistent matching for objects with the // same value on consecutive calls of regexp.test. This happens only if the diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index 05dfb1aa6e..9ce9ef60c8 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -4,8 +4,8 @@ import {LocalCollection} from './local_collection.js'; // a defined order, limit, and offset. creating a Cursor with LocalCollection.find(), export class Cursor { // don't call this ctor directly. use LocalCollection.find(). - constructor (collection, selector, options = {}) { -this.collection = collection; + constructor(collection, selector, options = {}) { + this.collection = collection; this.sorter = null; this.matcher = new Minimongo.Matcher(selector); @@ -19,7 +19,7 @@ this.collection = collection; this._selectorId = undefined; if (this.matcher.hasGeoQuery() || options.sort) { this.sorter = new Minimongo.Sorter(options.sort || [], - { matcher: this.matcher }); + { matcher: this.matcher }); } } @@ -32,8 +32,7 @@ this.collection = collection; this._transform = LocalCollection.wrapTransform(options.transform); // by default, queries register w/ Tracker when it is available. - if (typeof Tracker !== "undefined") - this.reactive = (options.reactive === undefined) ? true : options.reactive; + if (typeof Tracker !== 'undefined') {this.reactive = options.reactive === undefined ? true : options.reactive;} } /** @@ -44,10 +43,11 @@ this.collection = collection; * @locus Anywhere * @returns {Number} */ - count () { - if (this.reactive) + count() { + if (this.reactive) { this._depend({added: true, removed: true}, - true /* allow the observe to be unordered */); + true /* allow the observe to be unordered */); + } return this._getRawObjects({ordered: true}).length; } @@ -60,7 +60,7 @@ this.collection = collection; * @locus Anywhere * @returns {Object[]} */ - fetch () { + fetch() { const res = []; this.forEach(doc => { res.push(doc); @@ -82,7 +82,7 @@ this.collection = collection; * @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`. */ - forEach (callback, thisArg) { + forEach(callback, thisArg) { const objects = this._getRawObjects({ordered: true}); if (this.reactive) { @@ -97,13 +97,12 @@ this.collection = collection; // This doubles as a clone operation. elt = this._projectionFn(elt); - if (this._transform) - elt = this._transform(elt); + if (this._transform) {elt = this._transform(elt);} callback.call(thisArg, elt, i, this); }); } - getTransform () { + getTransform() { return this._transform; } @@ -116,7 +115,7 @@ this.collection = collection; * @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`. */ - map (callback, thisArg) { + map(callback, thisArg) { const res = []; this.forEach((doc, index) => { res.push(callback.call(thisArg, doc, index, this)); @@ -152,7 +151,7 @@ this.collection = collection; * @instance * @param {Object} callbacks Functions to call to deliver the result set as it changes */ - observe (options) { + observe(options) { return LocalCollection._observeFromObserveChanges(this, options); } @@ -163,29 +162,27 @@ this.collection = collection; * @instance * @param {Object} callbacks Functions to call to deliver the result set as it changes */ - observeChanges (options) { + observeChanges(options) { const ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); // there are several places that assume you aren't combining skip/limit with // unordered observe. eg, update's EJSON.clone, and the "there are several" // comment in _modifyAndNotify // XXX allow skip/limit with unordered observe - if (!options._allow_unordered && !ordered && (this.skip || this.limit)) - throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); + if (!options._allow_unordered && !ordered && (this.skip || this.limit)) {throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit");} - if (this.fields && (this.fields._id === 0 || this.fields._id === false)) - throw Error("You may not observe a cursor with {fields: {_id: 0}}"); + if (this.fields && (this.fields._id === 0 || this.fields._id === false)) {throw Error('You may not observe a cursor with {fields: {_id: 0}}');} const query = { dirty: false, matcher: this.matcher, // not fast pathed sorter: ordered && this.sorter, - distances: ( - this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap), + distances: + this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap, resultsSnapshot: null, ordered, cursor: this, - projectionFn: this._projectionFn + projectionFn: this._projectionFn, }; let qid; @@ -197,8 +194,7 @@ this.collection = collection; } query.results = this._getRawObjects({ ordered, distances: query.distances}); - if (this.collection.paused) - query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap); + if (this.collection.paused) {query.resultsSnapshot = ordered ? [] : new LocalCollection._IdMap;} // wrap callbacks we were passed. callbacks only fire when not paused and // are never undefined @@ -208,14 +204,12 @@ this.collection = collection; // furthermore, callbacks enqueue until the operation we're working on is // done. const wrapCallback = f => { - if (!f) - return () => {}; + if (!f) {return () => {};} const self = this; - return function (/*args*/) { + return function(/* args*/) { const args = arguments; - if (self.collection.paused) - return; + if (self.collection.paused) {return;} self.collection._observeQueue.queueTask(() => { f.apply(this, args); @@ -237,8 +231,7 @@ this.collection = collection; const fields = EJSON.clone(doc); delete fields._id; - if (ordered) - query.addedBefore(doc._id, this._projectionFn(fields), null); + if (ordered) {query.addedBefore(doc._id, this._projectionFn(fields), null);} query.added(doc._id, this._projectionFn(fields)); }); } @@ -247,9 +240,8 @@ this.collection = collection; Object.assign(handle, { collection: this.collection, stop: () => { - if (this.reactive) - delete this.collection.queries[qid]; - } + if (this.reactive) {delete this.collection.queries[qid];} + }, }); if (this.reactive && Tracker.active) { @@ -273,11 +265,11 @@ this.collection = collection; // reason to have a "rewind" interface. All it did was make multiple calls // to fetch/map/forEach return nothing the second time. // XXX COMPAT WITH 0.8.1 - rewind () {} + rewind() {} // XXX Maybe we need a version of observe that just calls a callback if // anything changed. - _depend (changers, _allow_unordered) { + _depend(changers, _allow_unordered) { if (Tracker.active) { const v = new Tracker.Dependency; v.depend(); @@ -285,11 +277,10 @@ this.collection = collection; const options = { _suppress_initial: true, - _allow_unordered + _allow_unordered, }; ['added', 'changed', 'removed', 'addedBefore', 'movedBefore'].forEach(fnName => { - if (changers[fnName]) - options[fnName] = notifyChange; + if (changers[fnName]) {options[fnName] = notifyChange;} }); // observeChanges will stop() when this computation is invalidated @@ -297,7 +288,7 @@ this.collection = collection; } } - _getCollectionName () { + _getCollectionName() { return this.collection.name; } @@ -316,7 +307,7 @@ this.collection = collection; // argument, this function will clear it and use it for this purpose (otherwise // it will just create its own _IdMap). The observeChanges implementation uses // this to remember the distances after this function returns. - _getRawObjects (options = {}) { + _getRawObjects(options = {}) { // XXX use OrderedDict instead of array, and make IdMap and OrderedDict // compatible const results = options.ordered ? [] : new LocalCollection._IdMap; @@ -326,15 +317,11 @@ this.collection = collection; // If you have non-zero skip and ask for a single id, you get // nothing. This is so it matches the behavior of the '{_id: foo}' // path. - if (this.skip) - return results; + if (this.skip) {return results;} const selectedDoc = this.collection._docs.get(this._selectorId); if (selectedDoc) { - if (options.ordered) - results.push(selectedDoc); - else - results.set(this._selectorId, selectedDoc); + if (options.ordered) {results.push(selectedDoc);} else {results.set(this._selectorId, selectedDoc);} } return results; } @@ -359,8 +346,7 @@ this.collection = collection; if (matchResult.result) { if (options.ordered) { results.push(doc); - if (distances && matchResult.distance !== undefined) - distances.set(id, matchResult.distance); + if (distances && matchResult.distance !== undefined) {distances.set(id, matchResult.distance);} } else { results.set(id, doc); } @@ -368,13 +354,11 @@ this.collection = collection; // Fast path for limited unsorted queries. // XXX 'length' check here seems wrong for ordered if (this.limit && !this.skip && !this.sorter && - results.length === this.limit) - return false; // break + results.length === this.limit) {return false;} // break return true; // continue }); - if (!options.ordered) - return results; + if (!options.ordered) {return results;} if (this.sorter) { const comparator = this.sorter.getComparator({distances}); @@ -382,13 +366,12 @@ this.collection = collection; } const idx_start = this.skip || 0; - const idx_end = this.limit ? (this.limit + idx_start) : results.length; + const idx_end = this.limit ? this.limit + idx_start : results.length; return results.slice(idx_start, idx_end); } - _publishCursor (sub) { - if (! this.collection.name) - throw new Error("Can't publish a cursor from a collection without a name."); + _publishCursor(sub) { + if (! this.collection.name) {throw new Error("Can't publish a cursor from a collection without a name.");} const collection = this.collection.name; // XXX minimongo should not depend on mongo-livedata! diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index baa0ee8be7..237092927e 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -4,14 +4,14 @@ import { isIndexable, isNumericKey, isOperatorObject, - projectionDetails + projectionDetails, } from './common.js'; // XXX type checking on selectors (graceful error if malformed) // LocalCollection: a set of documents that supports queries and modifiers. export class LocalCollection { - constructor (name) { + constructor(name) { this.name = name; // _id -> document (also containing id) this._docs = new LocalCollection._IdMap; @@ -55,19 +55,17 @@ export class LocalCollection { // XXX sort does not yet support subkeys ('a.b') .. fix that! // XXX add one more sort form: "key" // XXX tests - find (selector, options) { + find(selector, options) { // default syntax for everything is to omit the selector argument. // but if selector is explicitly passed in as false or undefined, we // want a selector that matches nothing. - if (arguments.length === 0) - selector = {}; + if (arguments.length === 0) {selector = {};} return new LocalCollection.Cursor(this, selector, options); } - findOne (selector, options) { - if (arguments.length === 0) - selector = {}; + findOne(selector, options) { + if (arguments.length === 0) {selector = {};} // NOTE: by setting limit 1 here, we end up using very inefficient // code that recomputes the whole query on each update. The upside is @@ -85,7 +83,7 @@ export class LocalCollection { // XXX possibly enforce that 'undefined' does not appear (we assume // this in our handling of null and $exists) - insert (doc, callback) { + insert(doc, callback) { doc = EJSON.clone(doc); assertHasValidFieldNames(doc); @@ -94,12 +92,11 @@ export class LocalCollection { // if you really want to use ObjectIDs, set this global. // Mongo.Collection specifies its own ids and does not use this code. doc._id = LocalCollection._useOID ? new MongoID.ObjectID() - : Random.id(); + : Random.id(); } const id = doc._id; - if (this._docs.has(id)) - throw MinimongoError(`Duplicate _id '${id}'`); + if (this._docs.has(id)) {throw MinimongoError(`Duplicate _id '${id}'`);} this._saveOriginal(id, undefined); this._docs.set(id, doc); @@ -111,36 +108,31 @@ export class LocalCollection { if (query.dirty) continue; const matchResult = query.matcher.documentMatches(doc); if (matchResult.result) { - if (query.distances && matchResult.distance !== undefined) - query.distances.set(id, matchResult.distance); - if (query.cursor.skip || query.cursor.limit) - queriesToRecompute.push(qid); - else - LocalCollection._insertInResults(query, doc); + if (query.distances && matchResult.distance !== undefined) {query.distances.set(id, matchResult.distance);} + if (query.cursor.skip || query.cursor.limit) {queriesToRecompute.push(qid);} else {LocalCollection._insertInResults(query, doc);} } } queriesToRecompute.forEach(qid => { - if (this.queries[qid]) - this._recomputeResults(this.queries[qid]); + if (this.queries[qid]) {this._recomputeResults(this.queries[qid]);} }); this._observeQueue.drain(); // Defer because the caller likely doesn't expect the callback to be run // immediately. - if (callback) + if (callback) { Meteor.defer(() => { callback(null, id); }); + } return id; } // Pause the observers. No callbacks from observers will fire until // 'resumeObservers' is called. - pauseObservers () { + pauseObservers() { // No-op if already paused. - if (this.paused) - return; + if (this.paused) {return;} // Set the 'paused' flag such that new observer messages don't fire. this.paused = true; @@ -153,7 +145,7 @@ export class LocalCollection { } } - remove (selector, callback) { + remove(selector, callback) { // Easy special case: if we're not calling observeChanges callbacks and we're // not saving originals and we got asked to remove everything, then just empty // everything directly. @@ -179,8 +171,7 @@ export class LocalCollection { const matcher = new Minimongo.Matcher(selector); const remove = []; this._eachPossiblyMatchingDoc(selector, (doc, id) => { - if (matcher.documentMatches(doc).result) - remove.push(id); + if (matcher.documentMatches(doc).result) {remove.push(id);} }); const queriesToRecompute = []; @@ -193,10 +184,7 @@ export class LocalCollection { if (query.dirty) return; if (query.matcher.documentMatches(removeDoc).result) { - if (query.cursor.skip || query.cursor.limit) - queriesToRecompute.push(qid); - else - queryRemove.push({qid, doc: removeDoc}); + if (query.cursor.skip || query.cursor.limit) {queriesToRecompute.push(qid);} else {queryRemove.push({qid, doc: removeDoc});} } }); this._saveOriginal(removeId, removeDoc); @@ -213,15 +201,15 @@ export class LocalCollection { }); queriesToRecompute.forEach(qid => { const query = this.queries[qid]; - if (query) - this._recomputeResults(query); + if (query) {this._recomputeResults(query);} }); this._observeQueue.drain(); const result = remove.length; - if (callback) + if (callback) { Meteor.defer(() => { callback(null, result); }); + } return result; } @@ -229,10 +217,9 @@ export class LocalCollection { // notifications to bring them to the current state of the // database. Note that this is not just replaying all the changes that // happened during the pause, it is a smarter 'coalesced' diff. - resumeObservers () { + resumeObservers() { // No-op if not paused. - if (!this.paused) - return; + if (!this.paused) {return;} // Unset the 'paused' flag. Make sure to do this first, otherwise // observer methods won't actually fire when we trigger them. @@ -256,9 +243,8 @@ export class LocalCollection { this._observeQueue.drain(); } - retrieveOriginals () { - if (!this._savedOriginals) - throw new Error("Called retrieveOriginals without saveOriginals"); + retrieveOriginals() { + if (!this._savedOriginals) {throw new Error('Called retrieveOriginals without saveOriginals');} const originals = this._savedOriginals; this._savedOriginals = null; @@ -272,15 +258,14 @@ export class LocalCollection { // at the time of saveOriginals. (In the case of an inserted document, undefined // is the value.) You must alternate between calls to saveOriginals() and // retrieveOriginals(). - saveOriginals () { - if (this._savedOriginals) - throw new Error("Called saveOriginals twice without retrieveOriginals"); + saveOriginals() { + if (this._savedOriginals) {throw new Error('Called saveOriginals twice without retrieveOriginals');} this._savedOriginals = new LocalCollection._IdMap; } // XXX atomicity: if multi is true, and one modification fails, do // we rollback the whole operation, or what? - update (selector, mod, options, callback) { + update(selector, mod, options, callback) { if (! callback && options instanceof Function) { callback = options; options = null; @@ -313,7 +298,7 @@ export class LocalCollection { } if (!(query.results instanceof Array)) { - throw new Error("Assertion failed: query.results not an array"); + throw new Error('Assertion failed: query.results not an array'); } // Clones a document to be stored in `qidToOriginalResults` @@ -323,18 +308,17 @@ export class LocalCollection { const memoizedCloneIfNeeded = doc => { if (docMap.has(doc._id)) { return docMap.get(doc._id); - } else { - let docToMemoize; - - if (idsMatchedBySelector && !idsMatchedBySelector.some(id => EJSON.equals(id, doc._id))) { - docToMemoize = doc; - } else { - docToMemoize = EJSON.clone(doc); - } - - docMap.set(doc._id, docToMemoize); - return docToMemoize; } + let docToMemoize; + + if (idsMatchedBySelector && !idsMatchedBySelector.some(id => EJSON.equals(id, doc._id))) { + docToMemoize = doc; + } else { + docToMemoize = EJSON.clone(doc); + } + + docMap.set(doc._id, docToMemoize); + return docToMemoize; }; qidToOriginalResults[qid] = query.results.map(memoizedCloneIfNeeded); @@ -351,16 +335,14 @@ export class LocalCollection { this._saveOriginal(id, doc); this._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices); ++updateCount; - if (!options.multi) - return false; // break + if (!options.multi) {return false;} // break } return true; }); Object.keys(recomputeQids).forEach(qid => { const query = this.queries[qid]; - if (query) - this._recomputeResults(query, qidToOriginalResults[qid]); + if (query) {this._recomputeResults(query, qidToOriginalResults[qid]);} }); this._observeQueue.drain(); @@ -369,7 +351,6 @@ export class LocalCollection { // generate an id for it. let insertedId; if (updateCount === 0 && options.upsert) { - let selectorModifier = LocalCollection._selectorIsId(selector) ? { _id: selector } : selector; @@ -387,8 +368,7 @@ export class LocalCollection { LocalCollection._modify(newDoc, {$set: selectorModifier}); LocalCollection._modify(newDoc, mod, {isInsert: true}); - if (! newDoc._id && options.insertedId) - newDoc._id = options.insertedId; + if (! newDoc._id && options.insertedId) {newDoc._id = options.insertedId;} insertedId = this.insert(newDoc); updateCount = 1; } @@ -399,32 +379,32 @@ export class LocalCollection { let result; if (options._returnObject) { result = { - numberAffected: updateCount + numberAffected: updateCount, }; - if (insertedId !== undefined) - result.insertedId = insertedId; + if (insertedId !== undefined) {result.insertedId = insertedId;} } else { result = updateCount; } - if (callback) + if (callback) { Meteor.defer(() => { callback(null, result); }); + } return result; } // A convenience wrapper on update. LocalCollection.upsert(sel, mod) is // equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject: // true }). - upsert (selector, mod, options, callback) { - if (! callback && typeof options === "function") { + upsert(selector, mod, options, callback) { + if (! callback && typeof options === 'function') { callback = options; options = {}; } return this.update(selector, mod, Object.assign({}, options, { upsert: true, - _returnObject: true + _returnObject: true, }), callback); } @@ -432,7 +412,7 @@ export class LocalCollection { // f(doc, id) on each of them. Specifically, if selector specifies // specific _id's, it only looks at those. doc is *not* cloned: it is the // same object that is in _docs. - _eachPossiblyMatchingDoc (selector, f) { + _eachPossiblyMatchingDoc(selector, f) { const specificIds = LocalCollection._idsMatchedBySelector(selector); if (specificIds) { for (let i = 0; i < specificIds.length; ++i) { @@ -440,8 +420,7 @@ export class LocalCollection { const doc = this._docs.get(id); if (doc) { const breakIfFalse = f(doc, id); - if (breakIfFalse === false) - break; + if (breakIfFalse === false) {break;} } } } else { @@ -449,7 +428,7 @@ export class LocalCollection { } } - _modifyAndNotify (doc, mod, recomputeQids, arrayIndices) { + _modifyAndNotify(doc, mod, recomputeQids, arrayIndices) { const matched_before = {}; for (let qid in this.queries) { const query = this.queries[qid]; @@ -475,8 +454,7 @@ export class LocalCollection { const before = matched_before[qid]; const afterMatch = query.matcher.documentMatches(doc); const after = afterMatch.result; - if (after && query.distances && afterMatch.distance !== undefined) - query.distances.set(doc._id, afterMatch.distance); + if (after && query.distances && afterMatch.distance !== undefined) {query.distances.set(doc._id, afterMatch.distance);} if (query.cursor.skip || query.cursor.limit) { // We need to recompute any query where the doc may have been in the @@ -486,8 +464,7 @@ export class LocalCollection { // applied... but if they are false, then the document definitely is NOT // in the output. So it's safe to skip recompute if neither before or // after are true.) - if (before || after) - recomputeQids[qid] = true; + if (before || after) {recomputeQids[qid] = true;} } else if (before && !after) { LocalCollection._removeFromResults(query, doc); } else if (!before && after) { @@ -508,7 +485,7 @@ export class LocalCollection { // in an oldResults which was deep-copied before the modifier was applied. // // oldResults is guaranteed to be ignored if the query is not paused. - _recomputeResults (query, oldResults) { + _recomputeResults(query, oldResults) { if (this.paused) { // There's no reason to recompute the results now as we're still paused. // By flagging the query as "dirty", the recompute will be performed @@ -517,10 +494,8 @@ export class LocalCollection { return; } - if (! this.paused && ! oldResults) - oldResults = query.results; - if (query.distances) - query.distances.clear(); + if (! this.paused && ! oldResults) {oldResults = query.results;} + if (query.distances) {query.distances.clear();} query.results = query.cursor._getRawObjects({ ordered: query.ordered, distances: query.distances}); @@ -531,15 +506,13 @@ export class LocalCollection { } } - _saveOriginal (id, doc) { + _saveOriginal(id, doc) { // Are we even trying to save originals? - if (!this._savedOriginals) - return; + if (!this._savedOriginals) {return;} // Have we previously mutated the original (and so 'doc' is not actually // original)? (Note the 'has' check rather than truth: we store undefined // here for inserted docs!) - if (this._savedOriginals.has(id)) - return; + if (this._savedOriginals.has(id)) {return;} this._savedOriginals.set(id, EJSON.clone(doc)); } } @@ -558,17 +531,16 @@ LocalCollection.ObserveHandle = ObserveHandle; // invoked immediately before the docs field is updated; this object is made // available as `this` to those callbacks. LocalCollection._CachingChangeObserver = class _CachingChangeObserver { - constructor (options = {}) { + constructor(options = {}) { const orderedFromCallbacks = options.callbacks && LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); if (options.hasOwnProperty('ordered')) { this.ordered = options.ordered; - if (options.callbacks && options.ordered !== orderedFromCallbacks) - throw Error("ordered option doesn't match callbacks"); + if (options.callbacks && options.ordered !== orderedFromCallbacks) {throw Error("ordered option doesn't match callbacks");} } else if (options.callbacks) { this.ordered = orderedFromCallbacks; } else { - throw Error("must provide ordered or callbacks"); + throw Error('must provide ordered or callbacks'); } const callbacks = options.callbacks || {}; @@ -591,7 +563,7 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { const doc = this.docs.get(id); callbacks.movedBefore && callbacks.movedBefore.call(this, id, before); this.docs.moveBefore(id, before || null); - } + }, }; } else { this.docs = new LocalCollection._IdMap; @@ -601,7 +573,7 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { callbacks.added && callbacks.added.call(this, id, fields); doc._id = id; this.docs.set(id, doc); - } + }, }; } @@ -609,8 +581,7 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { // identical. this.applyChange.changed = (id, fields) => { const doc = this.docs.get(id); - if (!doc) - throw new Error(`Unknown id for changed: ${id}`); + if (!doc) {throw new Error(`Unknown id for changed: ${id}`);} callbacks.changed && callbacks.changed.call( this, id, EJSON.clone(fields)); DiffSequence.applyChanges(doc, fields); @@ -623,7 +594,7 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { }; LocalCollection._IdMap = class _IdMap extends IdMap { - constructor () { + constructor() { super(MongoID.idStringify, MongoID.idParse); } }; @@ -638,18 +609,16 @@ LocalCollection._IdMap = class _IdMap extends IdMap { // original _id field // - If the return value doesn't have an _id field, add it back. LocalCollection.wrapTransform = transform => { - if (! transform) - return null; + if (! transform) {return null;} // No need to doubly-wrap transforms. - if (transform.__wrappedTransform__) - return transform; + if (transform.__wrappedTransform__) {return transform;} const wrapped = doc => { if (!doc.hasOwnProperty('_id')) { // XXX do we ever have a transform on the oplog's collection? because that // collection has no _id. - throw new Error("can only transform documents with _id"); + throw new Error('can only transform documents with _id'); } const id = doc._id; @@ -657,7 +626,7 @@ LocalCollection.wrapTransform = transform => { const transformed = Tracker.nonreactive(() => transform(doc)); if (!LocalCollection._isPlainObject(transformed)) { - throw new Error("transform must return object"); + throw new Error('transform must return object'); } if (transformed.hasOwnProperty('_id')) { @@ -685,7 +654,7 @@ LocalCollection._binarySearch = (cmp, array, value) => { let first = 0, rangeLength = array.length; while (rangeLength > 0) { - const halfRange = Math.floor(rangeLength/2); + const halfRange = Math.floor(rangeLength / 2); if (cmp(value, array[first + halfRange]) >= 0) { first += halfRange + 1; rangeLength -= halfRange + 1; @@ -697,17 +666,13 @@ LocalCollection._binarySearch = (cmp, array, value) => { }; LocalCollection._checkSupportedProjection = fields => { - if (fields !== Object(fields) || Array.isArray(fields)) - throw MinimongoError("fields option must be an object"); + if (fields !== Object(fields) || Array.isArray(fields)) {throw MinimongoError('fields option must be an object');} Object.keys(fields).forEach(keyPath => { const val = fields[keyPath]; - if (keyPath.split('.').includes('$')) - throw MinimongoError("Minimongo doesn't support $ operator in projections yet."); - if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) - throw MinimongoError("Minimongo doesn't support operators in projections yet."); - if (![1, 0, true, false].includes(val)) - throw MinimongoError("Projection values should be one of 1, 0, true, or false"); + if (keyPath.split('.').includes('$')) {throw MinimongoError("Minimongo doesn't support $ operator in projections yet.");} + if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) {throw MinimongoError("Minimongo doesn't support operators in projections yet.");} + if (![1, 0, true, false].includes(val)) {throw MinimongoError('Projection values should be one of 1, 0, true, or false');} }); }; @@ -727,23 +692,17 @@ LocalCollection._compileProjection = fields => { // returns transformed doc according to ruleTree const transform = (doc, ruleTree) => { // Special case for "sets" - if (Array.isArray(doc)) - return doc.map(subdoc => transform(subdoc, ruleTree)); + if (Array.isArray(doc)) {return doc.map(subdoc => transform(subdoc, ruleTree));} const res = details.including ? {} : EJSON.clone(doc); Object.keys(ruleTree).forEach(key => { const rule = ruleTree[key]; - if (!doc.hasOwnProperty(key)) - return; + if (!doc.hasOwnProperty(key)) {return;} if (rule === Object(rule)) { // For sub-objects/subsets we branch - if (doc[key] === Object(doc[key])) - res[key] = transform(doc[key], rule); + if (doc[key] === Object(doc[key])) {res[key] = transform(doc[key], rule);} // Otherwise we don't even touch this subfield - } else if (details.including) - res[key] = EJSON.clone(doc[key]); - else - delete res[key]; + } else if (details.including) {res[key] = EJSON.clone(doc[key]);} else {delete res[key];} }); return res; @@ -752,10 +711,8 @@ LocalCollection._compileProjection = fields => { return obj => { const res = transform(obj, details.tree); - if (_idProjection && obj.hasOwnProperty('_id')) - res._id = obj._id; - if (!_idProjection && res.hasOwnProperty('_id')) - delete res._id; + if (_idProjection && obj.hasOwnProperty('_id')) {res._id = obj._id;} + if (!_idProjection && res.hasOwnProperty('_id')) {delete res._id;} return res; }; }; @@ -781,12 +738,11 @@ LocalCollection._diffQueryUnorderedChanges = (oldResults, newResults, observer, }; LocalCollection._findInOrderedResults = (query, doc) => { - if (!query.ordered) - throw new Error("Can't call _findInOrderedResults on unordered query"); - for (let i = 0; i < query.results.length; i++) - if (query.results[i] === doc) - return i; - throw Error("object missing from query"); + if (!query.ordered) {throw new Error("Can't call _findInOrderedResults on unordered query");} + for (let i = 0; i < query.results.length; i++) { + if (query.results[i] === doc) {return i;} + } + throw Error('object missing from query'); }; // If this is a selector which explicitly constrains the match by ID to a finite @@ -796,16 +752,13 @@ LocalCollection._findInOrderedResults = (query, doc) => { // access-controlled update and remove. LocalCollection._idsMatchedBySelector = selector => { // Is the selector just an ID? - if (LocalCollection._selectorIsId(selector)) - return [selector]; - if (!selector) - return null; + if (LocalCollection._selectorIsId(selector)) {return [selector];} + if (!selector) {return null;} // Do we have an _id clause? if (selector.hasOwnProperty('_id')) { // Is the _id clause just an ID? - if (LocalCollection._selectorIsId(selector._id)) - return [selector._id]; + if (LocalCollection._selectorIsId(selector._id)) {return [selector._id];} // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? if (selector._id && selector._id.$in && Array.isArray(selector._id.$in) @@ -822,8 +775,7 @@ LocalCollection._idsMatchedBySelector = selector => { if (selector.$and && Array.isArray(selector.$and)) { for (let i = 0; i < selector.$and.length; ++i) { const subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); - if (subIds) - return subIds; + if (subIds) {return subIds;} } } @@ -841,11 +793,8 @@ LocalCollection._insertInResults = (query, doc) => { const i = LocalCollection._insertInSortedList( query.sorter.getComparator({distances: query.distances}), query.results, doc); - let next = query.results[i+1]; - if (next) - next = next._id; - else - next = null; + let next = query.results[i + 1]; + if (next) {next = next._id;} else {next = null;} query.addedBefore(doc._id, query.projectionFn(fields), next); } query.added(doc._id, query.projectionFn(fields)); @@ -887,8 +836,7 @@ LocalCollection._isPlainObject = x => { // out when to set the fields in $setOnInsert, if present. LocalCollection._modify = (doc, mod, options) => { options = options || {}; - if (!LocalCollection._isPlainObject(mod)) - throw MinimongoError("Modifier must be an object"); + if (!LocalCollection._isPlainObject(mod)) {throw MinimongoError('Modifier must be an object');} // Make sure the caller can't mutate our data structures. mod = EJSON.clone(mod); @@ -898,8 +846,7 @@ LocalCollection._modify = (doc, mod, options) => { let newDoc; if (!isModifier) { - if (mod._id && !EJSON.equals(doc._id, mod._id)) - throw MinimongoError("Cannot change the _id of a document"); + if (mod._id && !EJSON.equals(doc._id, mod._id)) {throw MinimongoError('Cannot change the _id of a document');} // replace the whole document assertHasValidFieldNames(mod); @@ -912,18 +859,16 @@ LocalCollection._modify = (doc, mod, options) => { const operand = mod[op]; let modFunc = MODIFIERS[op]; // Treat $setOnInsert as $set if this is an insert. - if (options.isInsert && op === '$setOnInsert') - modFunc = MODIFIERS['$set']; - if (!modFunc) - throw MinimongoError(`Invalid modifier specified ${op}`); + if (options.isInsert && op === '$setOnInsert') {modFunc = MODIFIERS.$set;} + if (!modFunc) {throw MinimongoError(`Invalid modifier specified ${op}`);} Object.keys(operand).forEach(keypath => { const arg = operand[keypath]; if (keypath === '') { - throw MinimongoError("An empty update path is not valid."); + throw MinimongoError('An empty update path is not valid.'); } if (keypath === '_id' && op !== '$setOnInsert') { - throw MinimongoError("Mod on _id not allowed"); + throw MinimongoError('Mod on _id not allowed'); } const keyparts = keypath.split('.'); @@ -934,11 +879,11 @@ LocalCollection._modify = (doc, mod, options) => { } const noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); - const forbidArray = (op === "$rename"); + const forbidArray = op === '$rename'; const target = findModTarget(newDoc, keyparts, { noCreate: NO_CREATE_MODIFIERS[op], - forbidArray: (op === "$rename"), - arrayIndices: options.arrayIndices + forbidArray: op === '$rename', + arrayIndices: options.arrayIndices, }); const field = keyparts.pop(); modFunc(target, field, arg, keypath, newDoc); @@ -951,8 +896,7 @@ LocalCollection._modify = (doc, mod, options) => { // Note: this used to be for (var k in doc) however, this does not // work right in Opera. Deleting from a doc while iterating over it // would sometimes cause opera to skip some keys. - if (k !== '_id') - delete doc[k]; + if (k !== '_id') {delete doc[k];} }); Object.keys(newDoc).forEach(k => { doc[k] = newDoc[k]; @@ -971,24 +915,21 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { // relative order, transforms, and applyChanges -- without the speed hit. const indices = !observeCallbacks._no_indices; observeChangesCallbacks = { - addedBefore (id, fields, before) { - if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) - return; + addedBefore(id, fields, before) { + if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) {return;} const doc = transform(Object.assign(fields, {_id: id})); if (observeCallbacks.addedAt) { const index = indices - ? (before ? this.docs.indexOf(before) : this.docs.size()) : -1; + ? before ? this.docs.indexOf(before) : this.docs.size() : -1; observeCallbacks.addedAt(doc, index, before); } else { observeCallbacks.added(doc); } }, - changed (id, fields) { - if (!(observeCallbacks.changedAt || observeCallbacks.changed)) - return; + changed(id, fields) { + if (!(observeCallbacks.changedAt || observeCallbacks.changed)) {return;} let doc = EJSON.clone(this.docs.get(id)); - if (!doc) - throw new Error(`Unknown id for changed: ${id}`); + if (!doc) {throw new Error(`Unknown id for changed: ${id}`);} const oldDoc = transform(EJSON.clone(doc)); DiffSequence.applyChanges(doc, fields); doc = transform(doc); @@ -999,23 +940,20 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { observeCallbacks.changed(doc, oldDoc); } }, - movedBefore (id, before) { - if (!observeCallbacks.movedTo) - return; + movedBefore(id, before) { + if (!observeCallbacks.movedTo) {return;} const from = indices ? this.docs.indexOf(id) : -1; let to = indices - ? (before ? this.docs.indexOf(before) : this.docs.size()) : -1; + ? before ? this.docs.indexOf(before) : this.docs.size() : -1; // When not moving backwards, adjust for the fact that removing the // document slides everything back one slot. - if (to > from) - --to; + if (to > from) {--to;} observeCallbacks.movedTo(transform(EJSON.clone(this.docs.get(id))), - from, to, before || null); + from, to, before || null); }, - removed (id) { - if (!(observeCallbacks.removedAt || observeCallbacks.removed)) - return; + removed(id) { + if (!(observeCallbacks.removedAt || observeCallbacks.removed)) {return;} // technically maybe there should be an EJSON.clone here, but it's about // to be removed from this.docs! const doc = transform(this.docs.get(id)); @@ -1025,30 +963,30 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { } else { observeCallbacks.removed(doc); } - } + }, }; } else { observeChangesCallbacks = { - added (id, fields) { + added(id, fields) { if (!suppressed && observeCallbacks.added) { const doc = Object.assign(fields, {_id: id}); observeCallbacks.added(transform(doc)); } }, - changed (id, fields) { + changed(id, fields) { if (observeCallbacks.changed) { const oldDoc = this.docs.get(id); const doc = EJSON.clone(oldDoc); DiffSequence.applyChanges(doc, fields); observeCallbacks.changed(transform(doc), - transform(EJSON.clone(oldDoc))); + transform(EJSON.clone(oldDoc))); } }, - removed (id) { + removed(id) { if (observeCallbacks.removed) { observeCallbacks.removed(transform(this.docs.get(id))); } - } + }, }; } @@ -1061,20 +999,16 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { }; LocalCollection._observeCallbacksAreOrdered = callbacks => { - if (callbacks.addedAt && callbacks.added) - throw new Error("Please specify only one of added() and addedAt()"); - if (callbacks.changedAt && callbacks.changed) - throw new Error("Please specify only one of changed() and changedAt()"); - if (callbacks.removed && callbacks.removedAt) - throw new Error("Please specify only one of removed() and removedAt()"); + if (callbacks.addedAt && callbacks.added) {throw new Error('Please specify only one of added() and addedAt()');} + if (callbacks.changedAt && callbacks.changed) {throw new Error('Please specify only one of changed() and changedAt()');} + if (callbacks.removed && callbacks.removedAt) {throw new Error('Please specify only one of removed() and removedAt()');} return !!(callbacks.addedAt || callbacks.movedTo || callbacks.changedAt || callbacks.removedAt); }; LocalCollection._observeChangesCallbacksAreOrdered = callbacks => { - if (callbacks.added && callbacks.addedBefore) - throw new Error("Please specify only one of added() and addedBefore()"); + if (callbacks.added && callbacks.addedBefore) {throw new Error('Please specify only one of added() and addedBefore()');} return !!(callbacks.addedBefore || callbacks.movedBefore); }; @@ -1117,22 +1051,21 @@ LocalCollection._removeFromResults = (query, doc) => { // Is this selector just shorthand for lookup by _id? LocalCollection._selectorIsId = selector => { - return (typeof selector === "string") || - (typeof selector === "number") || + return typeof selector === 'string' || + typeof selector === 'number' || selector instanceof MongoID.ObjectID; }; // Is the selector just lookup by _id (shorthand or not)? LocalCollection._selectorIsIdPerhapsAsObject = selector => { return LocalCollection._selectorIsId(selector) || - (selector && typeof selector === "object" && + selector && typeof selector === 'object' && selector._id && LocalCollection._selectorIsId(selector._id) && - Object.keys(selector).length === 1); + Object.keys(selector).length === 1; }; LocalCollection._updateInResults = (query, doc, old_doc) => { - if (!EJSON.equals(doc._id, old_doc._id)) - throw new Error("Can't change a doc's _id while updating"); + if (!EJSON.equals(doc._id, old_doc._id)) {throw new Error("Can't change a doc's _id while updating");} const projectionFn = query.projectionFn; const changedFields = DiffSequence.makeChangedFields( projectionFn(doc), projectionFn(old_doc)); @@ -1147,10 +1080,8 @@ LocalCollection._updateInResults = (query, doc, old_doc) => { const orig_idx = LocalCollection._findInOrderedResults(query, doc); - if (Object.keys(changedFields).length) - query.changed(doc._id, changedFields); - if (!query.sorter) - return; + if (Object.keys(changedFields).length) {query.changed(doc._id, changedFields);} + if (!query.sorter) {return;} // just take it out and put it back in again, and see if the index // changes @@ -1159,37 +1090,34 @@ LocalCollection._updateInResults = (query, doc, old_doc) => { query.sorter.getComparator({distances: query.distances}), query.results, doc); if (orig_idx !== new_idx) { - let next = query.results[new_idx+1]; - if (next) - next = next._id; - else - next = null; + let next = query.results[new_idx + 1]; + if (next) {next = next._id;} else {next = null;} query.movedBefore && query.movedBefore(doc._id, next); } }; const MODIFIERS = { $currentDate(target, field, arg) { - if (typeof arg === "object" && arg.hasOwnProperty("$type")) { - if (arg.$type !== "date") { - throw MinimongoError( - "Minimongo does currently only support the date type " + - "in $currentDate modifiers", - { field }); - } + if (typeof arg === 'object' && arg.hasOwnProperty('$type')) { + if (arg.$type !== 'date') { + throw MinimongoError( + 'Minimongo does currently only support the date type ' + + 'in $currentDate modifiers', + { field }); + } } else if (arg !== true) { - throw MinimongoError("Invalid $currentDate modifier", { field }); + throw MinimongoError('Invalid $currentDate modifier', { field }); } target[field] = new Date(); }, $min(target, field, arg) { - if (typeof arg !== "number") { - throw MinimongoError("Modifier $min allowed for numbers only", { field }); + if (typeof arg !== 'number') { + throw MinimongoError('Modifier $min allowed for numbers only', { field }); } if (field in target) { - if (typeof target[field] !== "number") { + if (typeof target[field] !== 'number') { throw MinimongoError( - "Cannot apply $min modifier to non-number", { field }); + 'Cannot apply $min modifier to non-number', { field }); } if (target[field] > arg) { target[field] = arg; @@ -1199,28 +1127,28 @@ const MODIFIERS = { } }, $max(target, field, arg) { - if (typeof arg !== "number") { - throw MinimongoError("Modifier $max allowed for numbers only", { field }); + if (typeof arg !== 'number') { + throw MinimongoError('Modifier $max allowed for numbers only', { field }); } if (field in target) { - if (typeof target[field] !== "number") { + if (typeof target[field] !== 'number') { throw MinimongoError( - "Cannot apply $max modifier to non-number", { field }); + 'Cannot apply $max modifier to non-number', { field }); } if (target[field] < arg) { - target[field] = arg; + target[field] = arg; } } else { target[field] = arg; } }, $inc(target, field, arg) { - if (typeof arg !== "number") - throw MinimongoError("Modifier $inc allowed for numbers only", { field }); + if (typeof arg !== 'number') {throw MinimongoError('Modifier $inc allowed for numbers only', { field });} if (field in target) { - if (typeof target[field] !== "number") + if (typeof target[field] !== 'number') { throw MinimongoError( - "Cannot apply $inc modifier to non-number", { field }); + 'Cannot apply $inc modifier to non-number', { field }); + } target[field] += arg; } else { target[field] = arg; @@ -1229,12 +1157,12 @@ const MODIFIERS = { $set(target, field, arg) { if (target !== Object(target)) { // not an array or an object const e = MinimongoError( - "Cannot set property on non-object field", { field }); + 'Cannot set property on non-object field', { field }); e.setPropertyError = true; throw e; } if (target === null) { - const e = MinimongoError("Cannot set property on null", { field }); + const e = MinimongoError('Cannot set property on null', { field }); e.setPropertyError = true; throw e; } @@ -1247,18 +1175,16 @@ const MODIFIERS = { $unset(target, field, arg) { if (target !== undefined) { if (target instanceof Array) { - if (field in target) - target[field] = null; - } else - delete target[field]; + if (field in target) {target[field] = null;} + } else {delete target[field];} } }, $push(target, field, arg) { - if (target[field] === undefined) - target[field] = []; - if (!(target[field] instanceof Array)) + if (target[field] === undefined) {target[field] = [];} + if (!(target[field] instanceof Array)) { throw MinimongoError( - "Cannot apply $push modifier to non-array", { field }); + 'Cannot apply $push modifier to non-array', { field }); + } if (!(arg && arg.$each)) { // Simple mode: not $each @@ -1269,27 +1195,25 @@ const MODIFIERS = { // Fancy mode: $each (and maybe $slice and $sort and $position) const toPush = arg.$each; - if (!(toPush instanceof Array)) - throw MinimongoError("$each must be an array", { field }); + if (!(toPush instanceof Array)) {throw MinimongoError('$each must be an array', { field });} assertHasValidFieldNames(toPush); // Parse $position let position = undefined; if ('$position' in arg) { - if (typeof arg.$position !== "number") - throw MinimongoError("$position must be a numeric value", { field }); + if (typeof arg.$position !== 'number') {throw MinimongoError('$position must be a numeric value', { field });} // XXX should check to make sure integer - if (arg.$position < 0) + if (arg.$position < 0) { throw MinimongoError( - "$position in $push must be zero or positive", { field }); + '$position in $push must be zero or positive', { field }); + } position = arg.$position; } // Parse $slice. let slice = undefined; if ('$slice' in arg) { - if (typeof arg.$slice !== "number") - throw MinimongoError("$slice must be a numeric value", { field }); + if (typeof arg.$slice !== 'number') {throw MinimongoError('$slice must be a numeric value', { field });} // XXX should check to make sure integer slice = arg.$slice; } @@ -1297,8 +1221,7 @@ const MODIFIERS = { // Parse $sort. let sortFunction = undefined; if (arg.$sort) { - if (slice === undefined) - throw MinimongoError("$sort requires $slice to be present", { field }); + if (slice === undefined) {throw MinimongoError('$sort requires $slice to be present', { field });} // XXX this allows us to use a $sort whose value is an array, but that's // actually an extension of the Node driver, so it won't work // server-side. Could be confusing! @@ -1306,106 +1229,84 @@ const MODIFIERS = { sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); for (let i = 0; i < toPush.length; i++) { if (LocalCollection._f._type(toPush[i]) !== 3) { - throw MinimongoError("$push like modifiers using $sort " + - "require all elements to be objects", { field }); + throw MinimongoError('$push like modifiers using $sort ' + + 'require all elements to be objects', { field }); } } } // Actually push. if (position === undefined) { - for (let j = 0; j < toPush.length; j++) - target[field].push(toPush[j]); + for (let j = 0; j < toPush.length; j++) {target[field].push(toPush[j]);} } else { const spliceArguments = [position, 0]; - for (let j = 0; j < toPush.length; j++) - spliceArguments.push(toPush[j]); + for (let j = 0; j < toPush.length; j++) {spliceArguments.push(toPush[j]);} Array.prototype.splice.apply(target[field], spliceArguments); } // Actually sort. - if (sortFunction) - target[field].sort(sortFunction); + if (sortFunction) {target[field].sort(sortFunction);} // Actually slice. if (slice !== undefined) { - if (slice === 0) - target[field] = []; // differs from Array.slice! - else if (slice < 0) - target[field] = target[field].slice(slice); - else - target[field] = target[field].slice(0, slice); + if (slice === 0) {target[field] = [];} // differs from Array.slice! + else if (slice < 0) {target[field] = target[field].slice(slice);} else {target[field] = target[field].slice(0, slice);} } }, $pushAll(target, field, arg) { - if (!(typeof arg === "object" && arg instanceof Array)) - throw MinimongoError("Modifier $pushAll/pullAll allowed for arrays only"); + if (!(typeof arg === 'object' && arg instanceof Array)) {throw MinimongoError('Modifier $pushAll/pullAll allowed for arrays only');} assertHasValidFieldNames(arg); const x = target[field]; - if (x === undefined) - target[field] = arg; - else if (!(x instanceof Array)) + if (x === undefined) {target[field] = arg;} else if (!(x instanceof Array)) { throw MinimongoError( - "Cannot apply $pushAll modifier to non-array", { field }); - else { - for (let i = 0; i < arg.length; i++) - x.push(arg[i]); + 'Cannot apply $pushAll modifier to non-array', { field }); + } else { + for (let i = 0; i < arg.length; i++) {x.push(arg[i]);} } }, $addToSet(target, field, arg) { let isEach = false; - if (typeof arg === "object") { - //check if first key is '$each' + if (typeof arg === 'object') { + // check if first key is '$each' const keys = Object.keys(arg); - if (keys[0] === "$each"){ + if (keys[0] === '$each') { isEach = true; } } - const values = isEach ? arg["$each"] : [arg]; + const values = isEach ? arg.$each : [arg]; assertHasValidFieldNames(values); const x = target[field]; - if (x === undefined) - target[field] = values; - else if (!(x instanceof Array)) + if (x === undefined) {target[field] = values;} else if (!(x instanceof Array)) { throw MinimongoError( - "Cannot apply $addToSet modifier to non-array", { field }); - else { + 'Cannot apply $addToSet modifier to non-array', { field }); + } else { values.forEach(value => { - for (let i = 0; i < x.length; i++) - if (LocalCollection._f._equal(value, x[i])) - return; + for (let i = 0; i < x.length; i++) { + if (LocalCollection._f._equal(value, x[i])) {return;} + } x.push(value); }); } }, $pop(target, field, arg) { - if (target === undefined) - return; + if (target === undefined) {return;} const x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) + if (x === undefined) {return;} else if (!(x instanceof Array)) { throw MinimongoError( - "Cannot apply $pop modifier to non-array", { field }); - else { - if (typeof arg === 'number' && arg < 0) - x.splice(0, 1); - else - x.pop(); + 'Cannot apply $pop modifier to non-array', { field }); + } else { + if (typeof arg === 'number' && arg < 0) {x.splice(0, 1);} else {x.pop();} } }, $pull(target, field, arg) { - if (target === undefined) - return; + if (target === undefined) {return;} const x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) + if (x === undefined) {return;} else if (!(x instanceof Array)) { throw MinimongoError( - "Cannot apply $pull/pullAll modifier to non-array", { field }); - else { + 'Cannot apply $pull/pullAll modifier to non-array', { field }); + } else { const out = []; - if (arg != null && typeof arg === "object" && !(arg instanceof Array)) { + if (arg != null && typeof arg === 'object' && !(arg instanceof Array)) { // XXX would be much nicer to compile this once, rather than // for each document we modify.. but usually we're not // modifying that many documents, so we'll let it slide for @@ -1416,30 +1317,28 @@ const MODIFIERS = { // like {$gt: 4} is not normally a complete selector. // same issue as $elemMatch possibly? const matcher = new Minimongo.Matcher(arg); - for (let i = 0; i < x.length; i++) - if (!matcher.documentMatches(x[i]).result) - out.push(x[i]); + for (let i = 0; i < x.length; i++) { + if (!matcher.documentMatches(x[i]).result) {out.push(x[i]);} + } } else { - for (let i = 0; i < x.length; i++) - if (!LocalCollection._f._equal(x[i], arg)) - out.push(x[i]); + for (let i = 0; i < x.length; i++) { + if (!LocalCollection._f._equal(x[i], arg)) {out.push(x[i]);} + } } target[field] = out; } }, $pullAll(target, field, arg) { - if (!(typeof arg === "object" && arg instanceof Array)) + if (!(typeof arg === 'object' && arg instanceof Array)) { throw MinimongoError( - "Modifier $pushAll/pullAll allowed for arrays only", { field }); - if (target === undefined) - return; + 'Modifier $pushAll/pullAll allowed for arrays only', { field }); + } + if (target === undefined) {return;} const x = target[field]; - if (x === undefined) - return; - else if (!(x instanceof Array)) + if (x === undefined) {return;} else if (!(x instanceof Array)) { throw MinimongoError( - "Cannot apply $pull/pullAll modifier to non-array", { field }); - else { + 'Cannot apply $pull/pullAll modifier to non-array', { field }); + } else { const out = []; for (let i = 0; i < x.length; i++) { let exclude = false; @@ -1449,20 +1348,17 @@ const MODIFIERS = { break; } } - if (!exclude) - out.push(x[i]); + if (!exclude) {out.push(x[i]);} } target[field] = out; } }, $rename(target, field, arg, keypath, doc) { if (keypath === arg) - // no idea why mongo has this restriction.. - throw MinimongoError("$rename source must differ from target", { field }); - if (target === null) - throw MinimongoError("$rename source field invalid", { field }); - if (typeof arg !== "string") - throw MinimongoError("$rename target must be a string", { field }); + // no idea why mongo has this restriction.. + {throw MinimongoError('$rename source must differ from target', { field });} + if (target === null) {throw MinimongoError('$rename source field invalid', { field });} + if (typeof arg !== 'string') {throw MinimongoError('$rename target must be a string', { field });} if (arg.includes('\0')) { // Null bytes are not allowed in Mongo field names // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names @@ -1470,23 +1366,21 @@ const MODIFIERS = { "The 'to' field for $rename cannot contain an embedded null byte", { field }); } - if (target === undefined) - return; + if (target === undefined) {return;} const v = target[field]; delete target[field]; const keyparts = arg.split('.'); const target2 = findModTarget(doc, keyparts, {forbidArray: true}); - if (target2 === null) - throw MinimongoError("$rename target field invalid", { field }); + if (target2 === null) {throw MinimongoError('$rename target field invalid', { field });} const field2 = keyparts.pop(); target2[field2] = v; }, $bit(target, field, arg) { // XXX mongo only supports $bit on integers, and we only support // native javascript numbers (doubles) so far, so we can't support $bit - throw MinimongoError("$bit is not supported", { field }); - } + throw MinimongoError('$bit is not supported', { field }); + }, }; const NO_CREATE_MODIFIERS = { @@ -1494,7 +1388,7 @@ const NO_CREATE_MODIFIERS = { $pop: true, $rename: true, $pull: true, - $pullAll: true + $pullAll: true, }; // Make sure field names do not contain Mongo restricted @@ -1502,13 +1396,13 @@ const NO_CREATE_MODIFIERS = { // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names const invalidCharMsg = { '.': "contain '.'", - '$': "start with '$'", - '\0': "contain null bytes", + $: "start with '$'", + '\0': 'contain null bytes', }; // checks if all field names in an object are valid -function assertHasValidFieldNames (doc){ - if (doc && typeof doc === "object") { +function assertHasValidFieldNames(doc) { + if (doc && typeof doc === 'object') { JSON.stringify(doc, (key, value) => { assertIsValidFieldName(key); return value; @@ -1516,7 +1410,7 @@ function assertHasValidFieldNames (doc){ } } -function assertIsValidFieldName (key) { +function assertIsValidFieldName(key) { let match; if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); @@ -1543,69 +1437,57 @@ function assertIsValidFieldName (key) { function findModTarget(doc, keyparts, options = {}) { let usedArrayIndex = false; for (let i = 0; i < keyparts.length; i++) { - const last = (i === keyparts.length - 1); + const last = i === keyparts.length - 1; let keypart = keyparts[i]; const indexable = isIndexable(doc); if (!indexable) { - if (options.noCreate) - return undefined; + if (options.noCreate) {return undefined;} const e = MinimongoError( `cannot use the part '${keypart}' to traverse ${doc}`); e.setPropertyError = true; throw e; } if (doc instanceof Array) { - if (options.forbidArray) - return null; + if (options.forbidArray) {return null;} if (keypart === '$') { - if (usedArrayIndex) - throw MinimongoError("Too many positional (i.e. '$') elements"); + if (usedArrayIndex) {throw MinimongoError("Too many positional (i.e. '$') elements");} if (!options.arrayIndices || !options.arrayIndices.length) { - throw MinimongoError("The positional operator did not find the " + - "match needed from the query"); + throw MinimongoError('The positional operator did not find the ' + + 'match needed from the query'); } keypart = options.arrayIndices[0]; usedArrayIndex = true; } else if (isNumericKey(keypart)) { keypart = parseInt(keypart); } else { - if (options.noCreate) - return undefined; + if (options.noCreate) {return undefined;} throw MinimongoError( `can't append to array using string field name [${keypart}]`); } if (last) - // handle 'a.01' - keyparts[i] = keypart; - if (options.noCreate && keypart >= doc.length) - return undefined; - while (doc.length < keypart) - doc.push(null); + // handle 'a.01' + {keyparts[i] = keypart;} + if (options.noCreate && keypart >= doc.length) {return undefined;} + while (doc.length < keypart) {doc.push(null);} if (!last) { - if (doc.length === keypart) - doc.push({}); - else if (typeof doc[keypart] !== "object") - throw MinimongoError(`can't modify field '${keyparts[i + 1]}' of list value ${JSON.stringify(doc[keypart])}`); + if (doc.length === keypart) {doc.push({});} else if (typeof doc[keypart] !== 'object') {throw MinimongoError(`can't modify field '${keyparts[i + 1]}' of list value ${JSON.stringify(doc[keypart])}`);} } } else { assertIsValidFieldName(keypart); if (!(keypart in doc)) { - if (options.noCreate) - return undefined; - if (!last) - doc[keypart] = {}; + if (options.noCreate) {return undefined;} + if (!last) {doc[keypart] = {};} } } - if (last) - return doc; + if (last) {return doc;} doc = doc[keypart]; } // notreached } -function objectOnlyHasDollarKeys (object) { +function objectOnlyHasDollarKeys(object) { const keys = Object.keys(object); return keys.length > 0 && keys.every(key => key.charAt(0) === '$'); } diff --git a/packages/minimongo/main_server.js b/packages/minimongo/main_server.js index 7b3fb9992c..926e5a155e 100644 --- a/packages/minimongo/main_server.js +++ b/packages/minimongo/main_server.js @@ -3,10 +3,10 @@ import { isNumericKey, isOperatorObject, pathsToTree, - projectionDetails + projectionDetails, } from './common.js'; -Minimongo._pathsElidingNumericKeys = function (paths) { +Minimongo._pathsElidingNumericKeys = function(paths) { return paths.map(path => path.split('.').filter(part => !isNumericKey(part)).join('.')); }; @@ -18,7 +18,7 @@ Minimongo._pathsElidingNumericKeys = function (paths) { // - 'foo.bar': 42 // - $unset // - 'abc.d': 1 -Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { +Minimongo.Matcher.prototype.affectedByModifier = function(modifier) { // safe check for $set/$unset being objects modifier = Object.assign({ $set: {}, $unset: {} }, modifier); const modifiedPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); @@ -34,19 +34,13 @@ Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { if (isNumericKey(sel[i]) && isNumericKey(mod[j])) { // foo.4.bar selector affected by foo.4 modifier // foo.3.bar selector unaffected by foo.4 modifier - if (sel[i] === mod[j]) - i++, j++; - else - return false; + if (sel[i] === mod[j]) {i++, j++;} else {return false;} } else if (isNumericKey(sel[i])) { // foo.4.bar selector unaffected by foo.bar modifier return false; } else if (isNumericKey(mod[j])) { j++; - } else if (sel[i] === mod[j]) - i++, j++; - else - return false; + } else if (sel[i] === mod[j]) {i++, j++;} else {return false;} } // One is a prefix of another, taking numeric fields into account @@ -63,19 +57,16 @@ Minimongo.Matcher.prototype.affectedByModifier = function (modifier) { // before, so if modifier can't convince selector in a positive change it would // stay 'false'. // Currently doesn't support $-operators and numeric indices precisely. -Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { - if (!this.affectedByModifier(modifier)) - return false; +Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { + if (!this.affectedByModifier(modifier)) {return false;} - modifier = Object.assign({$set:{}, $unset:{}}, modifier); + modifier = Object.assign({$set: {}, $unset: {}}, modifier); const modifierPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); - if (!this.isSimple()) - return true; + if (!this.isSimple()) {return true;} if (this._getPaths().some(pathHasNumericKeys) || - modifierPaths.some(pathHasNumericKeys)) - return true; + modifierPaths.some(pathHasNumericKeys)) {return true;} // check if there is a $set or $unset that indicates something is an // object rather than a scalar in the actual object where we saw $-operator @@ -84,13 +75,11 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // definitely set the result to false as 'a.b' appears to be an object. const expectedScalarIsObject = Object.keys(this._selector).some(path => { const sel = this._selector[path]; - if (! isOperatorObject(sel)) - return false; + if (! isOperatorObject(sel)) {return false;} return modifierPaths.some(modifierPath => startsWith(modifierPath, `${path}.`)); }); - if (expectedScalarIsObject) - return false; + if (expectedScalarIsObject) {return false;} // See if we can apply the modifier on the ideally matching object. If it // still matches the selector, then the modifier could have turned the real @@ -98,8 +87,7 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { const matchingDocument = EJSON.clone(this.matchingDocument()); // The selector is too complex, anything can happen. - if (matchingDocument === null) - return true; + if (matchingDocument === null) {return true;} try { LocalCollection._modify(matchingDocument, modifier); @@ -114,8 +102,7 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // We don't know what real document was like but from the error raised by // $set on a scalar field we can reason that the structure of real document // is completely different. - if (e.name === "MinimongoError" && e.setPropertyError) - return false; + if (e.name === 'MinimongoError' && e.setPropertyError) {return false;} throw e; } @@ -125,15 +112,14 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) { // Knows how to combine a mongo selector and a fields projection to a new fields // projection taking into account active fields from the passed selector. // @returns Object - projection object (same as fields option of mongo cursor) -Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { +Minimongo.Matcher.prototype.combineIntoProjection = function(projection) { const selectorPaths = Minimongo._pathsElidingNumericKeys(this._getPaths()); // Special case for $where operator in the selector - projection should depend // on all fields of the document. getSelectorPaths returns a list of paths // selector depends on. If one of the paths is '' (empty string) representing // the root or the whole document, complete projection should be returned. - if (selectorPaths.includes('')) - return {}; + if (selectorPaths.includes('')) {return {};} return combineImportantPathsIntoProjection(selectorPaths, projection); }; @@ -142,10 +128,9 @@ Minimongo.Matcher.prototype.combineIntoProjection = function (projection) { // selector is too complex for us to analyze // { 'a.b': { ans: 42 }, 'foo.bar': null, 'foo.baz': "something" } // => { a: { b: { ans: 42 } }, foo: { bar: null, baz: "something" } } -Minimongo.Matcher.prototype.matchingDocument = function () { +Minimongo.Matcher.prototype.matchingDocument = function() { // check if it was computed before - if (this._matchingDocument !== undefined) - return this._matchingDocument; + if (this._matchingDocument !== undefined) {return this._matchingDocument;} // If the analysis of this selector is too hard for our implementation // fallback to "YES" @@ -169,19 +154,16 @@ Minimongo.Matcher.prototype.matchingDocument = function () { } else if (onlyContainsKeys(valueSelector, ['$gt', '$gte', '$lt', '$lte'])) { let lowerBound = -Infinity, upperBound = Infinity; ['$lte', '$lt'].forEach(op => { - if (valueSelector.hasOwnProperty(op) && valueSelector[op] < upperBound) - upperBound = valueSelector[op]; + if (valueSelector.hasOwnProperty(op) && valueSelector[op] < upperBound) {upperBound = valueSelector[op];} }); ['$gte', '$gt'].forEach(op => { - if (valueSelector.hasOwnProperty(op) && valueSelector[op] > lowerBound) - lowerBound = valueSelector[op]; + if (valueSelector.hasOwnProperty(op) && valueSelector[op] > lowerBound) {lowerBound = valueSelector[op];} }); const middle = (lowerBound + upperBound) / 2; const matcher = new Minimongo.Matcher({ placeholder: valueSelector }); if (!matcher.documentMatches({ placeholder: middle }).result && - (middle === lowerBound || middle === upperBound)) - fallback = true; + (middle === lowerBound || middle === upperBound)) {fallback = true;} return middle; } else if (onlyContainsKeys(valueSelector, ['$nin', '$ne'])) { @@ -189,84 +171,78 @@ Minimongo.Matcher.prototype.matchingDocument = function () { // objects or arrays, we can confidently return an empty object as it // never matches any scalar. return {}; - } else { - fallback = true; } + fallback = true; } return this._selector[path]; }, x => x); - if (fallback) - this._matchingDocument = null; + if (fallback) {this._matchingDocument = null;} return this._matchingDocument; }; // Minimongo.Sorter gets a similar method, which delegates to a Matcher it made // for this exact purpose. -Minimongo.Sorter.prototype.affectedByModifier = function (modifier) { +Minimongo.Sorter.prototype.affectedByModifier = function(modifier) { return this._selectorForAffectedByModifier.affectedByModifier(modifier); }; -Minimongo.Sorter.prototype.combineIntoProjection = function (projection) { +Minimongo.Sorter.prototype.combineIntoProjection = function(projection) { const specPaths = Minimongo._pathsElidingNumericKeys(this._getPaths()); return combineImportantPathsIntoProjection(specPaths, projection); }; -function combineImportantPathsIntoProjection (paths, projection) { +function combineImportantPathsIntoProjection(paths, projection) { const prjDetails = projectionDetails(projection); let tree = prjDetails.tree; let mergedProjection = {}; // merge the paths to include tree = pathsToTree(paths, - path => true, - (node, path, fullPath) => true, - tree); + path => true, + (node, path, fullPath) => true, + tree); mergedProjection = treeToPaths(tree); if (prjDetails.including) { // both selector and projection are pointing on fields to include // so we can just return the merged tree return mergedProjection; - } else { - // selector is pointing at fields to include - // projection is pointing at fields to exclude - // make sure we don't exclude important paths - const mergedExclProjection = {}; - Object.keys(mergedProjection).forEach(path => { - const incl = mergedProjection[path]; - if (!incl) - mergedExclProjection[path] = false; - }); - - return mergedExclProjection; } + // selector is pointing at fields to include + // projection is pointing at fields to exclude + // make sure we don't exclude important paths + const mergedExclProjection = {}; + Object.keys(mergedProjection).forEach(path => { + const incl = mergedProjection[path]; + if (!incl) {mergedExclProjection[path] = false;} + }); + + return mergedExclProjection; } -function getPaths (sel) { +function getPaths(sel) { return Object.keys(new Minimongo.Matcher(sel)._paths); return Object.keys(sel).map(k => { const v = sel[k]; // we don't know how to handle $where because it can be anything - if (k === "$where") - return ''; // matches everything + if (k === '$where') {return '';} // matches everything // we branch from $or/$and/$nor operator - if (['$or', '$and', '$nor'].includes(k)) - return v.map(getPaths); + if (['$or', '$and', '$nor'].includes(k)) {return v.map(getPaths);} // the value is a literal or some comparison operator return k; }) - .reduce((a, b) => a.concat(b), []) - .filter((a, b, c) => c.indexOf(a) === b); + .reduce((a, b) => a.concat(b), []) + .filter((a, b, c) => c.indexOf(a) === b); } // A helper to ensure object has only certain keys -function onlyContainsKeys (obj, keys) { +function onlyContainsKeys(obj, keys) { return Object.keys(obj).every(k => keys.includes(k)); } -function pathHasNumericKeys (path) { +function pathHasNumericKeys(path) { return path.split('.').some(isNumericKey); } @@ -283,10 +259,7 @@ function treeToPaths(tree, prefix = '') { Object.keys(tree).forEach(key => { const val = tree[key]; - if (val === Object(val)) - Object.assign(result, treeToPaths(val, `${prefix + key}.`)); - else - result[prefix + key] = val; + if (val === Object(val)) {Object.assign(result, treeToPaths(val, `${prefix + key}.`));} else {result[prefix + key] = val;} }); return result; diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index 699b2c7ddc..6b64c032d7 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -1,7 +1,7 @@ import {LocalCollection} from './local_collection.js'; import { compileDocumentSelector, - nothingMatcher + nothingMatcher, } from './common.js'; // The minimongo selector compiler! @@ -25,7 +25,7 @@ import { // var matcher = new Minimongo.Matcher({a: {$gt: 5}}); // if (matcher.documentMatches({a: 7})) ... export class Matcher { - constructor (selector, isUpdate) { + constructor(selector, isUpdate) { // A set (object mapping string -> *) of all of the document paths looked // at by the selector. Also includes the empty string if it may look at any // path (eg, $where). @@ -53,28 +53,27 @@ export class Matcher { this._isUpdate = isUpdate; } - documentMatches (doc) { - if (doc !== Object(doc)) - throw Error('documentMatches needs a document'); + documentMatches(doc) { + if (doc !== Object(doc)) {throw Error('documentMatches needs a document');} return this._docMatcher(doc); } - hasGeoQuery () { + hasGeoQuery() { return this._hasGeoQuery; } - hasWhere () { + hasWhere() { return this._hasWhere; } - isSimple () { + isSimple() { return this._isSimple; } // Given a selector, return a function that takes one argument, a // document. It returns a result object. - _compileSelector (selector) { + _compileSelector(selector) { // you can pass a literal function instead of a selector if (selector instanceof Function) { this._isSimple = false; @@ -93,7 +92,7 @@ export class Matcher { // protect against dangerous selectors. falsey and {_id: falsey} are both // likely programmer error, and not what you want, particularly for // destructive operations. - if (!selector || (selector.hasOwnProperty('_id') && !selector._id)) { + if (!selector || selector.hasOwnProperty('_id') && !selector._id) { this._isSimple = false; return nothingMatcher; } @@ -101,8 +100,7 @@ export class Matcher { // Top level can't be an array or true or binary. if (Array.isArray(selector) || EJSON.isBinary(selector) || - typeof selector === 'boolean') - throw new Error(`Invalid selector: ${selector}`); + typeof selector === 'boolean') {throw new Error(`Invalid selector: ${selector}`);} this._selector = EJSON.clone(selector); @@ -111,11 +109,11 @@ export class Matcher { // Returns a list of key paths the given selector is looking for. It includes // the empty string if there is a $where. - _getPaths () { + _getPaths() { return Object.keys(this._paths); } - _recordPathUsed (path) { + _recordPathUsed(path) { this._paths[path] = true; } } @@ -125,27 +123,18 @@ LocalCollection._f = { // XXX for _all and _in, consider building 'inquery' at compile time.. _type(v) { - if (typeof v === "number") - return 1; - if (typeof v === "string") - return 2; - if (typeof v === "boolean") - return 8; - if (Array.isArray(v)) - return 4; - if (v === null) - return 10; + if (typeof v === 'number') {return 1;} + if (typeof v === 'string') {return 2;} + if (typeof v === 'boolean') {return 8;} + if (Array.isArray(v)) {return 4;} + if (v === null) {return 10;} if (v instanceof RegExp) - // note that typeof(/x/) === "object" - return 11; - if (typeof v === "function") - return 13; - if (v instanceof Date) - return 9; - if (EJSON.isBinary(v)) - return 5; - if (v instanceof MongoID.ObjectID) - return 7; + // note that typeof(/x/) === "object" + {return 11;} + if (typeof v === 'function') {return 13;} + if (v instanceof Date) {return 9;} + if (EJSON.isBinary(v)) {return 5;} + if (v instanceof MongoID.ObjectID) {return 7;} return 3; // object // XXX support some/all of these: @@ -170,25 +159,25 @@ LocalCollection._f = { // ('100' in the matrix below) // XXX minkey/maxkey return [-1, // (not a type) - 1, // number - 2, // string - 3, // object - 4, // array - 5, // binary - -1, // deprecated - 6, // ObjectID - 7, // bool - 8, // Date - 0, // null - 9, // RegExp - -1, // deprecated - 100, // JS code - 2, // deprecated (symbol) - 100, // JS code - 1, // 32-bit int - 8, // Mongo timestamp - 1 // 64-bit int - ][t]; + 1, // number + 2, // string + 3, // object + 4, // array + 5, // binary + -1, // deprecated + 6, // ObjectID + 7, // bool + 8, // Date + 0, // null + 9, // RegExp + -1, // deprecated + 100, // JS code + 2, // deprecated (symbol) + 100, // JS code + 1, // 32-bit int + 8, // Mongo timestamp + 1, // 64-bit int + ][t]; }, // compare two values of unknown type according to BSON ordering @@ -196,20 +185,17 @@ LocalCollection._f = { // any other value.) return negative if a is less, positive if b is // less, or 0 if equal _cmp(a, b) { - if (a === undefined) - return b === undefined ? 0 : -1; - if (b === undefined) - return 1; + if (a === undefined) {return b === undefined ? 0 : -1;} + if (b === undefined) {return 1;} let ta = LocalCollection._f._type(a); let tb = LocalCollection._f._type(b); const oa = LocalCollection._f._typeorder(ta); const ob = LocalCollection._f._typeorder(tb); - if (oa !== ob) - return oa < ob ? -1 : 1; + if (oa !== ob) {return oa < ob ? -1 : 1;} if (ta !== tb) - // XXX need to implement this if we implement Symbol or integers, or - // Timestamp - throw Error("Missing type coercion logic in _cmp"); + // XXX need to implement this if we implement Symbol or integers, or + // Timestamp + {throw Error('Missing type coercion logic in _cmp');} if (ta === 7) { // ObjectID // Convert to string. ta = tb = 2; @@ -224,9 +210,9 @@ LocalCollection._f = { } if (ta === 1) // double - return a - b; + {return a - b;} if (tb === 2) // string - return a < b ? -1 : (a === b ? 0 : 1); + {return a < b ? -1 : a === b ? 0 : 1;} if (ta === 3) { // Object // this could be much more efficient in the expected case ... const to_array = obj => { @@ -241,25 +227,19 @@ LocalCollection._f = { } if (ta === 4) { // Array for (let i = 0; ; i++) { - if (i === a.length) - return (i === b.length) ? 0 : -1; - if (i === b.length) - return 1; + if (i === a.length) {return i === b.length ? 0 : -1;} + if (i === b.length) {return 1;} const s = LocalCollection._f._cmp(a[i], b[i]); - if (s !== 0) - return s; + if (s !== 0) {return s;} } } if (ta === 5) { // binary // Surprisingly, a small binary blob is always less than a large one in // Mongo. - if (a.length !== b.length) - return a.length - b.length; + if (a.length !== b.length) {return a.length - b.length;} for (let i = 0; i < a.length; i++) { - if (a[i] < b[i]) - return -1; - if (a[i] > b[i]) - return 1; + if (a[i] < b[i]) {return -1;} + if (a[i] > b[i]) {return 1;} } return 0; } @@ -268,9 +248,9 @@ LocalCollection._f = { return b ? -1 : 0; } if (ta === 10) // null - return 0; + {return 0;} if (ta === 11) // regexp - throw Error("Sorting not supported on regular expression"); // XXX + {throw Error('Sorting not supported on regular expression');} // XXX // 13: javascript code // 14: symbol // 15: javascript code with scope @@ -280,7 +260,7 @@ LocalCollection._f = { // 255: minkey // 127: maxkey if (ta === 13) // javascript code - throw Error("Sorting not supported on Javascript code"); // XXX - throw Error("Unknown type to sort"); - } + {throw Error('Sorting not supported on Javascript code');} // XXX + throw Error('Unknown type to sort'); + }, }; diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 4c1ea0b187..9f16b1c9a3 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -1,4 +1,4 @@ -Tinytest.add("minimongo - wrapTransform", test => { +Tinytest.add('minimongo - wrapTransform', test => { const wrap = LocalCollection.wrapTransform; // Transforming no function gives falsey. @@ -12,7 +12,7 @@ Tinytest.add("minimongo - wrapTransform", test => { doc.z = () => 43; return doc; }; - const transformed = wrap(validTransform)({_id: "asdf", x: 54}); + const transformed = wrap(validTransform)({_id: 'asdf', x: 54}); test.equal(Object.keys(transformed), ['_id', 'y', 'z']); test.equal(transformed.y, 42); test.equal(transformed.z(), 43); @@ -21,19 +21,19 @@ Tinytest.add("minimongo - wrapTransform", test => { const oid1 = new MongoID.ObjectID(); const oid2 = new MongoID.ObjectID(oid1.toHexString()); test.equal(wrap(() => ({ - _id: oid2 + _id: oid2, }))({_id: oid1}), - {_id: oid2}); + {_id: oid2}); // transform functions must return objects const invalidObjects = [ - "asdf", new MongoID.ObjectID(), false, null, true, - 27, [123], /adsf/, new Date, () => {}, undefined + 'asdf', new MongoID.ObjectID(), false, null, true, + 27, [123], /adsf/, new Date, () => {}, undefined, ]; invalidObjects.forEach(invalidObject => { const wrapped = wrap(() => invalidObject); test.throws(() => { - wrapped({_id: "asdf"}); + wrapped({_id: 'asdf'}); }); }, /transform must return object/); @@ -45,7 +45,7 @@ Tinytest.add("minimongo - wrapTransform", test => { // transform functions may remove _ids test.equal({_id: 'a', x: 2}, - wrap(d => {delete d._id; return d;})({_id: 'a', x: 2})); + wrap(d => {delete d._id; return d;})({_id: 'a', x: 2})); // test that wrapped transform functions are nonreactive const unwrapped = doc => { @@ -54,7 +54,7 @@ Tinytest.add("minimongo - wrapTransform", test => { }; const handle = Tracker.autorun(() => { test.isTrue(Tracker.active); - wrap(unwrapped)({_id: "xxx"}); + wrap(unwrapped)({_id: 'xxx'}); }); handle.stop(); }); diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index e39efc79f0..d8be2f6d43 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -9,10 +9,10 @@ const assert_ordering = (test, f, values) => { let x = f(values[i], values[i]); if (x !== 0) { // XXX super janky - test.fail({type: "minimongo-ordering", - message: "value doesn't order as equal to itself", - value: JSON.stringify(values[i]), - should_be_zero_but_got: JSON.stringify(x)}); + test.fail({type: 'minimongo-ordering', + message: "value doesn't order as equal to itself", + value: JSON.stringify(values[i]), + should_be_zero_but_got: JSON.stringify(x)}); } if (i + 1 < values.length) { const less = values[i]; @@ -20,20 +20,20 @@ const assert_ordering = (test, f, values) => { x = f(less, more); if (!(x < 0)) { // XXX super janky - test.fail({type: "minimongo-ordering", - message: "ordering test failed", - first: JSON.stringify(less), - second: JSON.stringify(more), - should_be_negative_but_got: JSON.stringify(x)}); + test.fail({type: 'minimongo-ordering', + message: 'ordering test failed', + first: JSON.stringify(less), + second: JSON.stringify(more), + should_be_negative_but_got: JSON.stringify(x)}); } x = f(more, less); if (!(x > 0)) { // XXX super janky - test.fail({type: "minimongo-ordering", - message: "ordering test failed", - first: JSON.stringify(less), - second: JSON.stringify(more), - should_be_positive_but_got: JSON.stringify(x)}); + test.fail({type: 'minimongo-ordering', + message: 'ordering test failed', + first: JSON.stringify(less), + second: JSON.stringify(more), + should_be_positive_but_got: JSON.stringify(x)}); } } } @@ -60,41 +60,41 @@ const log_callbacks = operations => ({ const id = old_obj._id; delete old_obj._id; operations.push(EJSON.clone(['removed', id, at, old_obj])); - } + }, }); // XXX test shared structure in all MM entrypoints -Tinytest.add("minimongo - basics", test => { +Tinytest.add('minimongo - basics', test => { const c = new LocalCollection(); let fluffyKitten_id; let count; - fluffyKitten_id = c.insert({type: "kitten", name: "fluffy"}); - c.insert({type: "kitten", name: "snookums"}); - c.insert({type: "cryptographer", name: "alice"}); - c.insert({type: "cryptographer", name: "bob"}); - c.insert({type: "cryptographer", name: "cara"}); + fluffyKitten_id = c.insert({type: 'kitten', name: 'fluffy'}); + c.insert({type: 'kitten', name: 'snookums'}); + c.insert({type: 'cryptographer', name: 'alice'}); + c.insert({type: 'cryptographer', name: 'bob'}); + c.insert({type: 'cryptographer', name: 'cara'}); test.equal(c.find().count(), 5); - test.equal(c.find({type: "kitten"}).count(), 2); - test.equal(c.find({type: "cryptographer"}).count(), 3); - test.length(c.find({type: "kitten"}).fetch(), 2); - test.length(c.find({type: "cryptographer"}).fetch(), 3); - test.equal(fluffyKitten_id, c.findOne({type: "kitten", name: "fluffy"})._id); + test.equal(c.find({type: 'kitten'}).count(), 2); + test.equal(c.find({type: 'cryptographer'}).count(), 3); + test.length(c.find({type: 'kitten'}).fetch(), 2); + test.length(c.find({type: 'cryptographer'}).fetch(), 3); + test.equal(fluffyKitten_id, c.findOne({type: 'kitten', name: 'fluffy'})._id); - c.remove({name: "cara"}); + c.remove({name: 'cara'}); test.equal(c.find().count(), 4); - test.equal(c.find({type: "kitten"}).count(), 2); - test.equal(c.find({type: "cryptographer"}).count(), 2); - test.length(c.find({type: "kitten"}).fetch(), 2); - test.length(c.find({type: "cryptographer"}).fetch(), 2); + test.equal(c.find({type: 'kitten'}).count(), 2); + test.equal(c.find({type: 'cryptographer'}).count(), 2); + test.length(c.find({type: 'kitten'}).fetch(), 2); + test.length(c.find({type: 'cryptographer'}).fetch(), 2); - count = c.update({name: "snookums"}, {$set: {type: "cryptographer"}}); + count = c.update({name: 'snookums'}, {$set: {type: 'cryptographer'}}); test.equal(count, 1); test.equal(c.find().count(), 4); - test.equal(c.find({type: "kitten"}).count(), 1); - test.equal(c.find({type: "cryptographer"}).count(), 3); - test.length(c.find({type: "kitten"}).fetch(), 1); - test.length(c.find({type: "cryptographer"}).fetch(), 3); + test.equal(c.find({type: 'kitten'}).count(), 1); + test.equal(c.find({type: 'cryptographer'}).count(), 3); + test.length(c.find({type: 'kitten'}).fetch(), 1); + test.length(c.find({type: 'cryptographer'}).fetch(), 3); c.remove(null); c.remove(false); @@ -112,27 +112,27 @@ Tinytest.add("minimongo - basics", test => { test.equal(count, 4); test.equal(c.find().count(), 0); - c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); - c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); - c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); + c.insert({_id: 1, name: 'strawberry', tags: ['fruit', 'red', 'squishy']}); + c.insert({_id: 2, name: 'apple', tags: ['fruit', 'red', 'hard']}); + c.insert({_id: 3, name: 'rose', tags: ['flower', 'red', 'squishy']}); - test.equal(c.find({tags: "flower"}).count(), 1); - test.equal(c.find({tags: "fruit"}).count(), 2); - test.equal(c.find({tags: "red"}).count(), 3); - test.length(c.find({tags: "flower"}).fetch(), 1); - test.length(c.find({tags: "fruit"}).fetch(), 2); - test.length(c.find({tags: "red"}).fetch(), 3); + test.equal(c.find({tags: 'flower'}).count(), 1); + test.equal(c.find({tags: 'fruit'}).count(), 2); + test.equal(c.find({tags: 'red'}).count(), 3); + test.length(c.find({tags: 'flower'}).fetch(), 1); + test.length(c.find({tags: 'fruit'}).fetch(), 2); + test.length(c.find({tags: 'red'}).fetch(), 3); - test.equal(c.findOne(1).name, "strawberry"); - test.equal(c.findOne(2).name, "apple"); - test.equal(c.findOne(3).name, "rose"); + test.equal(c.findOne(1).name, 'strawberry'); + test.equal(c.findOne(2).name, 'apple'); + test.equal(c.findOne(3).name, 'rose'); test.equal(c.findOne(4), undefined); - test.equal(c.findOne("abc"), undefined); + test.equal(c.findOne('abc'), undefined); test.equal(c.findOne(undefined), undefined); test.equal(c.find(1).count(), 1); test.equal(c.find(4).count(), 0); - test.equal(c.find("abc").count(), 0); + test.equal(c.find('abc').count(), 0); test.equal(c.find(undefined).count(), 0); test.equal(c.find().count(), 3); test.equal(c.find(1, {skip: 1}).count(), 0); @@ -142,19 +142,19 @@ Tinytest.add("minimongo - basics", test => { test.equal(c.find({}, {limit: 2}).count(), 2); test.equal(c.find({}, {limit: 1}).count(), 1); test.equal(c.find({}, {skip: 1, limit: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {skip: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {limit: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {skip: 1, limit: 1}).count(), 1); - test.equal(c.find(1, {sort: ['_id','desc'], skip: 1}).count(), 0); - test.equal(c.find({_id: 1}, {sort: ['_id','desc'], skip: 1}).count(), 0); - test.equal(c.find({}, {sort: ['_id','desc'], skip: 1}).count(), 2); - test.equal(c.find({}, {sort: ['_id','desc'], skip: 2}).count(), 1); - test.equal(c.find({}, {sort: ['_id','desc'], limit: 2}).count(), 2); - test.equal(c.find({}, {sort: ['_id','desc'], limit: 1}).count(), 1); - test.equal(c.find({}, {sort: ['_id','desc'], skip: 1, limit: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], skip: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], limit: 1}).count(), 1); - test.equal(c.find({tags: "fruit"}, {sort: ['_id','desc'], skip: 1, limit: 1}).count(), 1); + test.equal(c.find({tags: 'fruit'}, {skip: 1}).count(), 1); + test.equal(c.find({tags: 'fruit'}, {limit: 1}).count(), 1); + test.equal(c.find({tags: 'fruit'}, {skip: 1, limit: 1}).count(), 1); + test.equal(c.find(1, {sort: ['_id', 'desc'], skip: 1}).count(), 0); + test.equal(c.find({_id: 1}, {sort: ['_id', 'desc'], skip: 1}).count(), 0); + test.equal(c.find({}, {sort: ['_id', 'desc'], skip: 1}).count(), 2); + test.equal(c.find({}, {sort: ['_id', 'desc'], skip: 2}).count(), 1); + test.equal(c.find({}, {sort: ['_id', 'desc'], limit: 2}).count(), 2); + test.equal(c.find({}, {sort: ['_id', 'desc'], limit: 1}).count(), 1); + test.equal(c.find({}, {sort: ['_id', 'desc'], skip: 1, limit: 1}).count(), 1); + test.equal(c.find({tags: 'fruit'}, {sort: ['_id', 'desc'], skip: 1}).count(), 1); + test.equal(c.find({tags: 'fruit'}, {sort: ['_id', 'desc'], limit: 1}).count(), 1); + test.equal(c.find({tags: 'fruit'}, {sort: ['_id', 'desc'], skip: 1, limit: 1}).count(), 1); // Regression test for #455. c.insert({foo: {bar: 'baz'}}); @@ -162,28 +162,27 @@ Tinytest.add("minimongo - basics", test => { test.equal(c.find({foo: {bar: 'baz'}}).count(), 1); }); -Tinytest.add("minimongo - error - no options", test => { +Tinytest.add('minimongo - error - no options', test => { try { - throw MinimongoError("Not fun to have errors"); + throw MinimongoError('Not fun to have errors'); } catch (e) { - test.equal(e.message, "Not fun to have errors"); + test.equal(e.message, 'Not fun to have errors'); } }); -Tinytest.add("minimongo - error - with field", test => { +Tinytest.add('minimongo - error - with field', test => { try { - throw MinimongoError("Cats are no fun", { field: "mice" }); + throw MinimongoError('Cats are no fun', { field: 'mice' }); } catch (e) { test.equal(e.message, "Cats are no fun for field 'mice'"); } }); -Tinytest.add("minimongo - cursors", test => { +Tinytest.add('minimongo - cursors', test => { const c = new LocalCollection(); let res; - for (let i = 0; i < 20; i++) - c.insert({i}); + for (let i = 0; i < 20; i++) {c.insert({i});} const q = c.find(); test.equal(q.count(), 20); @@ -200,7 +199,7 @@ Tinytest.add("minimongo - cursors", test => { // forEach let count = 0; const context = {}; - q.forEach(function (obj, i, cursor) { + q.forEach(function(obj, i, cursor) { test.equal(obj.i, count++); test.equal(obj.i, i); test.isTrue(context === this); @@ -211,15 +210,14 @@ Tinytest.add("minimongo - cursors", test => { test.length(q.fetch(), 20); // map - res = q.map(function (obj, i, cursor) { + res = q.map(function(obj, i, cursor) { test.equal(obj.i, i); test.isTrue(context === this); test.isTrue(cursor === q); return obj.i * 2; }, context); test.length(res, 20); - for (let i = 0; i < 20; i++) - test.equal(res[i], i * 2); + for (let i = 0; i < 20; i++) {test.equal(res[i], i * 2);} // call it again, it still works test.length(q.fetch(), 20); @@ -230,7 +228,7 @@ Tinytest.add("minimongo - cursors", test => { test.equal(c.findOne(id).i, 2); }); -Tinytest.add("minimongo - transform", test => { +Tinytest.add('minimongo - transform', test => { const c = new LocalCollection; c.insert({}); // transform functions must return objects @@ -246,13 +244,13 @@ Tinytest.add("minimongo - transform", test => { return docWithoutId; }; test.equal(c.findOne({}, {transform: transformWithoutId})._id, - c.findOne()._id); + c.findOne()._id); }); -Tinytest.add("minimongo - misc", test => { +Tinytest.add('minimongo - misc', test => { // deepcopy - let a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]}, - f: null, g: new Date()}; + let a = {a: [1, 2, 3], b: 'x', c: true, d: {x: 12, y: [12]}, + f: null, g: new Date()}; let b = EJSON.clone(a); test.equal(a, b); test.isTrue(LocalCollection._f._equal(a, b)); @@ -275,7 +273,7 @@ Tinytest.add("minimongo - misc", test => { test.equal(b.x.a, 14); // just to document current behavior }); -Tinytest.add("minimongo - lookup", test => { +Tinytest.add('minimongo - lookup', test => { const lookupA = MinimongoTest.makeLookupFunction('a'); test.equal(lookupA({}), [{value: undefined}]); test.equal(lookupA({a: 1}), [{value: 1}]); @@ -286,9 +284,9 @@ Tinytest.add("minimongo - lookup", test => { test.equal(lookupAX({a: {x: [1]}}), [{value: [1]}]); test.equal(lookupAX({a: 5}), [{value: undefined}]); test.equal(lookupAX({a: [{x: 1}, {x: [2]}, {y: 3}]}), - [{value: 1, arrayIndices: [0]}, - {value: [2], arrayIndices: [1]}, - {value: undefined, arrayIndices: [2]}]); + [{value: 1, arrayIndices: [0]}, + {value: [2], arrayIndices: [1]}, + {value: undefined, arrayIndices: [2]}]); const lookupA0X = MinimongoTest.makeLookupFunction('a.0.x'); test.equal(lookupA0X({a: [{x: 1}]}), [ @@ -308,30 +306,30 @@ Tinytest.add("minimongo - lookup", test => { // object {x:1} for a field named 0". {value: undefined, arrayIndices: [0]}, {value: undefined, arrayIndices: [1]}, - {value: undefined, arrayIndices: [2]} + {value: undefined, arrayIndices: [2]}, ]); test.equal( MinimongoTest.makeLookupFunction('w.x.0.z')({ w: [{x: [{z: 5}]}]}), [ - // From interpreting '0' as "0th array element". - {value: 5, arrayIndices: [0, 0, 'x']}, - // From interpreting '0' as "after branching in the array, look in the - // object {z:5} for a field named "0". - {value: undefined, arrayIndices: [0, 0]} - ]); + // From interpreting '0' as "0th array element". + {value: 5, arrayIndices: [0, 0, 'x']}, + // From interpreting '0' as "after branching in the array, look in the + // object {z:5} for a field named "0". + {value: undefined, arrayIndices: [0, 0]}, + ]); }); -Tinytest.add("minimongo - selector_compiler", test => { +Tinytest.add('minimongo - selector_compiler', test => { const matches = (shouldMatch, selector, doc) => { const doesMatch = new Minimongo.Matcher(selector).documentMatches(doc).result; if (doesMatch != shouldMatch) { // XXX super janky test.fail({message: `minimongo match failure: document ${shouldMatch ? "should match, but doesn't" : - "shouldn't match, but does"}`, - selector: JSON.stringify(selector), - document: JSON.stringify(doc) - }); + "shouldn't match, but does"}`, + selector: JSON.stringify(selector), + document: JSON.stringify(doc), + }); } }; @@ -383,11 +381,11 @@ Tinytest.add("minimongo - selector_compiler", test => { // arrays - match({a: [1,2]}, {a: [1, 2]}); - match({a: [1,2]}, {a: [[1, 2]]}); - match({a: [1,2]}, {a: [[3, 4], [1, 2]]}); - nomatch({a: [1,2]}, {a: [3, 4]}); - nomatch({a: [1,2]}, {a: [[[1, 2]]]}); + match({a: [1, 2]}, {a: [1, 2]}); + match({a: [1, 2]}, {a: [[1, 2]]}); + match({a: [1, 2]}, {a: [[3, 4], [1, 2]]}); + nomatch({a: [1, 2]}, {a: [3, 4]}); + nomatch({a: [1, 2]}, {a: [[[1, 2]]]}); // literal documents match({a: {b: 12}}, {a: {b: 12}}); @@ -396,10 +394,10 @@ Tinytest.add("minimongo - selector_compiler", test => { match({a: {b: 12, c: 13}}, {a: {b: 12, c: 13}}); nomatch({a: {b: 12, c: 13}}, {a: {c: 13, b: 12}}); // tested on mongodb nomatch({a: {}}, {a: {b: 12}}); - nomatch({a: {b:12}}, {a: {}}); + nomatch({a: {b: 12}}, {a: {}}); match( - {a: {b: 12, c: [13, true, false, 2.2, "a", null, {d: 14}]}}, - {a: {b: 12, c: [13, true, false, 2.2, "a", null, {d: 14}]}}); + {a: {b: 12, c: [13, true, false, 2.2, 'a', null, {d: 14}]}}, + {a: {b: 12, c: [13, true, false, 2.2, 'a', null, {d: 14}]}}); match({a: {b: 12}}, {a: {b: 12}, k: 99}); match({a: {b: 12}}, {a: [{b: 12}]}); @@ -437,7 +435,7 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({a: {$lt: 10}}, {a: [11, 12]}); // (there's a full suite of ordering test elsewhere) - nomatch({a: {$lt: "null"}}, {a: null}); + nomatch({a: {$lt: 'null'}}, {a: null}); match({a: {$lt: {x: [2, 3, 4]}}}, {a: {x: [1, 3, 4]}}); match({a: {$gt: {x: [2, 3, 4]}}}, {a: {x: [3, 3, 4]}}); nomatch({a: {$gt: {x: [2, 3, 4]}}}, {a: {x: [1, 3, 4]}}); @@ -462,7 +460,7 @@ Tinytest.add("minimongo - selector_compiler", test => { match({a: {$all: [1, 2]}}, {a: [1, 2]}); nomatch({a: {$all: [1, 2, 3]}}, {a: [1, 2]}); match({a: {$all: [1, 2]}}, {a: [3, 2, 1]}); - match({a: {$all: [1, "x"]}}, {a: [3, "x", 1]}); + match({a: {$all: [1, 'x']}}, {a: [3, 'x', 1]}); nomatch({a: {$all: ['2']}}, {a: 2}); nomatch({a: {$all: [2]}}, {a: '2'}); match({a: {$all: [[1, 2], [1, 3]]}}, {a: [[1, 3], [1, 2], [1, 4]]}); @@ -475,8 +473,8 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({a: {$all: [1, 2]}}, {a: {foo: 'bar'}}); // tested against mongodb, field is not an object nomatch({a: {$all: []}}, {a: []}); nomatch({a: {$all: []}}, {a: [5]}); - match({a: {$all: [/i/, /e/i]}}, {a: ["foo", "bEr", "biz"]}); - nomatch({a: {$all: [/i/, /e/i]}}, {a: ["foo", "bar", "biz"]}); + match({a: {$all: [/i/, /e/i]}}, {a: ['foo', 'bEr', 'biz']}); + nomatch({a: {$all: [/i/, /e/i]}}, {a: ['foo', 'bar', 'biz']}); match({a: {$all: [{b: 3}]}}, {a: [{b: 3}]}); // Members of $all other than regexps are *equality matches*, not document // matches. @@ -504,11 +502,11 @@ Tinytest.add("minimongo - selector_compiler", test => { match({a: {$exists: 1}}, {a: 5}); match({a: {$exists: 0}}, {b: 5}); - nomatch({'a.x':{$exists: false}}, {a: [{}, {x: 5}]}); - match({'a.x':{$exists: true}}, {a: [{}, {x: 5}]}); - match({'a.x':{$exists: true}}, {a: [{}, {x: 5}]}); - match({'a.x':{$exists: true}}, {a: {x: []}}); - match({'a.x':{$exists: true}}, {a: {x: null}}); + nomatch({'a.x': {$exists: false}}, {a: [{}, {x: 5}]}); + match({'a.x': {$exists: true}}, {a: [{}, {x: 5}]}); + match({'a.x': {$exists: true}}, {a: [{}, {x: 5}]}); + match({'a.x': {$exists: true}}, {a: {x: []}}); + match({'a.x': {$exists: true}}, {a: {x: null}}); // $mod match({a: {$mod: [10, 1]}}, {a: 11}); @@ -519,9 +517,9 @@ Tinytest.add("minimongo - selector_compiler", test => { 5, [10], [10, 1, 2], - "foo", + 'foo', {bar: 1}, - [] + [], ].forEach(badMod => { test.throws(() => { match({a: {$mod: badMod}}, {a: 11}); @@ -614,9 +612,9 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({a: {$nin: [1, 2, 3]}}, {a: [2]}); // tested against mongodb nomatch({a: {$nin: [{x: 1}, {x: 2}, {x: 3}]}}, {a: [{x: 2}]}); nomatch({a: {$nin: [1, 2, 3]}}, {a: [4, 2]}); - nomatch({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}, {b:2}]}); + nomatch({'a.b': {$nin: [1, 2, 3]}}, {a: [{b: 4}, {b: 2}]}); match({a: {$nin: [1, 2, 3]}}, {a: [4]}); - match({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}]}); + match({'a.b': {$nin: [1, 2, 3]}}, {a: [{b: 4}]}); nomatch({a: {$nin: ['x', /foo/i]}}, {a: 'x'}); nomatch({a: {$nin: ['x', /foo/i]}}, {a: 'fOo'}); @@ -642,20 +640,20 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({a: {$size: 0}}, {a: [2]}); nomatch({a: {$size: 1}}, {a: []}); nomatch({a: {$size: 1}}, {a: [2, 2]}); - nomatch({a: {$size: 0}}, {a: "2"}); - nomatch({a: {$size: 1}}, {a: "2"}); - nomatch({a: {$size: 2}}, {a: "2"}); + nomatch({a: {$size: 0}}, {a: '2'}); + nomatch({a: {$size: 1}}, {a: '2'}); + nomatch({a: {$size: 2}}, {a: '2'}); - nomatch({a: {$size: 2}}, {a: [[2,2]]}); // tested against mongodb + nomatch({a: {$size: 2}}, {a: [[2, 2]]}); // tested against mongodb // $bitsAllClear - number - match({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0}); - match({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b10000}); - nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b1}); - nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b10}); - nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b100}); - nomatch({a: {$bitsAllClear: [0,1,2,3]}}, {a: 0b1000}); + match({a: {$bitsAllClear: [0, 1, 2, 3]}}, {a: 0}); + match({a: {$bitsAllClear: [0, 1, 2, 3]}}, {a: 0b10000}); + nomatch({a: {$bitsAllClear: [0, 1, 2, 3]}}, {a: 0b1}); + nomatch({a: {$bitsAllClear: [0, 1, 2, 3]}}, {a: 0b10}); + nomatch({a: {$bitsAllClear: [0, 1, 2, 3]}}, {a: 0b100}); + nomatch({a: {$bitsAllClear: [0, 1, 2, 3]}}, {a: 0b1000}); // $bitsAllClear - buffer match({a: {$bitsAllClear: new Uint8Array([3])}}, {a: new Uint8Array([4])}); @@ -665,11 +663,11 @@ Tinytest.add("minimongo - selector_compiler", test => { match({a: {$bitsAllClear: new Uint8Array([3])}}, {a: 0 }); // $bitsAllSet - number - match({a: {$bitsAllSet: [0,1,2,3]}}, {a: 0b1111}); - nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 0b111}); - nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 256}); - nomatch({a: {$bitsAllSet: [0,1,2,3]}}, {a: 50000}); - match({a: {$bitsAllSet: [0,1,2]}}, {a: 15}); + match({a: {$bitsAllSet: [0, 1, 2, 3]}}, {a: 0b1111}); + nomatch({a: {$bitsAllSet: [0, 1, 2, 3]}}, {a: 0b111}); + nomatch({a: {$bitsAllSet: [0, 1, 2, 3]}}, {a: 256}); + nomatch({a: {$bitsAllSet: [0, 1, 2, 3]}}, {a: 50000}); + match({a: {$bitsAllSet: [0, 1, 2]}}, {a: 15}); match({a: {$bitsAllSet: [0, 12]}}, {a: 0b1000000000001}); nomatch({a: {$bitsAllSet: [0, 12]}}, {a: 0b1000000000000}); nomatch({a: {$bitsAllSet: [0, 12]}}, {a: 0b1}); @@ -680,13 +678,13 @@ Tinytest.add("minimongo - selector_compiler", test => { match({a: {$bitsAllSet: new Uint8Array([3])}}, {a: 3 }); // $bitsAnySet - number - match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b1}); - match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b10}); - match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b100}); - match({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b1000}); + match({a: {$bitsAnySet: [0, 1, 2, 3]}}, {a: 0b1}); + match({a: {$bitsAnySet: [0, 1, 2, 3]}}, {a: 0b10}); + match({a: {$bitsAnySet: [0, 1, 2, 3]}}, {a: 0b100}); + match({a: {$bitsAnySet: [0, 1, 2, 3]}}, {a: 0b1000}); match({a: {$bitsAnySet: [4]}}, {a: 0b10000}); - nomatch({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0b10000}); - nomatch({a: {$bitsAnySet: [0,1,2,3]}}, {a: 0}); + nomatch({a: {$bitsAnySet: [0, 1, 2, 3]}}, {a: 0b10000}); + nomatch({a: {$bitsAnySet: [0, 1, 2, 3]}}, {a: 0}); // $bitsAnySet - buffer match({a: {$bitsAnySet: new Uint8Array([3])}}, {a: new Uint8Array([7])}); @@ -694,18 +692,18 @@ Tinytest.add("minimongo - selector_compiler", test => { match({a: {$bitsAnySet: new Uint8Array([3])}}, {a: 1 }); // $bitsAnyClear - number - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b10}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b100}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1000}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b10000}); - nomatch({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1111}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b111}); - nomatch({a: {$bitsAnyClear: [0,1,2]}}, {a: 0b111}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b11}); - nomatch({a: {$bitsAnyClear: [0,1]}}, {a: 0b11}); - match({a: {$bitsAnyClear: [0,1,2,3]}}, {a: 0b1}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b1}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b10}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b100}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b1000}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b10000}); + nomatch({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b1111}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b111}); + nomatch({a: {$bitsAnyClear: [0, 1, 2]}}, {a: 0b111}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b11}); + nomatch({a: {$bitsAnyClear: [0, 1]}}, {a: 0b11}); + match({a: {$bitsAnyClear: [0, 1, 2, 3]}}, {a: 0b1}); nomatch({a: {$bitsAnyClear: [0]}}, {a: 0b1}); nomatch({a: {$bitsAnyClear: [4]}}, {a: 0b10000}); @@ -717,158 +715,158 @@ Tinytest.add("minimongo - selector_compiler", test => { // taken from: https://github.com/mongodb/mongo/blob/master/jstests/core/bittest.js const c = new LocalCollection; function matchCount(query, count) { - const matches = c.find(query).count() + const matches = c.find(query).count(); if (matches !== count) { test.fail({message: `minimongo match count failure: matched ${matches} times, but should match ${count} times`, query: JSON.stringify(query), - count: JSON.stringify(count) + count: JSON.stringify(count), }); } } // Tests on numbers. - c.insert({a: 0}) - c.insert({a: 1}) - c.insert({a: 54}) - c.insert({a: 88}) - c.insert({a: 255}) + c.insert({a: 0}); + c.insert({a: 1}); + c.insert({a: 54}); + c.insert({a: 88}); + c.insert({a: 255}); // Tests with bitmask. - matchCount({a: {$bitsAllSet: 0}}, 5) - matchCount({a: {$bitsAllSet: 1}}, 2) - matchCount({a: {$bitsAllSet: 16}}, 3) - matchCount({a: {$bitsAllSet: 54}}, 2) - matchCount({a: {$bitsAllSet: 55}}, 1) - matchCount({a: {$bitsAllSet: 88}}, 2) - matchCount({a: {$bitsAllSet: 255}}, 1) - matchCount({a: {$bitsAllClear: 0}}, 5) - matchCount({a: {$bitsAllClear: 1}}, 3) - matchCount({a: {$bitsAllClear: 16}}, 2) - matchCount({a: {$bitsAllClear: 129}}, 3) - matchCount({a: {$bitsAllClear: 255}}, 1) - matchCount({a: {$bitsAnySet: 0}}, 0) - matchCount({a: {$bitsAnySet: 9}}, 3) - matchCount({a: {$bitsAnySet: 255}}, 4) - matchCount({a: {$bitsAnyClear: 0}}, 0) - matchCount({a: {$bitsAnyClear: 18}}, 3) - matchCount({a: {$bitsAnyClear: 24}}, 3) - matchCount({a: {$bitsAnyClear: 255}}, 4) + matchCount({a: {$bitsAllSet: 0}}, 5); + matchCount({a: {$bitsAllSet: 1}}, 2); + matchCount({a: {$bitsAllSet: 16}}, 3); + matchCount({a: {$bitsAllSet: 54}}, 2); + matchCount({a: {$bitsAllSet: 55}}, 1); + matchCount({a: {$bitsAllSet: 88}}, 2); + matchCount({a: {$bitsAllSet: 255}}, 1); + matchCount({a: {$bitsAllClear: 0}}, 5); + matchCount({a: {$bitsAllClear: 1}}, 3); + matchCount({a: {$bitsAllClear: 16}}, 2); + matchCount({a: {$bitsAllClear: 129}}, 3); + matchCount({a: {$bitsAllClear: 255}}, 1); + matchCount({a: {$bitsAnySet: 0}}, 0); + matchCount({a: {$bitsAnySet: 9}}, 3); + matchCount({a: {$bitsAnySet: 255}}, 4); + matchCount({a: {$bitsAnyClear: 0}}, 0); + matchCount({a: {$bitsAnyClear: 18}}, 3); + matchCount({a: {$bitsAnyClear: 24}}, 3); + matchCount({a: {$bitsAnyClear: 255}}, 4); // Tests with array of bit positions. - matchCount({a: {$bitsAllSet: []}}, 5) - matchCount({a: {$bitsAllSet: [0]}}, 2) - matchCount({a: {$bitsAllSet: [4]}}, 3) - matchCount({a: {$bitsAllSet: [1, 2, 4, 5]}}, 2) - matchCount({a: {$bitsAllSet: [0, 1, 2, 4, 5]}}, 1) - matchCount({a: {$bitsAllSet: [3, 4, 6]}}, 2) - matchCount({a: {$bitsAllSet: [0, 1, 2, 3, 4, 5, 6, 7]}}, 1) - matchCount({a: {$bitsAllClear: []}}, 5) - matchCount({a: {$bitsAllClear: [0]}}, 3) - matchCount({a: {$bitsAllClear: [4]}}, 2) - matchCount({a: {$bitsAllClear: [1, 7]}}, 3) - matchCount({a: {$bitsAllClear: [0, 1, 2, 3, 4, 5, 6, 7]}}, 1) - matchCount({a: {$bitsAnySet: []}}, 0) - matchCount({a: {$bitsAnySet: [1, 3]}}, 3) - matchCount({a: {$bitsAnySet: [0, 1, 2, 3, 4, 5, 6, 7]}}, 4) - matchCount({a: {$bitsAnyClear: []}}, 0) - matchCount({a: {$bitsAnyClear: [1, 4]}}, 3) - matchCount({a: {$bitsAnyClear: [3, 4]}}, 3) - matchCount({a: {$bitsAnyClear: [0, 1, 2, 3, 4, 5, 6, 7]}}, 4) + matchCount({a: {$bitsAllSet: []}}, 5); + matchCount({a: {$bitsAllSet: [0]}}, 2); + matchCount({a: {$bitsAllSet: [4]}}, 3); + matchCount({a: {$bitsAllSet: [1, 2, 4, 5]}}, 2); + matchCount({a: {$bitsAllSet: [0, 1, 2, 4, 5]}}, 1); + matchCount({a: {$bitsAllSet: [3, 4, 6]}}, 2); + matchCount({a: {$bitsAllSet: [0, 1, 2, 3, 4, 5, 6, 7]}}, 1); + matchCount({a: {$bitsAllClear: []}}, 5); + matchCount({a: {$bitsAllClear: [0]}}, 3); + matchCount({a: {$bitsAllClear: [4]}}, 2); + matchCount({a: {$bitsAllClear: [1, 7]}}, 3); + matchCount({a: {$bitsAllClear: [0, 1, 2, 3, 4, 5, 6, 7]}}, 1); + matchCount({a: {$bitsAnySet: []}}, 0); + matchCount({a: {$bitsAnySet: [1, 3]}}, 3); + matchCount({a: {$bitsAnySet: [0, 1, 2, 3, 4, 5, 6, 7]}}, 4); + matchCount({a: {$bitsAnyClear: []}}, 0); + matchCount({a: {$bitsAnyClear: [1, 4]}}, 3); + matchCount({a: {$bitsAnyClear: [3, 4]}}, 3); + matchCount({a: {$bitsAnyClear: [0, 1, 2, 3, 4, 5, 6, 7]}}, 4); // Tests with multiple predicates. - matchCount({a: {$bitsAllSet: 54, $bitsAllClear: 201}}, 1) + matchCount({a: {$bitsAllSet: 54, $bitsAllClear: 201}}, 1); // Tests on negative numbers - c.remove({}) - c.insert({a: -0}) - c.insert({a: -1}) - c.insert({a: -54}) + c.remove({}); + c.insert({a: -0}); + c.insert({a: -1}); + c.insert({a: -54}); // Tests with bitmask. - matchCount({a: {$bitsAllSet: 0}}, 3) - matchCount({a: {$bitsAllSet: 2}}, 2) - matchCount({a: {$bitsAllSet: 127}}, 1) - matchCount({a: {$bitsAllSet: 74}}, 2) - matchCount({a: {$bitsAllClear: 0}}, 3) - matchCount({a: {$bitsAllClear: 53}}, 2) - matchCount({a: {$bitsAllClear: 127}}, 1) - matchCount({a: {$bitsAnySet: 0}}, 0) - matchCount({a: {$bitsAnySet: 2}}, 2) - matchCount({a: {$bitsAnySet: 127}}, 2) - matchCount({a: {$bitsAnyClear: 0}}, 0) - matchCount({a: {$bitsAnyClear: 53}}, 2) - matchCount({a: {$bitsAnyClear: 127}}, 2) + matchCount({a: {$bitsAllSet: 0}}, 3); + matchCount({a: {$bitsAllSet: 2}}, 2); + matchCount({a: {$bitsAllSet: 127}}, 1); + matchCount({a: {$bitsAllSet: 74}}, 2); + matchCount({a: {$bitsAllClear: 0}}, 3); + matchCount({a: {$bitsAllClear: 53}}, 2); + matchCount({a: {$bitsAllClear: 127}}, 1); + matchCount({a: {$bitsAnySet: 0}}, 0); + matchCount({a: {$bitsAnySet: 2}}, 2); + matchCount({a: {$bitsAnySet: 127}}, 2); + matchCount({a: {$bitsAnyClear: 0}}, 0); + matchCount({a: {$bitsAnyClear: 53}}, 2); + matchCount({a: {$bitsAnyClear: 127}}, 2); // Tests with array of bit positions. const allPositions = []; for (let i = 0; i < 64; i++) { - allPositions.push(i) + allPositions.push(i); } - matchCount({a: {$bitsAllSet: []}}, 3) - matchCount({a: {$bitsAllSet: [1]}}, 2) - matchCount({a: {$bitsAllSet: allPositions}}, 1) - matchCount({a: {$bitsAllSet: [1, 7, 6, 3, 100]}}, 2) - matchCount({a: {$bitsAllClear: []}}, 3) - matchCount({a: {$bitsAllClear: [5, 4, 2, 0]}}, 2) - matchCount({a: {$bitsAllClear: allPositions}}, 1) - matchCount({a: {$bitsAnySet: []}}, 0) - matchCount({a: {$bitsAnySet: [1]}}, 2) - matchCount({a: {$bitsAnySet: allPositions}}, 2) - matchCount({a: {$bitsAnyClear: []}}, 0) - matchCount({a: {$bitsAnyClear: [0, 2, 4, 5, 100]}}, 2) - matchCount({a: {$bitsAnyClear: allPositions}}, 2) + matchCount({a: {$bitsAllSet: []}}, 3); + matchCount({a: {$bitsAllSet: [1]}}, 2); + matchCount({a: {$bitsAllSet: allPositions}}, 1); + matchCount({a: {$bitsAllSet: [1, 7, 6, 3, 100]}}, 2); + matchCount({a: {$bitsAllClear: []}}, 3); + matchCount({a: {$bitsAllClear: [5, 4, 2, 0]}}, 2); + matchCount({a: {$bitsAllClear: allPositions}}, 1); + matchCount({a: {$bitsAnySet: []}}, 0); + matchCount({a: {$bitsAnySet: [1]}}, 2); + matchCount({a: {$bitsAnySet: allPositions}}, 2); + matchCount({a: {$bitsAnyClear: []}}, 0); + matchCount({a: {$bitsAnyClear: [0, 2, 4, 5, 100]}}, 2); + matchCount({a: {$bitsAnyClear: allPositions}}, 2); // Tests with multiple predicates. - matchCount({a: {$bitsAllSet: 74, $bitsAllClear: 53}}, 1) + matchCount({a: {$bitsAllSet: 74, $bitsAllClear: 53}}, 1); // Tests on BinData. - c.remove({}) - c.insert({a: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}) - c.insert({a: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}) - c.insert({a: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}) - c.insert({a: EJSON.parse('{"$binary": "////////////////////////////"}')}) + c.remove({}); + c.insert({a: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}); + c.insert({a: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}); + c.insert({a: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}); + c.insert({a: EJSON.parse('{"$binary": "////////////////////////////"}')}); // Tests with binary string bitmask. - matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 4) - matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 3) - matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}}, 2) - matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 1) - matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 4) - matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "AAyfAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 3) - matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "JAyfqwetkqwklEWRbWERKKJREtbq"}')}}, 2) - matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 1) - matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 0) - matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "AAyfAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 1) - matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "JAyfqwetkqwklEWRbWERKKJREtbq"}')}}, 2) - matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 3) - matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 0) - matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 1) - matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}}, 2) - matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 3) + matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 4); + matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 3); + matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}}, 2); + matchCount({a: {$bitsAllSet: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 1); + matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 4); + matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "AAyfAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 3); + matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "JAyfqwetkqwklEWRbWERKKJREtbq"}')}}, 2); + matchCount({a: {$bitsAllClear: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 1); + matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 0); + matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "AAyfAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 1); + matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "JAyfqwetkqwklEWRbWERKKJREtbq"}')}}, 2); + matchCount({a: {$bitsAnySet: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 3); + matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "AAAAAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 0); + matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}')}}, 1); + matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "JANgqwetkqwklEWRbWERKKJREtbq"}')}}, 2); + matchCount({a: {$bitsAnyClear: EJSON.parse('{"$binary": "////////////////////////////"}')}}, 3); // Tests with multiple predicates. matchCount({ - a: { - $bitsAllSet: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}'), - $bitsAllClear: EJSON.parse('{"$binary": "//yf////////////////////////"}') - } - }, 1) + a: { + $bitsAllSet: EJSON.parse('{"$binary": "AANgAAAAAAAAAAAAAAAAAAAAAAAA"}'), + $bitsAllClear: EJSON.parse('{"$binary": "//yf////////////////////////"}'), + }, + }, 1); - c.remove({}) + c.remove({}); - nomatch({a: {$bitsAllSet: 1}}, {a: false}) - nomatch({a: {$bitsAllSet: 1}}, {a: NaN}) - nomatch({a: {$bitsAllSet: 1}}, {a: Infinity}) - nomatch({a: {$bitsAllSet: 1}}, {a: null}) - nomatch({a: {$bitsAllSet: 1}}, {a: 'asdf'}) - nomatch({a: {$bitsAllSet: 1}}, {a: ['a', 'b']}) - nomatch({a: {$bitsAllSet: 1}}, {a: {foo: 'bar'}}) - nomatch({a: {$bitsAllSet: 1}}, {a: 1.2}) - nomatch({a: {$bitsAllSet: 1}}, {a: "1"}); + nomatch({a: {$bitsAllSet: 1}}, {a: false}); + nomatch({a: {$bitsAllSet: 1}}, {a: NaN}); + nomatch({a: {$bitsAllSet: 1}}, {a: Infinity}); + nomatch({a: {$bitsAllSet: 1}}, {a: null}); + nomatch({a: {$bitsAllSet: 1}}, {a: 'asdf'}); + nomatch({a: {$bitsAllSet: 1}}, {a: ['a', 'b']}); + nomatch({a: {$bitsAllSet: 1}}, {a: {foo: 'bar'}}); + nomatch({a: {$bitsAllSet: 1}}, {a: 1.2}); + nomatch({a: {$bitsAllSet: 1}}, {a: '1'}); [ false, @@ -879,8 +877,8 @@ Tinytest.add("minimongo - selector_compiler", test => { ['a', 'b'], {foo: 'bar'}, 1.2, - "1", - [0, -1] + '1', + [0, -1], ].forEach(badValue => { test.throws(() => { match({a: {$bitsAllSet: badValue}}, {a: 42}); @@ -890,8 +888,8 @@ Tinytest.add("minimongo - selector_compiler", test => { // $type match({a: {$type: 1}}, {a: 1.1}); match({a: {$type: 1}}, {a: 1}); - nomatch({a: {$type: 1}}, {a: "1"}); - match({a: {$type: 2}}, {a: "1"}); + nomatch({a: {$type: 1}}, {a: '1'}); + match({a: {$type: 2}}, {a: '1'}); nomatch({a: {$type: 2}}, {a: 1}); match({a: {$type: 3}}, {a: {}}); match({a: {$type: 3}}, {a: {b: 2}}); @@ -903,16 +901,16 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({a: {$type: 5}}, {a: []}); nomatch({a: {$type: 5}}, {a: [42]}); match({a: {$type: 7}}, {a: new MongoID.ObjectID()}); - nomatch({a: {$type: 7}}, {a: "1234567890abcd1234567890"}); + nomatch({a: {$type: 7}}, {a: '1234567890abcd1234567890'}); match({a: {$type: 8}}, {a: true}); match({a: {$type: 8}}, {a: false}); - nomatch({a: {$type: 8}}, {a: "true"}); + nomatch({a: {$type: 8}}, {a: 'true'}); nomatch({a: {$type: 8}}, {a: 0}); nomatch({a: {$type: 8}}, {a: null}); nomatch({a: {$type: 8}}, {a: ''}); nomatch({a: {$type: 8}}, {}); - match({a: {$type: 9}}, {a: (new Date)}); - nomatch({a: {$type: 9}}, {a: +(new Date)}); + match({a: {$type: 9}}, {a: new Date}); + nomatch({a: {$type: 9}}, {a: +new Date}); match({a: {$type: 10}}, {a: null}); nomatch({a: {$type: 10}}, {a: false}); nomatch({a: {$type: 10}}, {a: ''}); @@ -929,13 +927,13 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({a: {$type: 4}}, {a: [1]}); // tested against mongodb match({a: {$type: 1}}, {a: [1]}); nomatch({a: {$type: 2}}, {a: [1]}); - match({a: {$type: 1}}, {a: ["1", 1]}); - match({a: {$type: 2}}, {a: ["1", 1]}); - nomatch({a: {$type: 3}}, {a: ["1", 1]}); - nomatch({a: {$type: 4}}, {a: ["1", 1]}); - nomatch({a: {$type: 1}}, {a: ["1", []]}); - match({a: {$type: 2}}, {a: ["1", []]}); - match({a: {$type: 4}}, {a: ["1", []]}); // tested against mongodb + match({a: {$type: 1}}, {a: ['1', 1]}); + match({a: {$type: 2}}, {a: ['1', 1]}); + nomatch({a: {$type: 3}}, {a: ['1', 1]}); + nomatch({a: {$type: 4}}, {a: ['1', 1]}); + nomatch({a: {$type: 1}}, {a: ['1', []]}); + match({a: {$type: 2}}, {a: ['1', []]}); + match({a: {$type: 4}}, {a: ['1', []]}); // tested against mongodb // An exception to the normal rule is that an array found via numeric index is // examined itself, and its elements are not. match({'a.0': {$type: 4}}, {a: [[0]]}); @@ -1015,36 +1013,36 @@ Tinytest.add("minimongo - selector_compiler", test => { match({x: {$not: {$lt: 10, $gt: 7}}}, {x: 6}); match({x: {$not: {$gt: 7}}}, {x: [2, 3, 4]}); - match({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}]}); + match({'x.y': {$not: {$gt: 7}}}, {x: [{y: 2}, {y: 3}, {y: 4}]}); nomatch({x: {$not: {$gt: 7}}}, {x: [2, 3, 4, 10]}); - nomatch({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}, {y:10}]}); + nomatch({'x.y': {$not: {$gt: 7}}}, {x: [{y: 2}, {y: 3}, {y: 4}, {y: 10}]}); - match({x: {$not: /a/}}, {x: "dog"}); - nomatch({x: {$not: /a/}}, {x: "cat"}); - match({x: {$not: /a/}}, {x: ["dog", "puppy"]}); - nomatch({x: {$not: /a/}}, {x: ["kitten", "cat"]}); + match({x: {$not: /a/}}, {x: 'dog'}); + nomatch({x: {$not: /a/}}, {x: 'cat'}); + match({x: {$not: /a/}}, {x: ['dog', 'puppy']}); + nomatch({x: {$not: /a/}}, {x: ['kitten', 'cat']}); // dotted keypaths: bare values - match({"a.b": 1}, {a: {b: 1}}); - nomatch({"a.b": 1}, {a: {b: 2}}); - match({"a.b": [1,2,3]}, {a: {b: [1,2,3]}}); - nomatch({"a.b": [1,2,3]}, {a: {b: [4]}}); - match({"a.b": /a/}, {a: {b: "cat"}}); - nomatch({"a.b": /a/}, {a: {b: "dog"}}); - match({"a.b.c": null}, {}); - match({"a.b.c": null}, {a: 1}); - match({"a.b": null}, {a: 1}); - match({"a.b.c": null}, {a: {b: 4}}); + match({'a.b': 1}, {a: {b: 1}}); + nomatch({'a.b': 1}, {a: {b: 2}}); + match({'a.b': [1, 2, 3]}, {a: {b: [1, 2, 3]}}); + nomatch({'a.b': [1, 2, 3]}, {a: {b: [4]}}); + match({'a.b': /a/}, {a: {b: 'cat'}}); + nomatch({'a.b': /a/}, {a: {b: 'dog'}}); + match({'a.b.c': null}, {}); + match({'a.b.c': null}, {a: 1}); + match({'a.b': null}, {a: 1}); + match({'a.b.c': null}, {a: {b: 4}}); // dotted keypaths, nulls, numeric indices, arrays - nomatch({"a.b": null}, {a: [1]}); - match({"a.b": []}, {a: {b: []}}); + nomatch({'a.b': null}, {a: [1]}); + match({'a.b': []}, {a: {b: []}}); const big = {a: [{b: 1}, 2, {}, {b: [3, 4]}]}; - match({"a.b": 1}, big); - match({"a.b": [3, 4]}, big); - match({"a.b": 3}, big); - match({"a.b": 4}, big); - match({"a.b": null}, big); // matches on slot 2 + match({'a.b': 1}, big); + match({'a.b': [3, 4]}, big); + match({'a.b': 3}, big); + match({'a.b': 4}, big); + match({'a.b': null}, big); // matches on slot 2 match({'a.1': 8}, {a: [7, 8, 9]}); nomatch({'a.1': 7}, {a: [7, 8, 9]}); nomatch({'a.1': null}, {a: [7, 8, 9]}); @@ -1053,46 +1051,46 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({'a.1': 7}, {a: [[6, 7], [8, 9]]}); nomatch({'a.1': 8}, {a: [[6, 7], [8, 9]]}); nomatch({'a.1': 9}, {a: [[6, 7], [8, 9]]}); - match({"a.1": 2}, {a: [0, {1: 2}, 3]}); - match({"a.1": {1: 2}}, {a: [0, {1: 2}, 3]}); - match({"x.1.y": 8}, {x: [7, {y: 8}, 9]}); + match({'a.1': 2}, {a: [0, {1: 2}, 3]}); + match({'a.1': {1: 2}}, {a: [0, {1: 2}, 3]}); + match({'x.1.y': 8}, {x: [7, {y: 8}, 9]}); // comes from trying '1' as key in the plain object - match({"x.1.y": null}, {x: [7, {y: 8}, 9]}); - match({"a.1.b": 9}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); - match({"a.1.b": 'foo'}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); - match({"a.1.b": null}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); - match({"a.1.b": 2}, {a: [1, [{b: 2}], 3]}); - nomatch({"a.1.b": null}, {a: [1, [{b: 2}], 3]}); + match({'x.1.y': null}, {x: [7, {y: 8}, 9]}); + match({'a.1.b': 9}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); + match({'a.1.b': 'foo'}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); + match({'a.1.b': null}, {a: [7, {b: 9}, {1: {b: 'foo'}}]}); + match({'a.1.b': 2}, {a: [1, [{b: 2}], 3]}); + nomatch({'a.1.b': null}, {a: [1, [{b: 2}], 3]}); // this is new behavior in mongo 2.5 - nomatch({"a.0.b": null}, {a: [5]}); - match({"a.1": 4}, {a: [{1: 4}, 5]}); - match({"a.1": 5}, {a: [{1: 4}, 5]}); - nomatch({"a.1": null}, {a: [{1: 4}, 5]}); - match({"a.1.foo": 4}, {a: [{1: {foo: 4}}, {foo: 5}]}); - match({"a.1.foo": 5}, {a: [{1: {foo: 4}}, {foo: 5}]}); - match({"a.1.foo": null}, {a: [{1: {foo: 4}}, {foo: 5}]}); + nomatch({'a.0.b': null}, {a: [5]}); + match({'a.1': 4}, {a: [{1: 4}, 5]}); + match({'a.1': 5}, {a: [{1: 4}, 5]}); + nomatch({'a.1': null}, {a: [{1: 4}, 5]}); + match({'a.1.foo': 4}, {a: [{1: {foo: 4}}, {foo: 5}]}); + match({'a.1.foo': 5}, {a: [{1: {foo: 4}}, {foo: 5}]}); + match({'a.1.foo': null}, {a: [{1: {foo: 4}}, {foo: 5}]}); // trying to access a dotted field that is undefined at some point // down the chain - nomatch({"a.b": 1}, {x: 2}); - nomatch({"a.b.c": 1}, {a: {x: 2}}); - nomatch({"a.b.c": 1}, {a: {b: {x: 2}}}); - nomatch({"a.b.c": 1}, {a: {b: 1}}); - nomatch({"a.b.c": 1}, {a: {b: 0}}); + nomatch({'a.b': 1}, {x: 2}); + nomatch({'a.b.c': 1}, {a: {x: 2}}); + nomatch({'a.b.c': 1}, {a: {b: {x: 2}}}); + nomatch({'a.b.c': 1}, {a: {b: 1}}); + nomatch({'a.b.c': 1}, {a: {b: 0}}); // dotted keypaths: literal objects - match({"a.b": {c: 1}}, {a: {b: {c: 1}}}); - nomatch({"a.b": {c: 1}}, {a: {b: {c: 2}}}); - nomatch({"a.b": {c: 1}}, {a: {b: 2}}); - match({"a.b": {c: 1, d: 2}}, {a: {b: {c: 1, d: 2}}}); - nomatch({"a.b": {c: 1, d: 2}}, {a: {b: {c: 1, d: 1}}}); - nomatch({"a.b": {c: 1, d: 2}}, {a: {b: {d: 2}}}); + match({'a.b': {c: 1}}, {a: {b: {c: 1}}}); + nomatch({'a.b': {c: 1}}, {a: {b: {c: 2}}}); + nomatch({'a.b': {c: 1}}, {a: {b: 2}}); + match({'a.b': {c: 1, d: 2}}, {a: {b: {c: 1, d: 2}}}); + nomatch({'a.b': {c: 1, d: 2}}, {a: {b: {c: 1, d: 1}}}); + nomatch({'a.b': {c: 1, d: 2}}, {a: {b: {d: 2}}}); // dotted keypaths: $ operators - match({"a.b": {$in: [1, 2, 3]}}, {a: {b: [2]}}); // tested against mongodb - match({"a.b": {$in: [{x: 1}, {x: 2}, {x: 3}]}}, {a: {b: [{x: 2}]}}); - match({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4, 2]}}); - nomatch({"a.b": {$in: [1, 2, 3]}}, {a: {b: [4]}}); + match({'a.b': {$in: [1, 2, 3]}}, {a: {b: [2]}}); // tested against mongodb + match({'a.b': {$in: [{x: 1}, {x: 2}, {x: 3}]}}, {a: {b: [{x: 2}]}}); + match({'a.b': {$in: [1, 2, 3]}}, {a: {b: [4, 2]}}); + nomatch({'a.b': {$in: [1, 2, 3]}}, {a: {b: [4]}}); // $or test.throws(() => { @@ -1150,20 +1148,20 @@ Tinytest.add("minimongo - selector_compiler", test => { match({$or: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {b: 2}); // $or and dot-notation - match({$or: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); - match({$or: [{"a.b": 1}, {"a.c": 1}]}, {a: {b: 1}}); - nomatch({$or: [{"a.b": 2}, {"a.c": 1}]}, {a: {b: 1}}); + match({$or: [{'a.b': 1}, {'a.b': 2}]}, {a: {b: 1}}); + match({$or: [{'a.b': 1}, {'a.c': 1}]}, {a: {b: 1}}); + nomatch({$or: [{'a.b': 2}, {'a.c': 1}]}, {a: {b: 1}}); // $or and nested objects match({$or: [{a: {b: 1, c: 2}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); nomatch({$or: [{a: {b: 1, c: 3}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); // $or and regexes - match({$or: [{a: /a/}]}, {a: "cat"}); - nomatch({$or: [{a: /o/}]}, {a: "cat"}); - match({$or: [{a: /a/}, {a: /o/}]}, {a: "cat"}); - nomatch({$or: [{a: /i/}, {a: /o/}]}, {a: "cat"}); - match({$or: [{a: /i/}, {b: /o/}]}, {a: "cat", b: "dog"}); + match({$or: [{a: /a/}]}, {a: 'cat'}); + nomatch({$or: [{a: /o/}]}, {a: 'cat'}); + match({$or: [{a: /a/}, {a: /o/}]}, {a: 'cat'}); + nomatch({$or: [{a: /i/}, {a: /o/}]}, {a: 'cat'}); + match({$or: [{a: /i/}, {b: /o/}]}, {a: 'cat', b: 'dog'}); // $or and $ne match({$or: [{a: {$ne: 1}}]}, {}); @@ -1231,20 +1229,20 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({$nor: [{a: {$nin: [1, 2, 3]}}, {b: {$nin: [4, 5, 6]}}]}, {b: 2}); // $nor and dot-notation - nomatch({$nor: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); - nomatch({$nor: [{"a.b": 1}, {"a.c": 1}]}, {a: {b: 1}}); - match({$nor: [{"a.b": 2}, {"a.c": 1}]}, {a: {b: 1}}); + nomatch({$nor: [{'a.b': 1}, {'a.b': 2}]}, {a: {b: 1}}); + nomatch({$nor: [{'a.b': 1}, {'a.c': 1}]}, {a: {b: 1}}); + match({$nor: [{'a.b': 2}, {'a.c': 1}]}, {a: {b: 1}}); // $nor and nested objects nomatch({$nor: [{a: {b: 1, c: 2}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); match({$nor: [{a: {b: 1, c: 3}}, {a: {b: 2, c: 1}}]}, {a: {b: 1, c: 2}}); // $nor and regexes - nomatch({$nor: [{a: /a/}]}, {a: "cat"}); - match({$nor: [{a: /o/}]}, {a: "cat"}); - nomatch({$nor: [{a: /a/}, {a: /o/}]}, {a: "cat"}); - match({$nor: [{a: /i/}, {a: /o/}]}, {a: "cat"}); - nomatch({$nor: [{a: /i/}, {b: /o/}]}, {a: "cat", b: "dog"}); + nomatch({$nor: [{a: /a/}]}, {a: 'cat'}); + match({$nor: [{a: /o/}]}, {a: 'cat'}); + nomatch({$nor: [{a: /a/}, {a: /o/}]}, {a: 'cat'}); + match({$nor: [{a: /i/}, {a: /o/}]}, {a: 'cat'}); + nomatch({$nor: [{a: /i/}, {b: /o/}]}, {a: 'cat', b: 'dog'}); // $nor and $ne nomatch({$nor: [{a: {$ne: 1}}]}, {}); @@ -1284,25 +1282,25 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({$and: [{a: 1}, {b: 2}], c: 4}, {a: 1, b: 2, c: 3}); // $and and regexes - match({$and: [{a: /a/}]}, {a: "cat"}); - match({$and: [{a: /a/i}]}, {a: "CAT"}); - nomatch({$and: [{a: /o/}]}, {a: "cat"}); - nomatch({$and: [{a: /a/}, {a: /o/}]}, {a: "cat"}); - match({$and: [{a: /a/}, {b: /o/}]}, {a: "cat", b: "dog"}); - nomatch({$and: [{a: /a/}, {b: /a/}]}, {a: "cat", b: "dog"}); + match({$and: [{a: /a/}]}, {a: 'cat'}); + match({$and: [{a: /a/i}]}, {a: 'CAT'}); + nomatch({$and: [{a: /o/}]}, {a: 'cat'}); + nomatch({$and: [{a: /a/}, {a: /o/}]}, {a: 'cat'}); + match({$and: [{a: /a/}, {b: /o/}]}, {a: 'cat', b: 'dog'}); + nomatch({$and: [{a: /a/}, {b: /a/}]}, {a: 'cat', b: 'dog'}); // $and, dot-notation, and nested objects - match({$and: [{"a.b": 1}]}, {a: {b: 1}}); + match({$and: [{'a.b': 1}]}, {a: {b: 1}}); match({$and: [{a: {b: 1}}]}, {a: {b: 1}}); - nomatch({$and: [{"a.b": 2}]}, {a: {b: 1}}); - nomatch({$and: [{"a.c": 1}]}, {a: {b: 1}}); - nomatch({$and: [{"a.b": 1}, {"a.b": 2}]}, {a: {b: 1}}); - nomatch({$and: [{"a.b": 1}, {a: {b: 2}}]}, {a: {b: 1}}); - match({$and: [{"a.b": 1}, {"c.d": 2}]}, {a: {b: 1}, c: {d: 2}}); - nomatch({$and: [{"a.b": 1}, {"c.d": 1}]}, {a: {b: 1}, c: {d: 2}}); - match({$and: [{"a.b": 1}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); - nomatch({$and: [{"a.b": 1}, {c: {d: 1}}]}, {a: {b: 1}, c: {d: 2}}); - nomatch({$and: [{"a.b": 2}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{'a.b': 2}]}, {a: {b: 1}}); + nomatch({$and: [{'a.c': 1}]}, {a: {b: 1}}); + nomatch({$and: [{'a.b': 1}, {'a.b': 2}]}, {a: {b: 1}}); + nomatch({$and: [{'a.b': 1}, {a: {b: 2}}]}, {a: {b: 1}}); + match({$and: [{'a.b': 1}, {'c.d': 2}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{'a.b': 1}, {'c.d': 1}]}, {a: {b: 1}, c: {d: 2}}); + match({$and: [{'a.b': 1}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{'a.b': 1}, {c: {d: 1}}]}, {a: {b: 1}, c: {d: 2}}); + nomatch({$and: [{'a.b': 2}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); match({$and: [{a: {b: 1}}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); nomatch({$and: [{a: {b: 2}}, {c: {d: 2}}]}, {a: {b: 1}, c: {d: 2}}); @@ -1350,81 +1348,81 @@ Tinytest.add("minimongo - selector_compiler", test => { nomatch({$and: [{a: {$not: {$lt: 2}}}, {a: {$not: {$gt: 0}}}]}, {a: 1}); // $where - match({$where: "this.a === 1"}, {a: 1}); - match({$where: "obj.a === 1"}, {a: 1}); - nomatch({$where: "this.a !== 1"}, {a: 1}); - nomatch({$where: "obj.a !== 1"}, {a: 1}); - nomatch({$where: "this.a === 1", a: 2}, {a: 1}); - match({$where: "this.a === 1", b: 2}, {a: 1, b: 2}); - match({$where: "this.a === 1 && this.b === 2"}, {a: 1, b: 2}); - match({$where: "this.a instanceof Array"}, {a: []}); - nomatch({$where: "this.a instanceof Array"}, {a: 1}); + match({$where: 'this.a === 1'}, {a: 1}); + match({$where: 'obj.a === 1'}, {a: 1}); + nomatch({$where: 'this.a !== 1'}, {a: 1}); + nomatch({$where: 'obj.a !== 1'}, {a: 1}); + nomatch({$where: 'this.a === 1', a: 2}, {a: 1}); + match({$where: 'this.a === 1', b: 2}, {a: 1, b: 2}); + match({$where: 'this.a === 1 && this.b === 2'}, {a: 1, b: 2}); + match({$where: 'this.a instanceof Array'}, {a: []}); + nomatch({$where: 'this.a instanceof Array'}, {a: 1}); // reaching into array - match({"dogs.0.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({"dogs.1.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - nomatch({"dogs.1.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({"room.1b": "bla"}, {room: {"1b": "bla"}}); + match({'dogs.0.name': 'Fido'}, {dogs: [{name: 'Fido'}, {name: 'Rex'}]}); + match({'dogs.1.name': 'Rex'}, {dogs: [{name: 'Fido'}, {name: 'Rex'}]}); + nomatch({'dogs.1.name': 'Fido'}, {dogs: [{name: 'Fido'}, {name: 'Rex'}]}); + match({'room.1b': 'bla'}, {room: {'1b': 'bla'}}); - match({"dogs.name": "Fido"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({"dogs.name": "Rex"}, {dogs: [{name: "Fido"}, {name: "Rex"}]}); - match({"animals.dogs.name": "Fido"}, - {animals: [{dogs: [{name: "Rover"}]}, - {}, - {dogs: [{name: "Fido"}, {name: "Rex"}]}]}); - match({"animals.dogs.name": "Fido"}, - {animals: [{dogs: {name: "Rex"}}, - {dogs: {name: "Fido"}}]}); - match({"animals.dogs.name": "Fido"}, - {animals: [{dogs: [{name: "Rover"}]}, - {}, - {dogs: [{name: ["Fido"]}, {name: "Rex"}]}]}); - nomatch({"dogs.name": "Fido"}, {dogs: []}); + match({'dogs.name': 'Fido'}, {dogs: [{name: 'Fido'}, {name: 'Rex'}]}); + match({'dogs.name': 'Rex'}, {dogs: [{name: 'Fido'}, {name: 'Rex'}]}); + match({'animals.dogs.name': 'Fido'}, + {animals: [{dogs: [{name: 'Rover'}]}, + {}, + {dogs: [{name: 'Fido'}, {name: 'Rex'}]}]}); + match({'animals.dogs.name': 'Fido'}, + {animals: [{dogs: {name: 'Rex'}}, + {dogs: {name: 'Fido'}}]}); + match({'animals.dogs.name': 'Fido'}, + {animals: [{dogs: [{name: 'Rover'}]}, + {}, + {dogs: [{name: ['Fido']}, {name: 'Rex'}]}]}); + nomatch({'dogs.name': 'Fido'}, {dogs: []}); // $elemMatch match({dogs: {$elemMatch: {name: /e/}}}, - {dogs: [{name: "Fido"}, {name: "Rex"}]}); + {dogs: [{name: 'Fido'}, {name: 'Rex'}]}); nomatch({dogs: {$elemMatch: {name: /a/}}}, - {dogs: [{name: "Fido"}, {name: "Rex"}]}); + {dogs: [{name: 'Fido'}, {name: 'Rex'}]}); match({dogs: {$elemMatch: {age: {$gt: 4}}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - match({dogs: {$elemMatch: {name: "Fido", age: {$gt: 4}}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); - nomatch({dogs: {$elemMatch: {name: "Fido", age: {$gt: 5}}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + {dogs: [{name: 'Fido', age: 5}, {name: 'Rex', age: 3}]}); + match({dogs: {$elemMatch: {name: 'Fido', age: {$gt: 4}}}}, + {dogs: [{name: 'Fido', age: 5}, {name: 'Rex', age: 3}]}); + nomatch({dogs: {$elemMatch: {name: 'Fido', age: {$gt: 5}}}}, + {dogs: [{name: 'Fido', age: 5}, {name: 'Rex', age: 3}]}); match({dogs: {$elemMatch: {name: /i/, age: {$gt: 4}}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + {dogs: [{name: 'Fido', age: 5}, {name: 'Rex', age: 3}]}); nomatch({dogs: {$elemMatch: {name: /e/, age: 5}}}, - {dogs: [{name: "Fido", age: 5}, {name: "Rex", age: 3}]}); + {dogs: [{name: 'Fido', age: 5}, {name: 'Rex', age: 3}]}); match({x: {$elemMatch: {y: 9}}}, {x: [{y: 9}]}); nomatch({x: {$elemMatch: {y: 9}}}, {x: [[{y: 9}]]}); match({x: {$elemMatch: {$gt: 5, $lt: 9}}}, {x: [8]}); nomatch({x: {$elemMatch: {$gt: 5, $lt: 9}}}, {x: [[8]]}); match({'a.x': {$elemMatch: {y: 9}}}, - {a: [{x: []}, {x: [{y: 9}]}]}); + {a: [{x: []}, {x: [{y: 9}]}]}); nomatch({a: {$elemMatch: {x: 5}}}, {a: {x: 5}}); match({a: {$elemMatch: {0: {$gt: 5, $lt: 9}}}}, {a: [[6]]}); - match({a: {$elemMatch: {'0.b': {$gt: 5, $lt: 9}}}}, {a: [[{b:6}]]}); + match({a: {$elemMatch: {'0.b': {$gt: 5, $lt: 9}}}}, {a: [[{b: 6}]]}); match({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1, b: 1}]}); + {a: [{x: 1, b: 1}]}); match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}], x: 1}}}, - {a: [{x: 1, b: 1}]}); + {a: [{x: 1, b: 1}]}); match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1, b: 1}]}); + {a: [{x: 1, b: 1}]}); match({a: {$elemMatch: {$or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1, b: 1}]}); + {a: [{x: 1, b: 1}]}); match({a: {$elemMatch: {$and: [{b: 1}, {x: 1}]}}}, - {a: [{x: 1, b: 1}]}); + {a: [{x: 1, b: 1}]}); nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{b: 1}]}); + {a: [{b: 1}]}); nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1}]}); + {a: [{x: 1}]}); nomatch({a: {$elemMatch: {x: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1}, {b: 1}]}); + {a: [{x: 1}, {b: 1}]}); test.throws(() => { match({a: {$elemMatch: {$gte: 1, $or: [{a: 1}, {b: 1}]}}}, - {a: [{x: 1, b: 1}]}); + {a: [{x: 1, b: 1}]}); }); test.throws(() => { @@ -1432,14 +1430,14 @@ Tinytest.add("minimongo - selector_compiler", test => { }); // $comment - match({a: 5, $comment: "asdf"}, {a: 5}); - nomatch({a: 6, $comment: "asdf"}, {a: 5}); + match({a: 5, $comment: 'asdf'}, {a: 5}); + nomatch({a: 6, $comment: 'asdf'}, {a: 5}); // XXX still needs tests: // - non-scalar arguments to $gt, $lt, etc }); -Tinytest.add("minimongo - projection_compiler", test => { +Tinytest.add('minimongo - projection_compiler', test => { const testProjection = (projection, tests) => { const projection_f = LocalCollection._compileProjection(projection); const equalNonStrict = (a, b, desc) => { @@ -1457,160 +1455,160 @@ Tinytest.add("minimongo - projection_compiler", test => { }, expectedError); }; - testProjection({ 'foo': 1, 'bar': 1 }, [ - [{ foo: 42, bar: "something", baz: "else" }, - { foo: 42, bar: "something" }, - "simplest - whitelist"], + testProjection({ foo: 1, bar: 1 }, [ + [{ foo: 42, bar: 'something', baz: 'else' }, + { foo: 42, bar: 'something' }, + 'simplest - whitelist'], [{ foo: { nested: 17 }, baz: {} }, - { foo: { nested: 17 } }, - "nested whitelisted field"], + { foo: { nested: 17 } }, + 'nested whitelisted field'], - [{ _id: "uid", bazbaz: 42 }, - { _id: "uid" }, - "simplest whitelist - preserve _id"] + [{ _id: 'uid', bazbaz: 42 }, + { _id: 'uid' }, + 'simplest whitelist - preserve _id'], ]); - testProjection({ 'foo': 0, 'bar': 0 }, [ - [{ foo: 42, bar: "something", baz: "else" }, - { baz: "else" }, - "simplest - blacklist"], + testProjection({ foo: 0, bar: 0 }, [ + [{ foo: 42, bar: 'something', baz: 'else' }, + { baz: 'else' }, + 'simplest - blacklist'], - [{ foo: { nested: 17 }, baz: { foo: "something" } }, - { baz: { foo: "something" } }, - "nested blacklisted field"], + [{ foo: { nested: 17 }, baz: { foo: 'something' } }, + { baz: { foo: 'something' } }, + 'nested blacklisted field'], - [{ _id: "uid", bazbaz: 42 }, - { _id: "uid", bazbaz: 42 }, - "simplest blacklist - preserve _id"] + [{ _id: 'uid', bazbaz: 42 }, + { _id: 'uid', bazbaz: 42 }, + 'simplest blacklist - preserve _id'], ]); testProjection({ _id: 0, foo: 1 }, [ - [{ foo: 42, bar: 33, _id: "uid" }, - { foo: 42 }, - "whitelist - _id blacklisted"] + [{ foo: 42, bar: 33, _id: 'uid' }, + { foo: 42 }, + 'whitelist - _id blacklisted'], ]); testProjection({ _id: 0, foo: 0 }, [ - [{ foo: 42, bar: 33, _id: "uid" }, - { bar: 33 }, - "blacklist - _id blacklisted"] + [{ foo: 42, bar: 33, _id: 'uid' }, + { bar: 33 }, + 'blacklist - _id blacklisted'], ]); testProjection({ 'foo.bar.baz': 1 }, [ - [{ foo: { meh: "fur", bar: { baz: 42 }, tr: 1 }, bar: 33, baz: 'trolololo' }, - { foo: { bar: { baz: 42 } } }, - "whitelist nested"], + [{ foo: { meh: 'fur', bar: { baz: 42 }, tr: 1 }, bar: 33, baz: 'trolololo' }, + { foo: { bar: { baz: 42 } } }, + 'whitelist nested'], // Behavior of this test is looked up in actual mongo - [{ foo: { meh: "fur", bar: "nope", tr: 1 }, bar: 33, baz: 'trolololo' }, - { foo: {} }, - "whitelist nested - path not found in doc, different type"], + [{ foo: { meh: 'fur', bar: 'nope', tr: 1 }, bar: 33, baz: 'trolololo' }, + { foo: {} }, + 'whitelist nested - path not found in doc, different type'], // Behavior of this test is looked up in actual mongo - [{ foo: { meh: "fur", bar: [], tr: 1 }, bar: 33, baz: 'trolololo' }, - { foo: { bar: [] } }, - "whitelist nested - path not found in doc"] + [{ foo: { meh: 'fur', bar: [], tr: 1 }, bar: 33, baz: 'trolololo' }, + { foo: { bar: [] } }, + 'whitelist nested - path not found in doc'], ]); testProjection({ 'hope.humanity': 0, 'hope.people': 0 }, [ - [{ hope: { humanity: "lost", people: 'broken', candies: 'long live!' } }, - { hope: { candies: 'long live!' } }, - "blacklist nested"], + [{ hope: { humanity: 'lost', people: 'broken', candies: 'long live!' } }, + { hope: { candies: 'long live!' } }, + 'blacklist nested'], - [{ hope: "new" }, - { hope: "new" }, - "blacklist nested - path not found in doc"] + [{ hope: 'new' }, + { hope: 'new' }, + 'blacklist nested - path not found in doc'], ]); testProjection({ _id: 1 }, [ - [{ _id: 42, x: 1, y: { z: "2" } }, - { _id: 42 }, - "_id whitelisted"], + [{ _id: 42, x: 1, y: { z: '2' } }, + { _id: 42 }, + '_id whitelisted'], [{ _id: 33 }, - { _id: 33 }, - "_id whitelisted, _id only"], + { _id: 33 }, + '_id whitelisted, _id only'], [{ x: 1 }, - {}, - "_id whitelisted, no _id"] + {}, + '_id whitelisted, no _id'], ]); testProjection({ _id: 0 }, [ - [{ _id: 42, x: 1, y: { z: "2" } }, - { x: 1, y: { z: "2" } }, - "_id blacklisted"], + [{ _id: 42, x: 1, y: { z: '2' } }, + { x: 1, y: { z: '2' } }, + '_id blacklisted'], [{ _id: 33 }, - {}, - "_id blacklisted, _id only"], + {}, + '_id blacklisted, _id only'], [{ x: 1 }, - { x: 1 }, - "_id blacklisted, no _id"] + { x: 1 }, + '_id blacklisted, no _id'], ]); testProjection({}, [ - [{ a: 1, b: 2, c: "3" }, - { a: 1, b: 2, c: "3" }, - "empty projection"] + [{ a: 1, b: 2, c: '3' }, + { a: 1, b: 2, c: '3' }, + 'empty projection'], ]); testCompileProjectionThrows( - { 'inc': 1, 'excl': 0 }, - "You cannot currently mix including and excluding fields"); + { inc: 1, excl: 0 }, + 'You cannot currently mix including and excluding fields'); testCompileProjectionThrows( { _id: 1, a: 0 }, - "You cannot currently mix including and excluding fields"); + 'You cannot currently mix including and excluding fields'); testCompileProjectionThrows( - { 'a': 1, 'a.b': 1 }, - "using both of them may trigger unexpected behavior"); + { a: 1, 'a.b': 1 }, + 'using both of them may trigger unexpected behavior'); testCompileProjectionThrows( - { 'a.b.c': 1, 'a.b': 1, 'a': 1 }, - "using both of them may trigger unexpected behavior"); + { 'a.b.c': 1, 'a.b': 1, a: 1 }, + 'using both of them may trigger unexpected behavior'); - testCompileProjectionThrows("some string", "fields option must be an object"); + testCompileProjectionThrows('some string', 'fields option must be an object'); }); -Tinytest.add("minimongo - fetch with fields", test => { +Tinytest.add('minimongo - fetch with fields', test => { const c = new LocalCollection(); Array.from({length: 30}, (x, i) => { c.insert({ something: Random.id(), anything: { - foo: "bar", - cool: "hot" + foo: 'bar', + cool: 'hot', }, nothing: i, - i + i, }); }); // Test just a regular fetch with some projection let fetchResults = c.find({}, { fields: { - 'something': 1, - 'anything.foo': 1 + something: 1, + 'anything.foo': 1, } }).fetch(); test.isTrue(fetchResults.every(x => x && x.something && x.anything && x.anything.foo && - x.anything.foo === "bar" && + x.anything.foo === 'bar' && !x.hasOwnProperty('nothing') && !x.anything.hasOwnProperty('cool'))); // Test with a selector, even field used in the selector is excluded in the // projection fetchResults = c.find({ - nothing: { $gte: 5 } + nothing: { $gte: 5 }, }, { - fields: { nothing: 0 } + fields: { nothing: 0 }, }).fetch(); test.isTrue(fetchResults.every(x => x && x.something && x.anything && - x.anything.foo === "bar" && - x.anything.cool === "hot" && + x.anything.foo === 'bar' && + x.anything.cool === 'hot' && !x.hasOwnProperty('nothing') && x.i && x.i >= 5)); @@ -1622,14 +1620,14 @@ Tinytest.add("minimongo - fetch with fields", test => { // following find will get indexes [10..20) sorted by nothing fetchResults = c.find({}, { sort: { - nothing: 1 + nothing: 1, }, limit: 10, skip: 10, fields: { i: 1, - something: 1 - } + something: 1, + }, }).fetch(); test.isTrue(fetchResults.every(x => x && @@ -1638,7 +1636,7 @@ Tinytest.add("minimongo - fetch with fields", test => { fetchResults.forEach((x, i, arr) => { if (!i) return; - test.isTrue(x.i === arr[i-1].i + 1); + test.isTrue(x.i === arr[i - 1].i + 1); }); // Temporary unsupported operators @@ -1654,7 +1652,7 @@ Tinytest.add("minimongo - fetch with fields", test => { }); }); -Tinytest.add("minimongo - fetch with projection, subarrays", test => { +Tinytest.add('minimongo - fetch with projection, subarrays', test => { // Apparently projection of type 'foo.bar.x' for // { foo: [ { bar: { x: 42 } }, { bar: { x: 3 } } ] } // should return exactly this object. More precisely, arrays are considered as @@ -1665,19 +1663,19 @@ Tinytest.add("minimongo - fetch with projection, subarrays", test => { c.insert({ setA: [{ fieldA: 42, - fieldB: 33 + fieldB: 33, }, { - fieldA: "the good", - fieldB: "the bad", - fieldC: "the ugly" + fieldA: 'the good', + fieldB: 'the bad', + fieldC: 'the ugly', }], setB: [{ anotherA: { }, - anotherB: "meh" + anotherB: 'meh', }, { anotherA: 1234, - anotherB: 431 - }] + anotherB: 431, + }], }); const equalNonStrict = (a, b, desc) => { @@ -1690,58 +1688,58 @@ Tinytest.add("minimongo - fetch with projection, subarrays", test => { }; testForProjection({ 'setA.fieldA': 1, 'setB.anotherB': 1, _id: 0 }, - { - setA: [{ fieldA: 42 }, { fieldA: "the good" }], - setB: [{ anotherB: "meh" }, { anotherB: 431 }] - }); + { + setA: [{ fieldA: 42 }, { fieldA: 'the good' }], + setB: [{ anotherB: 'meh' }, { anotherB: 431 }], + }); testForProjection({ 'setA.fieldA': 0, 'setB.anotherA': 0, _id: 0 }, - { - setA: [{fieldB:33}, {fieldB:"the bad",fieldC:"the ugly"}], - setB: [{ anotherB: "meh" }, { anotherB: 431 }] - }); + { + setA: [{fieldB: 33}, {fieldB: 'the bad', fieldC: 'the ugly'}], + setB: [{ anotherB: 'meh' }, { anotherB: 431 }], + }); c.remove({}); - c.insert({a:[[{b:1,c:2},{b:2,c:4}],{b:3,c:5},[{b:4, c:9}]]}); + c.insert({a: [[{b: 1, c: 2}, {b: 2, c: 4}], {b: 3, c: 5}, [{b: 4, c: 9}]]}); testForProjection({ 'a.b': 1, _id: 0 }, - {a: [ [ { b: 1 }, { b: 2 } ], { b: 3 }, [ { b: 4 } ] ] }); + {a: [ [ { b: 1 }, { b: 2 } ], { b: 3 }, [ { b: 4 } ] ] }); testForProjection({ 'a.b': 0, _id: 0 }, - {a: [ [ { c: 2 }, { c: 4 } ], { c: 5 }, [ { c: 9 } ] ] }); + {a: [ [ { c: 2 }, { c: 4 } ], { c: 5 }, [ { c: 9 } ] ] }); }); -Tinytest.add("minimongo - fetch with projection, deep copy", test => { +Tinytest.add('minimongo - fetch with projection, deep copy', test => { // Compiled fields projection defines the contract: returned document doesn't // retain anything from the passed argument. const doc = { a: { x: 42 }, b: { - y: { z: 33 } + y: { z: 33 }, }, - c: "asdf" + c: 'asdf', }; let fields = { - 'a': 1, - 'b.y': 1 + a: 1, + 'b.y': 1, }; let projectionFn = LocalCollection._compileProjection(fields); let filteredDoc = projectionFn(doc); doc.a.x++; doc.b.y.z--; - test.equal(filteredDoc.a.x, 42, "projection returning deep copy - including"); - test.equal(filteredDoc.b.y.z, 33, "projection returning deep copy - including"); + test.equal(filteredDoc.a.x, 42, 'projection returning deep copy - including'); + test.equal(filteredDoc.b.y.z, 33, 'projection returning deep copy - including'); fields = { c: 0 }; projectionFn = LocalCollection._compileProjection(fields); filteredDoc = projectionFn(doc); doc.a.x = 5; - test.equal(filteredDoc.a.x, 43, "projection returning deep copy - excluding"); + test.equal(filteredDoc.a.x, 43, 'projection returning deep copy - excluding'); }); -Tinytest.add("minimongo - observe ordered with projection", test => { +Tinytest.add('minimongo - observe ordered with projection', test => { // These tests are copy-paste from "minimongo -observe ordered", // slightly modified to test projection const operations = []; @@ -1752,28 +1750,28 @@ Tinytest.add("minimongo - observe ordered with projection", test => { handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(cbs); test.isTrue(handle.collection === c); - c.insert({_id: 'foo', a:1, b:2}); - test.equal(operations.shift(), ['added', {a:1}, 0, null]); - c.update({a:1}, {$set: {a: 2, b: 1}}); - test.equal(operations.shift(), ['changed', {a:2}, 0, {a:1}]); - c.insert({_id: 'bar', a:10, c: 33}); - test.equal(operations.shift(), ['added', {a:10}, 1, null]); + c.insert({_id: 'foo', a: 1, b: 2}); + test.equal(operations.shift(), ['added', {a: 1}, 0, null]); + c.update({a: 1}, {$set: {a: 2, b: 1}}); + test.equal(operations.shift(), ['changed', {a: 2}, 0, {a: 1}]); + c.insert({_id: 'bar', a: 10, c: 33}); + test.equal(operations.shift(), ['added', {a: 10}, 1, null]); c.update({}, {$inc: {a: 1}}, {multi: true}); c.update({}, {$inc: {c: 1}}, {multi: true}); - test.equal(operations.shift(), ['changed', {a:3}, 0, {a:2}]); - test.equal(operations.shift(), ['changed', {a:11}, 1, {a:10}]); - c.update({a:11}, {a:1, b:44}); - test.equal(operations.shift(), ['changed', {a:1}, 1, {a:11}]); - test.equal(operations.shift(), ['moved', {a:1}, 1, 0, 'foo']); - c.remove({a:2}); + test.equal(operations.shift(), ['changed', {a: 3}, 0, {a: 2}]); + test.equal(operations.shift(), ['changed', {a: 11}, 1, {a: 10}]); + c.update({a: 11}, {a: 1, b: 44}); + test.equal(operations.shift(), ['changed', {a: 1}, 1, {a: 11}]); + test.equal(operations.shift(), ['moved', {a: 1}, 1, 0, 'foo']); + c.remove({a: 2}); test.equal(operations.shift(), undefined); - c.remove({a:3}); - test.equal(operations.shift(), ['removed', 'foo', 1, {a:3}]); + c.remove({a: 3}); + test.equal(operations.shift(), ['removed', 'foo', 1, {a: 3}]); // test stop handle.stop(); const idA2 = Random.id(); - c.insert({_id: idA2, a:2}); + c.insert({_id: idA2, a: 2}); test.equal(operations.shift(), undefined); const cursor = c.find({}, {fields: {a: 1, _id: 0}}); @@ -1786,63 +1784,63 @@ Tinytest.add("minimongo - observe ordered with projection", test => { // test initial inserts (and backwards sort) handle = c.find({}, {sort: {a: -1}, fields: { a: 1 } }).observe(cbs); - test.equal(operations.shift(), ['added', {a:2}, 0, null]); - test.equal(operations.shift(), ['added', {a:1}, 1, null]); + test.equal(operations.shift(), ['added', {a: 2}, 0, null]); + test.equal(operations.shift(), ['added', {a: 1}, 1, null]); handle.stop(); // test _suppress_initial handle = c.find({}, {sort: {a: -1}, fields: { a: 1 }}).observe(Object.assign(cbs, {_suppress_initial: true})); test.equal(operations.shift(), undefined); - c.insert({a:100, b: { foo: "bar" }}); - test.equal(operations.shift(), ['added', {a:100}, 0, idA2]); + c.insert({a: 100, b: { foo: 'bar' }}); + test.equal(operations.shift(), ['added', {a: 100}, 0, idA2]); handle.stop(); // test skip and limit. c.remove({}); - handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2, fields: { 'blacklisted': 0 }}).observe(cbs); + handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2, fields: { blacklisted: 0 }}).observe(cbs); test.equal(operations.shift(), undefined); - c.insert({a:1, blacklisted:1324}); + c.insert({a: 1, blacklisted: 1324}); test.equal(operations.shift(), undefined); - c.insert({_id: 'foo', a:2, blacklisted:["something"]}); - test.equal(operations.shift(), ['added', {a:2}, 0, null]); - c.insert({a:3, blacklisted: { 2: 3 }}); - test.equal(operations.shift(), ['added', {a:3}, 1, null]); - c.insert({a:4, blacklisted: 6}); + c.insert({_id: 'foo', a: 2, blacklisted: ['something']}); + test.equal(operations.shift(), ['added', {a: 2}, 0, null]); + c.insert({a: 3, blacklisted: { 2: 3 }}); + test.equal(operations.shift(), ['added', {a: 3}, 1, null]); + c.insert({a: 4, blacklisted: 6}); test.equal(operations.shift(), undefined); - c.update({a:1}, {a:0, blacklisted:4444}); + c.update({a: 1}, {a: 0, blacklisted: 4444}); test.equal(operations.shift(), undefined); - c.update({a:0}, {a:5, blacklisted:11111}); - test.equal(operations.shift(), ['removed', 'foo', 0, {a:2}]); - test.equal(operations.shift(), ['added', {a:4}, 1, null]); - c.update({a:3}, {a:3.5, blacklisted:333.4444}); - test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]); + c.update({a: 0}, {a: 5, blacklisted: 11111}); + test.equal(operations.shift(), ['removed', 'foo', 0, {a: 2}]); + test.equal(operations.shift(), ['added', {a: 4}, 1, null]); + c.update({a: 3}, {a: 3.5, blacklisted: 333.4444}); + test.equal(operations.shift(), ['changed', {a: 3.5}, 0, {a: 3}]); handle.stop(); // test _no_indices c.remove({}); handle = c.find({}, {sort: {a: 1}, fields: { a: 1 }}).observe(Object.assign(cbs, {_no_indices: true})); - c.insert({_id: 'foo', a:1, zoo: "crazy"}); - test.equal(operations.shift(), ['added', {a:1}, -1, null]); - c.update({a:1}, {$set: {a: 2, foobar: "player"}}); - test.equal(operations.shift(), ['changed', {a:2}, -1, {a:1}]); - c.insert({a:10, b:123.45}); - test.equal(operations.shift(), ['added', {a:10}, -1, null]); - c.update({}, {$inc: {a: 1, b:2}}, {multi: true}); - test.equal(operations.shift(), ['changed', {a:3}, -1, {a:2}]); - test.equal(operations.shift(), ['changed', {a:11}, -1, {a:10}]); - c.update({a:11, b:125.45}, {a:1, b:444}); - test.equal(operations.shift(), ['changed', {a:1}, -1, {a:11}]); - test.equal(operations.shift(), ['moved', {a:1}, -1, -1, 'foo']); - c.remove({a:2}); + c.insert({_id: 'foo', a: 1, zoo: 'crazy'}); + test.equal(operations.shift(), ['added', {a: 1}, -1, null]); + c.update({a: 1}, {$set: {a: 2, foobar: 'player'}}); + test.equal(operations.shift(), ['changed', {a: 2}, -1, {a: 1}]); + c.insert({a: 10, b: 123.45}); + test.equal(operations.shift(), ['added', {a: 10}, -1, null]); + c.update({}, {$inc: {a: 1, b: 2}}, {multi: true}); + test.equal(operations.shift(), ['changed', {a: 3}, -1, {a: 2}]); + test.equal(operations.shift(), ['changed', {a: 11}, -1, {a: 10}]); + c.update({a: 11, b: 125.45}, {a: 1, b: 444}); + test.equal(operations.shift(), ['changed', {a: 1}, -1, {a: 11}]); + test.equal(operations.shift(), ['moved', {a: 1}, -1, -1, 'foo']); + c.remove({a: 2}); test.equal(operations.shift(), undefined); - c.remove({a:3}); - test.equal(operations.shift(), ['removed', 'foo', -1, {a:3}]); + c.remove({a: 3}); + test.equal(operations.shift(), ['removed', 'foo', -1, {a: 3}]); handle.stop(); }); -Tinytest.add("minimongo - ordering", test => { +Tinytest.add('minimongo - ordering', test => { const shortBinary = EJSON.newBinary(1); shortBinary[0] = 128; const longBinary1 = EJSON.newBinary(2); @@ -1857,15 +1855,15 @@ Tinytest.add("minimongo - ordering", test => { assert_ordering(test, LocalCollection._f._cmp, [ null, 1, 2.2, 3, - "03", "1", "11", "2", "a", "aaa", + '03', '1', '11', '2', 'a', 'aaa', {}, {a: 2}, {a: 3}, {a: 3, b: 4}, {b: 4}, {b: 4, a: 3}, {b: {}}, {b: [1, 2, 3]}, {b: [1, 2, 4]}, - [], [1, 2], [1, 2, 3], [1, 2, 4], [1, 2, "4"], [1, 2, [4]], + [], [1, 2], [1, 2, 3], [1, 2, 4], [1, 2, '4'], [1, 2, [4]], shortBinary, longBinary1, longBinary2, - new MongoID.ObjectID("1234567890abcd1234567890"), - new MongoID.ObjectID("abcd1234567890abcd123456"), + new MongoID.ObjectID('1234567890abcd1234567890'), + new MongoID.ObjectID('abcd1234567890abcd123456'), false, true, - date1, date2 + date1, date2, ]); // document ordering under a sort specification @@ -1881,25 +1879,25 @@ Tinytest.add("minimongo - ordering", test => { // verified with MongoDB 2.2.1.) We don't define the relative order of {a: []} // and {c: 1} is undefined (MongoDB does seem to care but it's not clear how // or why). - verify([{"a" : 1}, ["a"], [["a", "asc"]]], - [{a: []}, {a: 1}, {a: {}}, {a: true}]); - verify([{"a" : 1}, ["a"], [["a", "asc"]]], - [{c: 1}, {a: 1}, {a: {}}, {a: true}]); - verify([{"a" : -1}, [["a", "desc"]]], - [{a: true}, {a: {}}, {a: 1}, {c: 1}]); - verify([{"a" : -1}, [["a", "desc"]]], - [{a: true}, {a: {}}, {a: 1}, {a: []}]); + verify([{a: 1}, ['a'], [['a', 'asc']]], + [{a: []}, {a: 1}, {a: {}}, {a: true}]); + verify([{a: 1}, ['a'], [['a', 'asc']]], + [{c: 1}, {a: 1}, {a: {}}, {a: true}]); + verify([{a: -1}, [['a', 'desc']]], + [{a: true}, {a: {}}, {a: 1}, {c: 1}]); + verify([{a: -1}, [['a', 'desc']]], + [{a: true}, {a: {}}, {a: 1}, {a: []}]); - verify([{"a" : 1, "b": -1}, ["a", ["b", "desc"]], - [["a", "asc"], ["b", "desc"]]], - [{c: 1}, {a: 1, b: 3}, {a: 1, b: 2}, {a: 2, b: 0}]); + verify([{a: 1, b: -1}, ['a', ['b', 'desc']], + [['a', 'asc'], ['b', 'desc']]], + [{c: 1}, {a: 1, b: 3}, {a: 1, b: 2}, {a: 2, b: 0}]); - verify([{"a" : 1, "b": 1}, ["a", "b"], - [["a", "asc"], ["b", "asc"]]], - [{c: 1}, {a: 1, b: 2}, {a: 1, b: 3}, {a: 2, b: 0}]); + verify([{a: 1, b: 1}, ['a', 'b'], + [['a', 'asc'], ['b', 'asc']]], + [{c: 1}, {a: 1, b: 2}, {a: 1, b: 3}, {a: 2, b: 0}]); test.throws(() => { - new Minimongo.Sorter("a"); + new Minimongo.Sorter('a'); }); test.throws(() => { @@ -1913,54 +1911,54 @@ Tinytest.add("minimongo - ordering", test => { }); // No sort spec implies everything equal. - test.equal(new Minimongo.Sorter({}).getComparator()({a:1}, {a:2}), 0); + test.equal(new Minimongo.Sorter({}).getComparator()({a: 1}, {a: 2}), 0); // All sorts of array edge cases! // Increasing sort sorts by the smallest element it finds; 1 < 2. verify({a: 1}, [ {a: [1, 10, 20]}, - {a: [5, 2, 99]} + {a: [5, 2, 99]}, ]); // Decreasing sorts by largest it finds; 99 > 20. verify({a: -1}, [ {a: [5, 2, 99]}, - {a: [1, 10, 20]} + {a: [1, 10, 20]}, ]); // Can also sort by specific array indices. verify({'a.1': 1}, [ {a: [5, 2, 99]}, - {a: [1, 10, 20]} + {a: [1, 10, 20]}, ]); // We do NOT expand sub-arrays, so the minimum in the second doc is 5, not // -20. (Numbers always sort before arrays.) verify({a: 1}, [ {a: [1, [10, 15], 20]}, - {a: [5, [-5, -20], 18]} + {a: [5, [-5, -20], 18]}, ]); // The maximum in each of these is the array, since arrays are "greater" than // numbers. And [10, 15] is greater than [-5, -20]. verify({a: -1}, [ {a: [1, [10, 15], 20]}, - {a: [5, [-5, -20], 18]} + {a: [5, [-5, -20], 18]}, ]); // 'a.0' here ONLY means "first element of a", not "first element of something // found in a", so it CANNOT find the 10 or -5. verify({'a.0': 1}, [ {a: [1, [10, 15], 20]}, - {a: [5, [-5, -20], 18]} + {a: [5, [-5, -20], 18]}, ]); verify({'a.0': -1}, [ {a: [5, [-5, -20], 18]}, - {a: [1, [10, 15], 20]} + {a: [1, [10, 15], 20]}, ]); // Similarly, this is just comparing [-5,-20] to [10, 15]. verify({'a.1': 1}, [ {a: [5, [-5, -20], 18]}, - {a: [1, [10, 15], 20]} + {a: [1, [10, 15], 20]}, ]); verify({'a.1': -1}, [ {a: [1, [10, 15], 20]}, - {a: [5, [-5, -20], 18]} + {a: [5, [-5, -20], 18]}, ]); // Here we are just comparing [10,15] directly to [19,3] (and NOT also // iterating over the numbers; this is implemented by setting dontIterate in @@ -1968,28 +1966,28 @@ Tinytest.add("minimongo - ordering", test => { // number you can find there. verify({'a.1': 1}, [ {a: [1, [10, 15], 20]}, - {a: [5, [19, 3], 18]} + {a: [5, [19, 3], 18]}, ]); verify({'a.1': -1}, [ {a: [5, [19, 3], 18]}, - {a: [1, [10, 15], 20]} + {a: [1, [10, 15], 20]}, ]); // Minimal elements are 1 and 5. verify({a: 1}, [ {a: [1, [10, 15], 20]}, - {a: [5, [19, 3], 18]} + {a: [5, [19, 3], 18]}, ]); // Maximal elements are [19,3] and [10,15] (because arrays sort higher than // numbers), even though there's a 20 floating around. verify({a: -1}, [ {a: [5, [19, 3], 18]}, - {a: [1, [10, 15], 20]} + {a: [1, [10, 15], 20]}, ]); // Maximal elements are [10,15] and [3,19]. [10,15] is bigger even though 19 // is the biggest number in them, because array comparison is lexicographic. verify({a: -1}, [ {a: [1, [10, 15], 20]}, - {a: [5, [3, 19], 18]} + {a: [5, [3, 19], 18]}, ]); // (0,4) < (0,5), so they go in this order. It's not correct to consider @@ -1997,47 +1995,47 @@ Tinytest.add("minimongo - ordering", test => { // different a-branches. verify({'a.x': 1, 'a.y': 1}, [ {a: [{x: 0, y: 4}]}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}]} + {a: [{x: 0, y: 5}, {x: 1, y: 3}]}, ]); verify({'a.0.s': 1}, [ {a: [ {s: 1} ]}, - {a: [ {s: 2} ]} + {a: [ {s: 2} ]}, ]); }); -Tinytest.add("minimongo - sort", test => { +Tinytest.add('minimongo - sort', test => { const c = new LocalCollection(); - for (let i = 0; i < 50; i++) - for (let j = 0; j < 2; j++) - c.insert({a: i, b: j, _id: `${i}_${j}`}); + for (let i = 0; i < 50; i++) { + for (let j = 0; j < 2; j++) {c.insert({a: i, b: j, _id: `${i}_${j}`});} + } test.equal( c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, limit: 5}).fetch(), [ - {a: 11, b: 1, _id: "11_1"}, - {a: 12, b: 1, _id: "12_1"}, - {a: 13, b: 1, _id: "13_1"}, - {a: 14, b: 1, _id: "14_1"}, - {a: 15, b: 1, _id: "15_1"}]); + {a: 11, b: 1, _id: '11_1'}, + {a: 12, b: 1, _id: '12_1'}, + {a: 13, b: 1, _id: '13_1'}, + {a: 14, b: 1, _id: '14_1'}, + {a: 15, b: 1, _id: '15_1'}]); test.equal( c.find({a: {$gt: 10}}, {sort: {b: -1, a: 1}, skip: 3, limit: 5}).fetch(), [ - {a: 14, b: 1, _id: "14_1"}, - {a: 15, b: 1, _id: "15_1"}, - {a: 16, b: 1, _id: "16_1"}, - {a: 17, b: 1, _id: "17_1"}, - {a: 18, b: 1, _id: "18_1"}]); + {a: 14, b: 1, _id: '14_1'}, + {a: 15, b: 1, _id: '15_1'}, + {a: 16, b: 1, _id: '16_1'}, + {a: 17, b: 1, _id: '17_1'}, + {a: 18, b: 1, _id: '18_1'}]); test.equal( c.find({a: {$gte: 20}}, {sort: {a: 1, b: -1}, skip: 50, limit: 5}).fetch(), [ - {a: 45, b: 1, _id: "45_1"}, - {a: 45, b: 0, _id: "45_0"}, - {a: 46, b: 1, _id: "46_1"}, - {a: 46, b: 0, _id: "46_0"}, - {a: 47, b: 1, _id: "47_1"}]); + {a: 45, b: 1, _id: '45_1'}, + {a: 45, b: 0, _id: '45_0'}, + {a: 46, b: 1, _id: '46_1'}, + {a: 46, b: 0, _id: '46_0'}, + {a: 47, b: 1, _id: '47_1'}]); }); -Tinytest.add("minimongo - subkey sort", test => { +Tinytest.add('minimongo - subkey sort', test => { const c = new LocalCollection(); // normal case @@ -2070,7 +2068,7 @@ Tinytest.add("minimongo - subkey sort", test => { test.equal(c.find({}, {sort: {'a.nope.c': -1}}).count(), 6); }); -Tinytest.add("minimongo - array sort", test => { +Tinytest.add('minimongo - array sort', test => { const c = new LocalCollection(); // "up" and "down" are the indices that the docs should have when sorted @@ -2094,20 +2092,19 @@ Tinytest.add("minimongo - array sort", test => { const testCursorMatchesField = (cursor, field) => { const fieldValues = []; c.find().forEach(doc => { - if (doc.hasOwnProperty(field)) - fieldValues.push(doc[field]); + if (doc.hasOwnProperty(field)) {fieldValues.push(doc[field]);} }); test.equal(cursor.fetch().map(doc => doc[field]), - Array.from({length: Math.max.apply(null, fieldValues) + 1}, (x, i) => i)); + Array.from({length: Math.max.apply(null, fieldValues) + 1}, (x, i) => i)); }; testCursorMatchesField(c.find({}, {sort: {'a.x': 1}}), 'up'); testCursorMatchesField(c.find({}, {sort: {'a.x': -1}}), 'down'); testCursorMatchesField(c.find({'a.x': {$gt: 1}}, {sort: {'a.x': 1}}), - 'selected'); + 'selected'); }); -Tinytest.add("minimongo - sort keys", test => { +Tinytest.add('minimongo - sort keys', test => { const keyListToObject = keyList => { const obj = {}; keyList.forEach(key => { @@ -2137,40 +2134,40 @@ Tinytest.add("minimongo - sort keys", test => { // Just non-array fields. testKeys({'a.x': 1, 'a.y': 1}, - {a: {x: 0, y: 5}}, - [[0,5]]); + {a: {x: 0, y: 5}}, + [[0, 5]]); // Ensure that we don't get [0,3] and [1,5]. testKeys({'a.x': 1, 'a.y': 1}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}]}, - [[0,5], [1,3]]); + {a: [{x: 0, y: 5}, {x: 1, y: 3}]}, + [[0, 5], [1, 3]]); // Ensure we can combine "array fields" with "non-array fields". testKeys({'a.x': 1, 'a.y': 1, b: -1}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, - [[0,5,42], [1,3,42]]); + {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, + [[0, 5, 42], [1, 3, 42]]); testKeys({b: -1, 'a.x': 1, 'a.y': 1}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, - [[42,0,5], [42,1,3]]); + {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, + [[42, 0, 5], [42, 1, 3]]); testKeys({'a.x': 1, b: -1, 'a.y': 1}, - {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, - [[0,42,5], [1,42,3]]); + {a: [{x: 0, y: 5}, {x: 1, y: 3}], b: 42}, + [[0, 42, 5], [1, 42, 3]]); testKeys({a: 1, b: 1}, - {a: [1, 2, 3], b: 42}, - [[1,42], [2,42], [3,42]]); + {a: [1, 2, 3], b: 42}, + [[1, 42], [2, 42], [3, 42]]); // Don't support multiple arrays at the same level. testParallelError({a: 1, b: 1}, - {a: [1, 2, 3], b: [42]}); + {a: [1, 2, 3], b: [42]}); // We are MORE STRICT than Mongo here; Mongo supports this! // XXX support this too #NestedArraySort testParallelError({'a.x': 1, 'a.y': 1}, - {a: [{x: 1, y: [2, 3]}, - {x: 2, y: [4, 5]}]}); + {a: [{x: 1, y: [2, 3]}, + {x: 2, y: [4, 5]}]}); }); -Tinytest.add("minimongo - sort key filter", test => { +Tinytest.add('minimongo - sort key filter', test => { const testOrder = (sortSpec, selector, doc1, doc2) => { const matcher = new Minimongo.Matcher(selector); const sorter = new Minimongo.Sorter(sortSpec, {matcher}); @@ -2180,11 +2177,11 @@ Tinytest.add("minimongo - sort key filter", test => { }; testOrder({'a.x': 1}, {'a.x': {$gt: 1}}, - {a: {x: 3}}, - {a: {x: [1, 4]}}); + {a: {x: 3}}, + {a: {x: [1, 4]}}); testOrder({'a.x': 1}, {'a.x': {$gt: 0}}, - {a: {x: [1, 4]}}, - {a: {x: 3}}); + {a: {x: [1, 4]}}, + {a: {x: 3}}); const keyCompatible = (sortSpec, selector, key, compatible) => { const matcher = new Minimongo.Matcher(selector); @@ -2225,33 +2222,33 @@ Tinytest.add("minimongo - sort key filter", test => { keyCompatible({a: 1}, {a: /^foo+/}, ['foooobar'], true); keyCompatible({a: 1}, {a: /^foo+/}, ['afoooo'], false); keyCompatible({a: 1}, {a: /^foo+/}, [''], false); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foo'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foooo'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['foooobar'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, ['afoooo'], false); - keyCompatible({a: 1}, {a: {$regex: "^foo+"}}, [''], false); + keyCompatible({a: 1}, {a: {$regex: '^foo+'}}, ['foo'], true); + keyCompatible({a: 1}, {a: {$regex: '^foo+'}}, ['foooo'], true); + keyCompatible({a: 1}, {a: {$regex: '^foo+'}}, ['foooobar'], true); + keyCompatible({a: 1}, {a: {$regex: '^foo+'}}, ['afoooo'], false); + keyCompatible({a: 1}, {a: {$regex: '^foo+'}}, [''], false); keyCompatible({a: 1}, {a: /^foo+/i}, ['foo'], true); // Key compatibility check appears to be turned off for regexps with flags. keyCompatible({a: 1}, {a: /^foo+/i}, ['bar'], true); keyCompatible({a: 1}, {a: /^foo+/m}, ['bar'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+", $options: "i"}}, ['bar'], true); - keyCompatible({a: 1}, {a: {$regex: "^foo+", $options: "m"}}, ['bar'], true); + keyCompatible({a: 1}, {a: {$regex: '^foo+', $options: 'i'}}, ['bar'], true); + keyCompatible({a: 1}, {a: {$regex: '^foo+', $options: 'm'}}, ['bar'], true); // Multiple keys! keyCompatible({a: 1, b: 1, c: 1}, - {a: {$gt: 5}, c: {$lt: 3}}, [6, "bla", 2], true); + {a: {$gt: 5}, c: {$lt: 3}}, [6, 'bla', 2], true); keyCompatible({a: 1, b: 1, c: 1}, - {a: {$gt: 5}, c: {$lt: 3}}, [6, "bla", 4], false); + {a: {$gt: 5}, c: {$lt: 3}}, [6, 'bla', 4], false); keyCompatible({a: 1, b: 1, c: 1}, - {a: {$gt: 5}, c: {$lt: 3}}, [3, "bla", 1], false); + {a: {$gt: 5}, c: {$lt: 3}}, [3, 'bla', 1], false); // No filtering is done (ie, all keys are compatible) if the first key isn't // constrained. keyCompatible({a: 1, b: 1, c: 1}, - {c: {$lt: 3}}, [3, "bla", 4], true); + {c: {$lt: 3}}, [3, 'bla', 4], true); }); -Tinytest.add("minimongo - sort function", test => { +Tinytest.add('minimongo - sort function', test => { const c = new LocalCollection(); c.insert({a: 1}); @@ -2269,7 +2266,7 @@ Tinytest.add("minimongo - sort function", test => { test.equal(c.find({}, {sort: {a: -1}}).fetch(), c.find({}).fetch().sort(sortFunction)); }); -Tinytest.add("minimongo - binary search", test => { +Tinytest.add('minimongo - binary search', test => { const forwardCmp = (a, b) => a - b; const backwardCmp = (a, b) => -1 * forwardCmp(a, b); @@ -2277,8 +2274,8 @@ Tinytest.add("minimongo - binary search", test => { const checkSearch = (cmp, array, value, expected, message) => { const actual = LocalCollection._binarySearch(cmp, array, value); if (expected != actual) { - test.fail({type: "minimongo-binary-search", - message: `${message} : Expected index ${expected} but had ${actual}` + test.fail({type: 'minimongo-binary-search', + message: `${message} : Expected index ${expected} but had ${actual}`, }); } }; @@ -2290,38 +2287,38 @@ Tinytest.add("minimongo - binary search", test => { checkSearch(backwardCmp, array, value, expected, message); }; - checkSearchForward([1, 2, 5, 7], 4, 2, "Inner insert"); - checkSearchForward([1, 2, 3, 4], 3, 3, "Inner insert, equal value"); - checkSearchForward([1, 2, 5], 4, 2, "Inner insert, odd length"); - checkSearchForward([1, 3, 5, 6], 9, 4, "End insert"); - checkSearchForward([1, 3, 5, 6], 0, 0, "Beginning insert"); - checkSearchForward([1], 0, 0, "Single array, less than."); - checkSearchForward([1], 1, 1, "Single array, equal."); - checkSearchForward([1], 2, 1, "Single array, greater than."); - checkSearchForward([], 1, 0, "Empty array"); - checkSearchForward([1, 1, 1, 2, 2, 2, 2], 1, 3, "Highly degenerate array, lower"); - checkSearchForward([1, 1, 1, 2, 2, 2, 2], 2, 7, "Highly degenerate array, upper"); - checkSearchForward([2, 2, 2, 2, 2, 2, 2], 1, 0, "Highly degenerate array, lower"); - checkSearchForward([2, 2, 2, 2, 2, 2, 2], 2, 7, "Highly degenerate array, equal"); - checkSearchForward([2, 2, 2, 2, 2, 2, 2], 3, 7, "Highly degenerate array, upper"); + checkSearchForward([1, 2, 5, 7], 4, 2, 'Inner insert'); + checkSearchForward([1, 2, 3, 4], 3, 3, 'Inner insert, equal value'); + checkSearchForward([1, 2, 5], 4, 2, 'Inner insert, odd length'); + checkSearchForward([1, 3, 5, 6], 9, 4, 'End insert'); + checkSearchForward([1, 3, 5, 6], 0, 0, 'Beginning insert'); + checkSearchForward([1], 0, 0, 'Single array, less than.'); + checkSearchForward([1], 1, 1, 'Single array, equal.'); + checkSearchForward([1], 2, 1, 'Single array, greater than.'); + checkSearchForward([], 1, 0, 'Empty array'); + checkSearchForward([1, 1, 1, 2, 2, 2, 2], 1, 3, 'Highly degenerate array, lower'); + checkSearchForward([1, 1, 1, 2, 2, 2, 2], 2, 7, 'Highly degenerate array, upper'); + checkSearchForward([2, 2, 2, 2, 2, 2, 2], 1, 0, 'Highly degenerate array, lower'); + checkSearchForward([2, 2, 2, 2, 2, 2, 2], 2, 7, 'Highly degenerate array, equal'); + checkSearchForward([2, 2, 2, 2, 2, 2, 2], 3, 7, 'Highly degenerate array, upper'); - checkSearchBackward([7, 5, 2, 1], 4, 2, "Backward: Inner insert"); - checkSearchBackward([4, 3, 2, 1], 3, 2, "Backward: Inner insert, equal value"); - checkSearchBackward([5, 2, 1], 4, 1, "Backward: Inner insert, odd length"); - checkSearchBackward([6, 5, 3, 1], 9, 0, "Backward: Beginning insert"); - checkSearchBackward([6, 5, 3, 1], 0, 4, "Backward: End insert"); - checkSearchBackward([1], 0, 1, "Backward: Single array, less than."); - checkSearchBackward([1], 1, 1, "Backward: Single array, equal."); - checkSearchBackward([1], 2, 0, "Backward: Single array, greater than."); - checkSearchBackward([], 1, 0, "Backward: Empty array"); - checkSearchBackward([2, 2, 2, 2, 1, 1, 1], 1, 7, "Backward: Degenerate array, lower"); - checkSearchBackward([2, 2, 2, 2, 1, 1, 1], 2, 4, "Backward: Degenerate array, upper"); - checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 1, 7, "Backward: Highly degenerate array, upper"); - checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 2, 7, "Backward: Highly degenerate array, upper"); - checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 3, 0, "Backward: Highly degenerate array, upper"); + checkSearchBackward([7, 5, 2, 1], 4, 2, 'Backward: Inner insert'); + checkSearchBackward([4, 3, 2, 1], 3, 2, 'Backward: Inner insert, equal value'); + checkSearchBackward([5, 2, 1], 4, 1, 'Backward: Inner insert, odd length'); + checkSearchBackward([6, 5, 3, 1], 9, 0, 'Backward: Beginning insert'); + checkSearchBackward([6, 5, 3, 1], 0, 4, 'Backward: End insert'); + checkSearchBackward([1], 0, 1, 'Backward: Single array, less than.'); + checkSearchBackward([1], 1, 1, 'Backward: Single array, equal.'); + checkSearchBackward([1], 2, 0, 'Backward: Single array, greater than.'); + checkSearchBackward([], 1, 0, 'Backward: Empty array'); + checkSearchBackward([2, 2, 2, 2, 1, 1, 1], 1, 7, 'Backward: Degenerate array, lower'); + checkSearchBackward([2, 2, 2, 2, 1, 1, 1], 2, 4, 'Backward: Degenerate array, upper'); + checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 1, 7, 'Backward: Highly degenerate array, upper'); + checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 2, 7, 'Backward: Highly degenerate array, upper'); + checkSearchBackward([2, 2, 2, 2, 2, 2, 2], 3, 0, 'Backward: Highly degenerate array, upper'); }); -Tinytest.add("minimongo - modify", test => { +Tinytest.add('minimongo - modify', test => { const modifyWithQuery = (doc, query, mod, expected) => { const coll = new LocalCollection; coll.insert(doc); @@ -2330,7 +2327,7 @@ Tinytest.add("minimongo - modify", test => { const actual = coll.findOne(); delete actual._id; // added by insert - if (typeof expected === "function") { + if (typeof expected === 'function') { expected(actual, EJSON.stringify({input: doc, mod})); } else { test.equal(actual, expected, EJSON.stringify({input: doc, mod})); @@ -2359,8 +2356,7 @@ Tinytest.add("minimongo - modify", test => { if (expected._id) { test.equal(result.insertedId, expected._id); - } - else { + } else { delete actual._id; } @@ -2377,18 +2373,18 @@ Tinytest.add("minimongo - modify", test => { // document replacement modify({}, {}, {}); modify({a: 12}, {}, {}); // tested against mongodb - modify({a: 12}, {a: 13}, {a:13}); - modify({a: 12, b: 99}, {a: 13}, {a:13}); + modify({a: 12}, {a: 13}, {a: 13}); + modify({a: 12, b: 99}, {a: 13}, {a: 13}); exception({a: 12}, {a: 13, $set: {b: 13}}); exception({a: 12}, {$set: {b: 13}, a: 13}); - exception({a: 12}, {$a: 13}); //invalid operator - exception({a: 12}, {b:{$a: 13}}); - exception({a: 12}, {b:{'a.b': 13}}); - exception({a: 12}, {b:{'\0a': 13}}); + exception({a: 12}, {$a: 13}); // invalid operator + exception({a: 12}, {b: {$a: 13}}); + exception({a: 12}, {b: {'a.b': 13}}); + exception({a: 12}, {b: {'\0a': 13}}); // keys - modify({}, {$set: {'a': 12}}, {a: 12}); + modify({}, {$set: {a: 12}}, {a: 12}); modify({}, {$set: {'a.b': 12}}, {a: {b: 12}}); modify({}, {$set: {'a.b.c': 12}}, {a: {b: {c: 12}}}); modify({a: {d: 99}}, {$set: {'a.b.c': 12}}, {a: {d: 99, b: {c: 12}}}); @@ -2397,7 +2393,7 @@ Tinytest.add("minimongo - modify", test => { a: {b: [null, null, null, {c: 12}]}}); exception({a: [null, null, null]}, {$set: {'a.1.b': 12}}); exception({a: [null, 1, null]}, {$set: {'a.1.b': 12}}); - exception({a: [null, "x", null]}, {$set: {'a.1.b': 12}}); + exception({a: [null, 'x', null]}, {$set: {'a.1.b': 12}}); exception({a: [null, [], null]}, {$set: {'a.1.b': 12}}); modify({a: [null, null, null]}, {$set: {'a.3.b': 12}}, { a: [null, null, null, {b: 12}]}); @@ -2406,7 +2402,7 @@ Tinytest.add("minimongo - modify", test => { exception({a: 'x'}, {$set: {'a.b': 99}}); exception({a: true}, {$set: {'a.b': 99}}); exception({a: null}, {$set: {'a.b': 99}}); - modify({a: {}}, {$set: {'a.3': 12}}, {a: {'3': 12}}); + modify({a: {}}, {$set: {'a.3': 12}}, {a: {3: 12}}); modify({a: []}, {$set: {'a.3': 12}}, {a: [null, null, null, 12]}); exception({}, {$set: {'': 12}}); // tested on mongo exception({}, {$set: {'.': 12}}); // tested on mongo @@ -2414,26 +2410,26 @@ Tinytest.add("minimongo - modify", test => { exception({}, {$set: {'. ': 12}}); // tested on mongo exception({}, {$inc: {'... ': 12}}); // tested on mongo exception({}, {$set: {'a..b': 12}}); // tested on mongo - modify({a: [1,2,3]}, {$set: {'a.01': 99}}, {a: [1, 99, 3]}); - modify({a: [1,{a: 98},3]}, {$set: {'a.01.b': 99}}, {a: [1,{a:98, b: 99},3]}); - modify({}, {$set: {'2.a.b': 12}}, {'2': {'a': {'b': 12}}}); // tested + modify({a: [1, 2, 3]}, {$set: {'a.01': 99}}, {a: [1, 99, 3]}); + modify({a: [1, {a: 98}, 3]}, {$set: {'a.01.b': 99}}, {a: [1, {a: 98, b: 99}, 3]}); + modify({}, {$set: {'2.a.b': 12}}, {2: {a: {b: 12}}}); // tested exception({x: []}, {$set: {'x.2..a': 99}}); modify({x: [null, null]}, {$set: {'x.2.a': 1}}, {x: [null, null, {a: 1}]}); exception({x: [null, null]}, {$set: {'x.1.a': 1}}); // a.$.b modifyWithQuery({a: [{x: 2}, {x: 4}]}, {'a.x': 4}, {$set: {'a.$.z': 9}}, - {a: [{x: 2}, {x: 4, z: 9}]}); + {a: [{x: 2}, {x: 4, z: 9}]}); exception({a: [{x: 2}, {x: 4}]}, {$set: {'a.$.z': 9}}); exceptionWithQuery({a: [{x: 2}, {x: 4}], b: 5}, {b: 5}, {$set: {'a.$.z': 9}}); // can't have two $ exceptionWithQuery({a: [{x: [2]}]}, {'a.x': 2}, {$set: {'a.$.x.$': 9}}); modifyWithQuery({a: [5, 6, 7]}, {a: 6}, {$set: {'a.$': 9}}, {a: [5, 9, 7]}); modifyWithQuery({a: [{b: [{c: 9}, {c: 10}]}, {b: {c: 11}}]}, {'a.b.c': 10}, - {$unset: {'a.$.b': 1}}, {a: [{}, {b: {c: 11}}]}); + {$unset: {'a.$.b': 1}}, {a: [{}, {b: {c: 11}}]}); modifyWithQuery({a: [{b: [{c: 9}, {c: 10}]}, {b: {c: 11}}]}, {'a.b.c': 11}, - {$unset: {'a.$.b': 1}}, - {a: [{b: [{c: 9}, {c: 10}]}, {}]}); + {$unset: {'a.$.b': 1}}, + {a: [{b: [{c: 9}, {c: 10}]}, {}]}); modifyWithQuery({a: [1]}, {'a.0': 1}, {$set: {'a.$': 5}}, {a: [5]}); modifyWithQuery({a: [9]}, {a: {$mod: [2, 1]}}, {$set: {'a.$': 5}}, {a: [5]}); // Negatives don't set '$'. @@ -2441,84 +2437,84 @@ Tinytest.add("minimongo - modify", test => { exceptionWithQuery({a: [1]}, {'a.0': {$ne: 2}}, {$set: {'a.$': 5}}); // One $or clause works. modifyWithQuery({a: [{x: 2}, {x: 4}]}, - {$or: [{'a.x': 4}]}, {$set: {'a.$.z': 9}}, - {a: [{x: 2}, {x: 4, z: 9}]}); + {$or: [{'a.x': 4}]}, {$set: {'a.$.z': 9}}, + {a: [{x: 2}, {x: 4, z: 9}]}); // More $or clauses throw. exceptionWithQuery({a: [{x: 2}, {x: 4}]}, - {$or: [{'a.x': 4}, {'a.x': 4}]}, - {$set: {'a.$.z': 9}}); + {$or: [{'a.x': 4}, {'a.x': 4}]}, + {$set: {'a.$.z': 9}}); // $and uses the last one. modifyWithQuery({a: [{x: 1}, {x: 3}]}, - {$and: [{'a.x': 1}, {'a.x': 3}]}, - {$set: {'a.$.x': 5}}, - {a: [{x: 1}, {x: 5}]}); + {$and: [{'a.x': 1}, {'a.x': 3}]}, + {$set: {'a.$.x': 5}}, + {a: [{x: 1}, {x: 5}]}); modifyWithQuery({a: [{x: 1}, {x: 3}]}, - {$and: [{'a.x': 3}, {'a.x': 1}]}, - {$set: {'a.$.x': 5}}, - {a: [{x: 5}, {x: 3}]}); + {$and: [{'a.x': 3}, {'a.x': 1}]}, + {$set: {'a.$.x': 5}}, + {a: [{x: 5}, {x: 3}]}); // Same goes for the implicit AND of a document selector. modifyWithQuery({a: [{x: 1}, {y: 3}]}, - {'a.x': 1, 'a.y': 3}, - {$set: {'a.$.z': 5}}, - {a: [{x: 1}, {y: 3, z: 5}]}); + {'a.x': 1, 'a.y': 3}, + {$set: {'a.$.z': 5}}, + {a: [{x: 1}, {y: 3, z: 5}]}); modifyWithQuery({a: [{x: 1}, {y: 1}, {x: 1, y: 1}]}, - {a: {$elemMatch: {x: 1, y: 1}}}, - {$set: {'a.$.x': 2}}, - {a: [{x: 1}, {y: 1}, {x: 2, y: 1}]}); + {a: {$elemMatch: {x: 1, y: 1}}}, + {$set: {'a.$.x': 2}}, + {a: [{x: 1}, {y: 1}, {x: 2, y: 1}]}); modifyWithQuery({a: [{b: [{x: 1}, {y: 1}, {x: 1, y: 1}]}]}, - {'a.b': {$elemMatch: {x: 1, y: 1}}}, - {$set: {'a.$.b': 3}}, - {a: [{b: 3}]}); + {'a.b': {$elemMatch: {x: 1, y: 1}}}, + {$set: {'a.$.b': 3}}, + {a: [{b: 3}]}); // with $near, make sure it does not find the closest one (#3599) modifyWithQuery({a: []}, - {'a.b': {$near: [5, 5]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[]}); - modifyWithQuery({a: [{b: [ [3,3], [4,4] ]}]}, - {'a.b': {$near: [5, 5]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"}]}); - modifyWithQuery({a: [{b: [1,1]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [5, 5]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); - modifyWithQuery({a: [{b: [1,1]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [9, 9], $maxDistance: 1}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); - modifyWithQuery({a: [{b: [1,1]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [9, 9]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); - modifyWithQuery({a: [{b: [9,9]}, - {b: [ [3,3], [4,4] ]}, - {b: [9,9]}]}, - {'a.b': {$near: [9, 9]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"b":"k"},{"b":[[3,3],[4,4]]},{"b":[9,9]}]}); - modifyWithQuery({a: [{b:[4,3]}, - {c: [1,1]}]}, - {'a.c': {$near: [1, 1]}}, - {$set: {'a.$.c': 'k'}}, - {"a":[{"c": "k", "b":[4,3]},{"c":[1,1]}]}); - modifyWithQuery({a: [{c: [9,9]}, - {b: [ [3,3], [4,4] ]}, - {b: [1,1]}]}, - {'a.b': {$near: [1, 1]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"c": [9,9], "b":"k"},{"b": [ [3,3], [4,4]]},{"b":[1,1]}]}); - modifyWithQuery({a: [{c: [9,9], b:[4,3]}, - {b: [ [3,3], [4,4] ]}, - {b: [1,1]}]}, - {'a.b': {$near: [1, 1]}}, - {$set: {'a.$.b': 'k'}}, - {"a":[{"c": [9,9], "b":"k"},{"b": [ [3,3], [4,4]]},{"b":[1,1]}]}); + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {a: []}); + modifyWithQuery({a: [{b: [ [3, 3], [4, 4] ]}]}, + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {a: [{b: 'k'}]}); + modifyWithQuery({a: [{b: [1, 1]}, + {b: [ [3, 3], [4, 4] ]}, + {b: [9, 9]}]}, + {'a.b': {$near: [5, 5]}}, + {$set: {'a.$.b': 'k'}}, + {a: [{b: 'k'}, {b: [[3, 3], [4, 4]]}, {b: [9, 9]}]}); + modifyWithQuery({a: [{b: [1, 1]}, + {b: [ [3, 3], [4, 4] ]}, + {b: [9, 9]}]}, + {'a.b': {$near: [9, 9], $maxDistance: 1}}, + {$set: {'a.$.b': 'k'}}, + {a: [{b: 'k'}, {b: [[3, 3], [4, 4]]}, {b: [9, 9]}]}); + modifyWithQuery({a: [{b: [1, 1]}, + {b: [ [3, 3], [4, 4] ]}, + {b: [9, 9]}]}, + {'a.b': {$near: [9, 9]}}, + {$set: {'a.$.b': 'k'}}, + {a: [{b: 'k'}, {b: [[3, 3], [4, 4]]}, {b: [9, 9]}]}); + modifyWithQuery({a: [{b: [9, 9]}, + {b: [ [3, 3], [4, 4] ]}, + {b: [9, 9]}]}, + {'a.b': {$near: [9, 9]}}, + {$set: {'a.$.b': 'k'}}, + {a: [{b: 'k'}, {b: [[3, 3], [4, 4]]}, {b: [9, 9]}]}); + modifyWithQuery({a: [{b: [4, 3]}, + {c: [1, 1]}]}, + {'a.c': {$near: [1, 1]}}, + {$set: {'a.$.c': 'k'}}, + {a: [{c: 'k', b: [4, 3]}, {c: [1, 1]}]}); + modifyWithQuery({a: [{c: [9, 9]}, + {b: [ [3, 3], [4, 4] ]}, + {b: [1, 1]}]}, + {'a.b': {$near: [1, 1]}}, + {$set: {'a.$.b': 'k'}}, + {a: [{c: [9, 9], b: 'k'}, {b: [ [3, 3], [4, 4]]}, {b: [1, 1]}]}); + modifyWithQuery({a: [{c: [9, 9], b: [4, 3]}, + {b: [ [3, 3], [4, 4] ]}, + {b: [1, 1]}]}, + {'a.b': {$near: [1, 1]}}, + {$set: {'a.$.b': 'k'}}, + {a: [{c: [9, 9], b: 'k'}, {b: [ [3, 3], [4, 4]]}, {b: [1, 1]}]}); // $inc modify({a: 1, b: 2}, {$inc: {a: 10}}, {a: 11, b: 2}); @@ -2539,11 +2535,11 @@ Tinytest.add("minimongo - modify", test => { exception({}, {$inc: {_id: 1}}); // $currentDate - modify({}, {$currentDate: {a: true}}, (result, msg) => { test.instanceOf(result.a,Date,msg) }); - modify({}, {$currentDate: {a: {$type: "date"}}}, (result, msg) => { test.instanceOf(result.a,Date,msg) }); + modify({}, {$currentDate: {a: true}}, (result, msg) => { test.instanceOf(result.a, Date, msg); }); + modify({}, {$currentDate: {a: {$type: 'date'}}}, (result, msg) => { test.instanceOf(result.a, Date, msg); }); exception({}, {$currentDate: {a: false}}); exception({}, {$currentDate: {a: {}}}); - exception({}, {$currentDate: {a: {$type: "timestamp"}}}); + exception({}, {$currentDate: {a: {$type: 'timestamp'}}}); // $min modify({a: 1, b: 2}, {$min: {b: 1}}, {a: 1, b: 1}); @@ -2591,19 +2587,19 @@ Tinytest.add("minimongo - modify", test => { modify({a: 1, b: 2}, {$set: {a: {c: 10}}}, {a: {c: 10}, b: 2}); modify({a: [1, 2], b: 2}, {$set: {a: [3, 4]}}, {a: [3, 4], b: 2}); modify({a: [1, 2, 3], b: 2}, {$set: {'a.1': [3, 4]}}, - {a: [1, [3, 4], 3], b:2}); + {a: [1, [3, 4], 3], b: 2}); modify({a: [1], b: 2}, {$set: {'a.1': 9}}, {a: [1, 9], b: 2}); modify({a: [1], b: 2}, {$set: {'a.2': 9}}, {a: [1, null, 9], b: 2}); modify({a: {b: 1}}, {$set: {'a.c': 9}}, {a: {b: 1, c: 9}}); modify({}, {$set: {'x._id': 4}}, {x: {_id: 4}}); exception({}, {$set: {_id: 4}}); exception({_id: 4}, {$set: {_id: 4}}); // even not-changing _id is bad - //restricted field names - exception({a:{}}, {$set:{a:{$a:1}}}); + // restricted field names + exception({a: {}}, {$set: {a: {$a: 1}}}); exception({ a: {} }, { $set: { a: { c: [{ b: { $a: 1 } }] } } }); - exception({a:{}}, {$set:{a:{'\0a':1}}}); - exception({a:{}}, {$set:{a:{'a.b':1}}}); + exception({a: {}}, {$set: {a: {'\0a': 1}}}); + exception({a: {}}, {$set: {a: {'a.b': 1}}}); // $unset modify({}, {$unset: {a: 1}}, {}); @@ -2632,21 +2628,21 @@ Tinytest.add("minimongo - modify", test => { modify({a: []}, {$push: {'a.1': 99}}, {a: [null, [99]]}); // tested modify({a: {}}, {$push: {'a.x': 99}}, {a: {x: [99]}}); modify({}, {$push: {a: {$each: [1, 2, 3]}}}, - {a: [1, 2, 3]}); + {a: [1, 2, 3]}); modify({a: []}, {$push: {a: {$each: [1, 2, 3]}}}, - {a: [1, 2, 3]}); + {a: [1, 2, 3]}); modify({a: [true]}, {$push: {a: {$each: [1, 2, 3]}}}, - {a: [true, 1, 2, 3]}); + {a: [true, 1, 2, 3]}); modify({a: [true]}, {$push: {a: {$each: [1, 2, 3], $slice: -2}}}, - {a: [2, 3]}); + {a: [2, 3]}); modify({a: [false, true]}, {$push: {a: {$each: [1], $slice: -2}}}, - {a: [true, 1]}); + {a: [true, 1]}); modify( {a: [{x: 3}, {x: 1}]}, {$push: {a: { $each: [{x: 4}, {x: 2}], $slice: -2, - $sort: {x: 1} + $sort: {x: 1}, }}}, {a: [{x: 3}, {x: 4}]}); modify({}, {$push: {a: {$each: [1, 2, 3], $slice: 0}}}, {a: []}); @@ -2676,26 +2672,26 @@ Tinytest.add("minimongo - modify", test => { {$push: {a: {$each: [{x: 3}], $position: 0, $sort: {x: 1}, $slice: 0}}}, {a: []} ); - //restricted field names + // restricted field names exception({}, {$push: {$a: 1}}); exception({}, {$push: {'\0a': 1}}); - exception({}, {$push: {a: {$a:1}}}); - exception({}, {$push: {a: {$each: [{$a:1}]}}}); - exception({}, {$push: {a: {$each: [{"a.b":1}]}}}); - exception({}, {$push: {a: {$each: [{'\0a':1}]}}}); - modify({}, {$push: {a: {$each: [{'':1}]}}}, {a: [ { '': 1 } ]}); - modify({}, {$push: {a: {$each: [{' ':1}]}}}, {a: [ { ' ': 1 } ]}); - exception({}, {$push: {a: {$each: [{'.':1}]}}}); + exception({}, {$push: {a: {$a: 1}}}); + exception({}, {$push: {a: {$each: [{$a: 1}]}}}); + exception({}, {$push: {a: {$each: [{'a.b': 1}]}}}); + exception({}, {$push: {a: {$each: [{'\0a': 1}]}}}); + modify({}, {$push: {a: {$each: [{'': 1}]}}}, {a: [ { '': 1 } ]}); + modify({}, {$push: {a: {$each: [{' ': 1}]}}}, {a: [ { ' ': 1 } ]}); + exception({}, {$push: {a: {$each: [{'.': 1}]}}}); // #issue 5167 // $push $slice with positive numbers - modify({}, {$push: {a: {$each: [], $slice: 5}}}, {a:[]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [], $slice: 1}}}, {a:[1]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 1}}}, {a:[1]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 2}}}, {a:[1,2]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 4}}}, {a:[1,2,3,4]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 5}}}, {a:[1,2,3,4,5]}); - modify({a:[1,2,3]}, {$push: {a: {$each: [4,5], $slice: 10}}}, {a:[1,2,3,4,5]}); + modify({}, {$push: {a: {$each: [], $slice: 5}}}, {a: []}); + modify({a: [1, 2, 3]}, {$push: {a: {$each: [], $slice: 1}}}, {a: [1]}); + modify({a: [1, 2, 3]}, {$push: {a: {$each: [4, 5], $slice: 1}}}, {a: [1]}); + modify({a: [1, 2, 3]}, {$push: {a: {$each: [4, 5], $slice: 2}}}, {a: [1, 2]}); + modify({a: [1, 2, 3]}, {$push: {a: {$each: [4, 5], $slice: 4}}}, {a: [1, 2, 3, 4]}); + modify({a: [1, 2, 3]}, {$push: {a: {$each: [4, 5], $slice: 5}}}, {a: [1, 2, 3, 4, 5]}); + modify({a: [1, 2, 3]}, {$push: {a: {$each: [4, 5], $slice: 10}}}, {a: [1, 2, 3, 4, 5]}); // $pushAll @@ -2714,9 +2710,9 @@ Tinytest.add("minimongo - modify", test => { modify({a: []}, {$pushAll: {'a.1': []}}, {a: [null, []]}); modify({a: {}}, {$pushAll: {'a.x': [99]}}, {a: {x: [99]}}); modify({a: {}}, {$pushAll: {'a.x': []}}, {a: {x: []}}); - exception({a: [1]}, {$pushAll: {a: [{$a:1}]}}); - exception({a: [1]}, {$pushAll: {a: [{'\0a':1}]}}); - exception({a: [1]}, {$pushAll: {a: [{"a.b":1}]}}); + exception({a: [1]}, {$pushAll: {a: [{$a: 1}]}}); + exception({a: [1]}, {$pushAll: {a: [{'\0a': 1}]}}); + exception({a: [1]}, {$pushAll: {a: [{'a.b': 1}]}}); // $addToSet modify({}, {$addToSet: {a: 1}}, {a: [1]}); @@ -2731,9 +2727,9 @@ Tinytest.add("minimongo - modify", test => { modify({a: [{x: 1}]}, {$addToSet: {a: {x: 1}}}, {a: [{x: 1}]}); modify({a: [{x: 1}]}, {$addToSet: {a: {x: 2}}}, {a: [{x: 1}, {x: 2}]}); modify({a: [{x: 1, y: 2}]}, {$addToSet: {a: {x: 1, y: 2}}}, - {a: [{x: 1, y: 2}]}); + {a: [{x: 1, y: 2}]}); modify({a: [{x: 1, y: 2}]}, {$addToSet: {a: {y: 2, x: 1}}}, - {a: [{x: 1, y: 2}, {y: 2, x: 1}]}); + {a: [{x: 1, y: 2}, {y: 2, x: 1}]}); modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4]}}}, {a: [1, 2, 3, 4]}); modify({}, {$addToSet: {a: {$each: []}}}, {a: []}); modify({}, {$addToSet: {a: {$each: [1]}}}, {a: [1]}); @@ -2741,17 +2737,17 @@ Tinytest.add("minimongo - modify", test => { modify({a: {}}, {$addToSet: {'a.x': 99}}, {a: {x: [99]}}); // invalid field names - exception({}, {$addToSet: {a: {$b:1}}}); - exception({}, {$addToSet: {a: {"a.b":1}}}); - exception({}, {$addToSet: {a: {"a.":1}}}); - exception({}, {$addToSet: {a: {'\u0000a':1}}}); - exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, {$a:1}]}}}); - exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, {'\0a':1}]}}}); - exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, [{$a:1}]]}}}); - exception({a: [1, 2]}, {$addToSet: {a:{$each: [3, 1, [{b:{c:[{a:1},{"d.s":2}]}}]]}}}); - exception({a: [1, 2]}, {$addToSet: {a:{b: [3, 1, [{b:{c:[{a:1},{"d.s":2}]}}]]}}}); - //$each is first element and thus an operator - modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4], b: 12}}},{a: [ 1, 2, 3, 4 ]}); + exception({}, {$addToSet: {a: {$b: 1}}}); + exception({}, {$addToSet: {a: {'a.b': 1}}}); + exception({}, {$addToSet: {a: {'a.': 1}}}); + exception({}, {$addToSet: {a: {'\u0000a': 1}}}); + exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, {$a: 1}]}}}); + exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, {'\0a': 1}]}}}); + exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, [{$a: 1}]]}}}); + exception({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}); + exception({a: [1, 2]}, {$addToSet: {a: {b: [3, 1, [{b: {c: [{a: 1}, {'d.s': 2}]}}]]}}}); + // $each is first element and thus an operator + modify({a: [1, 2]}, {$addToSet: {a: {$each: [3, 1, 4], b: 12}}}, {a: [ 1, 2, 3, 4 ]}); // this should fail because $each is now a field name (not first in object) and thus invalid field name with $ exception({a: [1, 2]}, {$addToSet: {a: {b: 12, $each: [3, 1, 4]}}}); @@ -2762,12 +2758,12 @@ Tinytest.add("minimongo - modify", test => { modify({a: []}, {$pop: {a: -1}}, {a: []}); modify({a: [1, 2, 3]}, {$pop: {a: 1}}, {a: [1, 2]}); modify({a: [1, 2, 3]}, {$pop: {a: 10}}, {a: [1, 2]}); - modify({a: [1, 2, 3]}, {$pop: {a: .001}}, {a: [1, 2]}); + modify({a: [1, 2, 3]}, {$pop: {a: 0.001}}, {a: [1, 2]}); modify({a: [1, 2, 3]}, {$pop: {a: 0}}, {a: [1, 2]}); - modify({a: [1, 2, 3]}, {$pop: {a: "stuff"}}, {a: [1, 2]}); + modify({a: [1, 2, 3]}, {$pop: {a: 'stuff'}}, {a: [1, 2]}); modify({a: [1, 2, 3]}, {$pop: {a: -1}}, {a: [2, 3]}); modify({a: [1, 2, 3]}, {$pop: {a: -10}}, {a: [2, 3]}); - modify({a: [1, 2, 3]}, {$pop: {a: -.001}}, {a: [2, 3]}); + modify({a: [1, 2, 3]}, {$pop: {a: -0.001}}, {a: [2, 3]}); exception({a: true}, {$pop: {a: 1}}); exception({a: true}, {$pop: {a: -1}}); modify({a: []}, {$pop: {'a.1': 1}}, {a: []}); // tested @@ -2786,11 +2782,11 @@ Tinytest.add("minimongo - modify", test => { modify({a: [1, null, 2, null]}, {$pull: {a: null}}, {a: [1, 2]}); modify({a: []}, {$pull: {a: 3}}, {a: []}); modify({a: [[2], [2, 1], [3]]}, {$pull: {a: [2, 1]}}, - {a: [[2], [3]]}); // tested + {a: [[2], [3]]}); // tested modify({a: [{b: 1, c: 2}, {b: 2, c: 2}]}, {$pull: {a: {b: 1}}}, - {a: [{b: 2, c: 2}]}); + {a: [{b: 2, c: 2}]}); modify({a: [{b: 1, c: 2}, {b: 2, c: 2}]}, {$pull: {a: {c: 2}}}, - {a: []}); + {a: []}); // XXX implement this functionality! // probably same refactoring as $elemMatch? // modify({a: [1, 2, 3, 4]}, {$pull: {$gt: 2}}, {a: [1,2]}); fails! @@ -2805,7 +2801,7 @@ Tinytest.add("minimongo - modify", test => { exception({a: true}, {$pullAll: {a: [1]}}); exception({a: [1, 2, 3]}, {$pullAll: {a: 1}}); modify({x: [{a: 1}, {a: 1, b: 2}]}, {$pullAll: {x: [{a: 1}]}}, - {x: [{a: 1, b: 2}]}); + {x: [{a: 1, b: 2}]}); // $rename modify({}, {$rename: {a: 'b'}}, {}); @@ -2816,7 +2812,7 @@ Tinytest.add("minimongo - modify", test => { modify({a: {b: 12}}, {$rename: {'a.b': 'q.r'}}, {a: {}, q: {r: 12}}); modify({a: {b: 12}}, {$rename: {'a.b': 'q.2.r'}}, {a: {}, q: {2: {r: 12}}}); modify({a: {b: 12}, q: {}}, {$rename: {'a.b': 'q.2.r'}}, - {a: {}, q: {2: {r: 12}}}); + {a: {}, q: {2: {r: 12}}}); exception({a: {b: 12}, q: []}, {$rename: {'a.b': 'q.2'}}); // tested exception({a: {b: 12}, q: []}, {$rename: {'a.b': 'q.2.r'}}); // tested // These strange MongoDB behaviors throw. @@ -2824,7 +2820,7 @@ Tinytest.add("minimongo - modify", test => { // {a: {b: 12}, x: []}); // tested // modify({a: {b: 12}, q: []}, {$rename: {'q.1.j': 'x'}}, // {a: {b: 12}, x: []}); // tested - exception({}, {$rename: {'a': 'a'}}); + exception({}, {$rename: {a: 'a'}}); exception({}, {$rename: {'a.b': 'a.b'}}); modify({a: 12, b: 13}, {$rename: {a: 'b'}}, {b: 12}); exception({a: [12]}, {$rename: {a: '$b'}}); @@ -2834,16 +2830,16 @@ Tinytest.add("minimongo - modify", test => { modify({a: 0}, {$setOnInsert: {a: 12}}, {a: 0}); upsert({a: 12}, {$setOnInsert: {b: 12}}, {a: 12, b: 12}); upsert({a: 12}, {$setOnInsert: {_id: 'test'}}, {_id: 'test', a: 12}); - upsert({"a.b": 10}, {$setOnInsert: {a: {b: 10, c: 12}}}, {a: {b: 10, c: 12}}); - upsert({"a.b": 10}, {$setOnInsert: {c: 12}}, {a: {b: 10}, c: 12}); - upsert({"_id": 'test'}, {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); + upsert({'a.b': 10}, {$setOnInsert: {a: {b: 10, c: 12}}}, {a: {b: 10, c: 12}}); + upsert({'a.b': 10}, {$setOnInsert: {c: 12}}, {a: {b: 10}, c: 12}); + upsert({_id: 'test'}, {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); upsert('test', {$setOnInsert: {c: 12}}, {_id: 'test', c: 12}); upsertException({a: 0}, {$setOnInsert: {$a: 12}}); upsertException({a: 0}, {$setOnInsert: {'\0a': 12}}); - upsert({a: 0}, {$setOnInsert: {b: {a:1}}}, {a:0, b:{a:1}}); - upsertException({a: 0}, {$setOnInsert: {b: {$a:1}}}); - upsertException({a: 0}, {$setOnInsert: {b: {'a.b':1}}}); - upsertException({a: 0}, {$setOnInsert: {b: {'\0a':1}}}); + upsert({a: 0}, {$setOnInsert: {b: {a: 1}}}, {a: 0, b: {a: 1}}); + upsertException({a: 0}, {$setOnInsert: {b: {$a: 1}}}); + upsertException({a: 0}, {$setOnInsert: {b: {'a.b': 1}}}); + upsertException({a: 0}, {$setOnInsert: {b: {'\0a': 1}}}); // Test for https://github.com/meteor/meteor/issues/8775. upsert( @@ -2881,8 +2877,8 @@ Tinytest.add("minimongo - modify", test => { $exists: { writable: true, configurable: true, - value: true - } + value: true, + }, }), }, { $setOnInsert: { a: 123 } }, @@ -2910,7 +2906,7 @@ Tinytest.add("minimongo - modify", test => { // XXX test update() (selecting docs, multi, upsert..) -Tinytest.add("minimongo - observe ordered", test => { +Tinytest.add('minimongo - observe ordered', test => { const operations = []; const cbs = log_callbacks(operations); let handle; @@ -2919,62 +2915,62 @@ Tinytest.add("minimongo - observe ordered", test => { handle = c.find({}, {sort: {a: 1}}).observe(cbs); test.isTrue(handle.collection === c); - c.insert({_id: 'foo', a:1}); - test.equal(operations.shift(), ['added', {a:1}, 0, null]); - c.update({a:1}, {$set: {a: 2}}); - test.equal(operations.shift(), ['changed', {a:2}, 0, {a:1}]); - c.insert({a:10}); - test.equal(operations.shift(), ['added', {a:10}, 1, null]); + c.insert({_id: 'foo', a: 1}); + test.equal(operations.shift(), ['added', {a: 1}, 0, null]); + c.update({a: 1}, {$set: {a: 2}}); + test.equal(operations.shift(), ['changed', {a: 2}, 0, {a: 1}]); + c.insert({a: 10}); + test.equal(operations.shift(), ['added', {a: 10}, 1, null]); c.update({}, {$inc: {a: 1}}, {multi: true}); - test.equal(operations.shift(), ['changed', {a:3}, 0, {a:2}]); - test.equal(operations.shift(), ['changed', {a:11}, 1, {a:10}]); - c.update({a:11}, {a:1}); - test.equal(operations.shift(), ['changed', {a:1}, 1, {a:11}]); - test.equal(operations.shift(), ['moved', {a:1}, 1, 0, 'foo']); - c.remove({a:2}); + test.equal(operations.shift(), ['changed', {a: 3}, 0, {a: 2}]); + test.equal(operations.shift(), ['changed', {a: 11}, 1, {a: 10}]); + c.update({a: 11}, {a: 1}); + test.equal(operations.shift(), ['changed', {a: 1}, 1, {a: 11}]); + test.equal(operations.shift(), ['moved', {a: 1}, 1, 0, 'foo']); + c.remove({a: 2}); test.equal(operations.shift(), undefined); - c.remove({a:3}); - test.equal(operations.shift(), ['removed', 'foo', 1, {a:3}]); + c.remove({a: 3}); + test.equal(operations.shift(), ['removed', 'foo', 1, {a: 3}]); // test stop handle.stop(); const idA2 = Random.id(); - c.insert({_id: idA2, a:2}); + c.insert({_id: idA2, a: 2}); test.equal(operations.shift(), undefined); // test initial inserts (and backwards sort) handle = c.find({}, {sort: {a: -1}}).observe(cbs); - test.equal(operations.shift(), ['added', {a:2}, 0, null]); - test.equal(operations.shift(), ['added', {a:1}, 1, null]); + test.equal(operations.shift(), ['added', {a: 2}, 0, null]); + test.equal(operations.shift(), ['added', {a: 1}, 1, null]); handle.stop(); // test _suppress_initial handle = c.find({}, {sort: {a: -1}}).observe(Object.assign({ _suppress_initial: true}, cbs)); test.equal(operations.shift(), undefined); - c.insert({a:100}); - test.equal(operations.shift(), ['added', {a:100}, 0, idA2]); + c.insert({a: 100}); + test.equal(operations.shift(), ['added', {a: 100}, 0, idA2]); handle.stop(); // test skip and limit. c.remove({}); handle = c.find({}, {sort: {a: 1}, skip: 1, limit: 2}).observe(cbs); test.equal(operations.shift(), undefined); - c.insert({a:1}); + c.insert({a: 1}); test.equal(operations.shift(), undefined); - c.insert({_id: 'foo', a:2}); - test.equal(operations.shift(), ['added', {a:2}, 0, null]); - c.insert({a:3}); - test.equal(operations.shift(), ['added', {a:3}, 1, null]); - c.insert({a:4}); + c.insert({_id: 'foo', a: 2}); + test.equal(operations.shift(), ['added', {a: 2}, 0, null]); + c.insert({a: 3}); + test.equal(operations.shift(), ['added', {a: 3}, 1, null]); + c.insert({a: 4}); test.equal(operations.shift(), undefined); - c.update({a:1}, {a:0}); + c.update({a: 1}, {a: 0}); test.equal(operations.shift(), undefined); - c.update({a:0}, {a:5}); - test.equal(operations.shift(), ['removed', 'foo', 0, {a:2}]); - test.equal(operations.shift(), ['added', {a:4}, 1, null]); - c.update({a:3}, {a:3.5}); - test.equal(operations.shift(), ['changed', {a:3.5}, 0, {a:3}]); + c.update({a: 0}, {a: 5}); + test.equal(operations.shift(), ['removed', 'foo', 0, {a: 2}]); + test.equal(operations.shift(), ['added', {a: 4}, 1, null]); + c.update({a: 3}, {a: 3.5}); + test.equal(operations.shift(), ['changed', {a: 3.5}, 0, {a: 3}]); handle.stop(); // test observe limit with pre-existing docs @@ -2983,12 +2979,12 @@ Tinytest.add("minimongo - observe ordered", test => { c.insert({_id: 'two', a: 2}); c.insert({a: 3}); handle = c.find({}, {sort: {a: 1}, limit: 2}).observe(cbs); - test.equal(operations.shift(), ['added', {a:1}, 0, null]); - test.equal(operations.shift(), ['added', {a:2}, 1, null]); + test.equal(operations.shift(), ['added', {a: 1}, 0, null]); + test.equal(operations.shift(), ['added', {a: 2}, 1, null]); test.equal(operations.shift(), undefined); c.remove({a: 2}); - test.equal(operations.shift(), ['removed', 'two', 1, {a:2}]); - test.equal(operations.shift(), ['added', {a:3}, 1, null]); + test.equal(operations.shift(), ['removed', 'two', 1, {a: 2}]); + test.equal(operations.shift(), ['added', {a: 3}, 1, null]); test.equal(operations.shift(), undefined); handle.stop(); @@ -2996,22 +2992,22 @@ Tinytest.add("minimongo - observe ordered", test => { c.remove({}); handle = c.find({}, {sort: {a: 1}}).observe(Object.assign(cbs, {_no_indices: true})); - c.insert({_id: 'foo', a:1}); - test.equal(operations.shift(), ['added', {a:1}, -1, null]); - c.update({a:1}, {$set: {a: 2}}); - test.equal(operations.shift(), ['changed', {a:2}, -1, {a:1}]); - c.insert({a:10}); - test.equal(operations.shift(), ['added', {a:10}, -1, null]); + c.insert({_id: 'foo', a: 1}); + test.equal(operations.shift(), ['added', {a: 1}, -1, null]); + c.update({a: 1}, {$set: {a: 2}}); + test.equal(operations.shift(), ['changed', {a: 2}, -1, {a: 1}]); + c.insert({a: 10}); + test.equal(operations.shift(), ['added', {a: 10}, -1, null]); c.update({}, {$inc: {a: 1}}, {multi: true}); - test.equal(operations.shift(), ['changed', {a:3}, -1, {a:2}]); - test.equal(operations.shift(), ['changed', {a:11}, -1, {a:10}]); - c.update({a:11}, {a:1}); - test.equal(operations.shift(), ['changed', {a:1}, -1, {a:11}]); - test.equal(operations.shift(), ['moved', {a:1}, -1, -1, 'foo']); - c.remove({a:2}); + test.equal(operations.shift(), ['changed', {a: 3}, -1, {a: 2}]); + test.equal(operations.shift(), ['changed', {a: 11}, -1, {a: 10}]); + c.update({a: 11}, {a: 1}); + test.equal(operations.shift(), ['changed', {a: 1}, -1, {a: 11}]); + test.equal(operations.shift(), ['moved', {a: 1}, -1, -1, 'foo']); + c.remove({a: 2}); test.equal(operations.shift(), undefined); - c.remove({a:3}); - test.equal(operations.shift(), ['removed', 'foo', -1, {a:3}]); + c.remove({a: 3}); + test.equal(operations.shift(), ['removed', 'foo', -1, {a: 3}]); handle.stop(); }); @@ -3019,45 +3015,45 @@ Tinytest.add("minimongo - observe ordered", test => { Tinytest.add(`minimongo - observe ordered: ${ordered}`, test => { const c = new LocalCollection(); - let ev = ""; + let ev = ''; const makecb = tag => { const ret = {}; - ["added", "changed", "removed"].forEach(fn => { + ['added', 'changed', 'removed'].forEach(fn => { const fnName = ordered ? `${fn}At` : fn; ret[fnName] = doc => { - ev = (`${ev + fn.substr(0, 1) + tag + doc._id}_`); + ev = `${ev + fn.substr(0, 1) + tag + doc._id}_`; }; }); return ret; }; const expect = x => { test.equal(ev, x); - ev = ""; + ev = ''; }; - c.insert({_id: 1, name: "strawberry", tags: ["fruit", "red", "squishy"]}); - c.insert({_id: 2, name: "apple", tags: ["fruit", "red", "hard"]}); - c.insert({_id: 3, name: "rose", tags: ["flower", "red", "squishy"]}); + c.insert({_id: 1, name: 'strawberry', tags: ['fruit', 'red', 'squishy']}); + c.insert({_id: 2, name: 'apple', tags: ['fruit', 'red', 'hard']}); + c.insert({_id: 3, name: 'rose', tags: ['flower', 'red', 'squishy']}); // This should work equally well for ordered and unordered observations // (because the callbacks don't look at indices and there's no 'moved' // callback). - let handle = c.find({tags: "flower"}).observe(makecb('a')); - expect("aa3_"); - c.update({name: "rose"}, {$set: {tags: ["bloom", "red", "squishy"]}}); - expect("ra3_"); - c.update({name: "rose"}, {$set: {tags: ["flower", "red", "squishy"]}}); - expect("aa3_"); - c.update({name: "rose"}, {$set: {food: false}}); - expect("ca3_"); + let handle = c.find({tags: 'flower'}).observe(makecb('a')); + expect('aa3_'); + c.update({name: 'rose'}, {$set: {tags: ['bloom', 'red', 'squishy']}}); + expect('ra3_'); + c.update({name: 'rose'}, {$set: {tags: ['flower', 'red', 'squishy']}}); + expect('aa3_'); + c.update({name: 'rose'}, {$set: {food: false}}); + expect('ca3_'); c.remove({}); - expect("ra3_"); - c.insert({_id: 4, name: "daisy", tags: ["flower"]}); - expect("aa4_"); + expect('ra3_'); + c.insert({_id: 4, name: 'daisy', tags: ['flower']}); + expect('aa4_'); handle.stop(); // After calling stop, no more callbacks are called. - c.insert({_id: 5, name: "iris", tags: ["flower"]}); - expect(""); + c.insert({_id: 5, name: 'iris', tags: ['flower']}); + expect(''); // Test that observing a lookup by ID works. handle = c.find(4).observe(makecb('b')); @@ -3067,17 +3063,17 @@ Tinytest.add("minimongo - observe ordered", test => { handle.stop(); // Test observe with reactive: false. - handle = c.find({tags: "flower"}, {reactive: false}).observe(makecb('c')); + handle = c.find({tags: 'flower'}, {reactive: false}).observe(makecb('c')); expect('ac4_ac5_'); // This insert shouldn't trigger a callback because it's not reactive. - c.insert({_id: 6, name: "river", tags: ["flower"]}); + c.insert({_id: 6, name: 'river', tags: ['flower']}); expect(''); handle.stop(); }); }); -Tinytest.add("minimongo - saveOriginals", test => { +Tinytest.add('minimongo - saveOriginals', test => { // set up some data const c = new LocalCollection(); @@ -3090,7 +3086,7 @@ Tinytest.add("minimongo - saveOriginals", test => { // Save originals and make some changes. c.saveOriginals(); - c.insert({_id: "hooray", z: 'insertme'}); + c.insert({_id: 'hooray', z: 'insertme'}); c.remove({y: 'removeme'}); count = c.update({x: 'updateme'}, {$set: {z: 5}}, {multi: true}); c.update('bar', {$set: {k: 7}}); // update same doc twice @@ -3134,7 +3130,7 @@ Tinytest.add("minimongo - saveOriginals", test => { test.equal(originals.get('temp'), undefined); }); -Tinytest.add("minimongo - saveOriginals errors", test => { +Tinytest.add('minimongo - saveOriginals errors', test => { const c = new LocalCollection(); // Can't call retrieve before save. test.throws(() => { c.retrieveOriginals(); }); @@ -3143,32 +3139,32 @@ Tinytest.add("minimongo - saveOriginals errors", test => { test.throws(() => { c.saveOriginals(); }); }); -Tinytest.add("minimongo - objectid transformation", test => { +Tinytest.add('minimongo - objectid transformation', test => { const testId = item => { test.equal(item, MongoID.idParse(MongoID.idStringify(item))); }; const randomOid = new MongoID.ObjectID(); testId(randomOid); - testId("FOO"); - testId("ffffffffffff"); - testId("0987654321abcdef09876543"); + testId('FOO'); + testId('ffffffffffff'); + testId('0987654321abcdef09876543'); testId(new MongoID.ObjectID()); - testId("--a string"); + testId('--a string'); - test.equal("ffffffffffff", MongoID.idParse(MongoID.idStringify("ffffffffffff"))); + test.equal('ffffffffffff', MongoID.idParse(MongoID.idStringify('ffffffffffff'))); }); -Tinytest.add("minimongo - objectid", test => { +Tinytest.add('minimongo - objectid', test => { const randomOid = new MongoID.ObjectID(); const anotherRandomOid = new MongoID.ObjectID(); test.notEqual(randomOid, anotherRandomOid); - test.throws(() => { new MongoID.ObjectID("qqqqqqqqqqqqqqqqqqqqqqqq");}); - test.throws(() => { new MongoID.ObjectID("ABCDEF"); }); + test.throws(() => { new MongoID.ObjectID('qqqqqqqqqqqqqqqqqqqqqqqq');}); + test.throws(() => { new MongoID.ObjectID('ABCDEF'); }); test.equal(randomOid, new MongoID.ObjectID(randomOid.valueOf())); }); -Tinytest.add("minimongo - pause", test => { +Tinytest.add('minimongo - pause', test => { const operations = []; const cbs = log_callbacks(operations); @@ -3177,7 +3173,7 @@ Tinytest.add("minimongo - pause", test => { // remove and add cancel out. c.insert({_id: 1, a: 1}); - test.equal(operations.shift(), ['added', {a:1}, 0, null]); + test.equal(operations.shift(), ['added', {a: 1}, 0, null]); c.pauseObservers(); @@ -3197,7 +3193,7 @@ Tinytest.add("minimongo - pause", test => { c.update({_id: 1}, {a: 3}); c.resumeObservers(); - test.equal(operations.shift(), ['changed', {a:3}, 0, {a:1}]); + test.equal(operations.shift(), ['changed', {a: 3}, 0, {a: 1}]); test.length(operations, 0); // test special case for remove({}) @@ -3205,34 +3201,34 @@ Tinytest.add("minimongo - pause", test => { test.equal(c.remove({}), 1); test.length(operations, 0); c.resumeObservers(); - test.equal(operations.shift(), ['removed', 1, 0, {a:3}]); + test.equal(operations.shift(), ['removed', 1, 0, {a: 3}]); test.length(operations, 0); h.stop(); }); -Tinytest.add("minimongo - ids matched by selector", test => { +Tinytest.add('minimongo - ids matched by selector', test => { const check = (selector, ids) => { const idsFromSelector = LocalCollection._idsMatchedBySelector(selector); // XXX normalize order, in a way that also works for ObjectIDs? test.equal(idsFromSelector, ids); }; - check("foo", ["foo"]); - check({_id: "foo"}, ["foo"]); + check('foo', ['foo']); + check({_id: 'foo'}, ['foo']); const oid1 = new MongoID.ObjectID(); check(oid1, [oid1]); check({_id: oid1}, [oid1]); - check({_id: "foo", x: 42}, ["foo"]); + check({_id: 'foo', x: 42}, ['foo']); check({}, null); - check({_id: {$in: ["foo", oid1]}}, ["foo", oid1]); - check({_id: {$ne: "foo"}}, null); + check({_id: {$in: ['foo', oid1]}}, ['foo', oid1]); + check({_id: {$ne: 'foo'}}, null); // not actually valid, but works for now... - check({$and: ["foo"]}, ["foo"]); + check({$and: ['foo']}, ['foo']); check({$and: [{x: 42}, {_id: oid1}]}, [oid1]); check({$and: [{x: 42}, {_id: {$in: [oid1]}}]}, [oid1]); }); -Tinytest.add("minimongo - reactive stop", test => { +Tinytest.add('minimongo - reactive stop', test => { const coll = new LocalCollection(); coll.insert({_id: 'A'}); coll.insert({_id: 'B'}); @@ -3240,8 +3236,7 @@ Tinytest.add("minimongo - reactive stop", test => { const addBefore = (str, newChar, before) => { const idx = str.indexOf(before); - if (idx === -1) - return str + newChar; + if (idx === -1) {return str + newChar;} return str.slice(0, idx) + newChar + str.slice(idx); }; @@ -3250,39 +3245,39 @@ Tinytest.add("minimongo - reactive stop", test => { const c = Tracker.autorun(() => { const q = coll.find({}, {sort: {_id: sortOrder.get()}}); - x = ""; + x = ''; q.observe({ addedAt(doc, atIndex, before) { x = addBefore(x, doc._id, before); }}); - y = ""; + y = ''; q.observeChanges({ addedBefore(id, fields, before) { y = addBefore(y, id, before); }}); }); - test.equal(x, "ABC"); - test.equal(y, "ABC"); + test.equal(x, 'ABC'); + test.equal(y, 'ABC'); sortOrder.set(-1); - test.equal(x, "ABC"); - test.equal(y, "ABC"); + test.equal(x, 'ABC'); + test.equal(y, 'ABC'); Tracker.flush(); - test.equal(x, "CBA"); - test.equal(y, "CBA"); + test.equal(x, 'CBA'); + test.equal(y, 'CBA'); coll.insert({_id: 'D'}); coll.insert({_id: 'E'}); - test.equal(x, "EDCBA"); - test.equal(y, "EDCBA"); + test.equal(x, 'EDCBA'); + test.equal(y, 'EDCBA'); c.stop(); // stopping kills the observes immediately coll.insert({_id: 'F'}); - test.equal(x, "EDCBA"); - test.equal(y, "EDCBA"); + test.equal(x, 'EDCBA'); + test.equal(y, 'EDCBA'); }); -Tinytest.add("minimongo - immediate invalidate", test => { +Tinytest.add('minimongo - immediate invalidate', test => { const coll = new LocalCollection(); coll.insert({_id: 'A'}); @@ -3303,7 +3298,7 @@ Tinytest.add("minimongo - immediate invalidate", test => { }); -Tinytest.add("minimongo - count on cursor with limit", test => { +Tinytest.add('minimongo - count on cursor with limit', test => { const coll = new LocalCollection(); let count; @@ -3339,7 +3334,7 @@ Tinytest.add("minimongo - count on cursor with limit", test => { c.stop(); }); -Tinytest.add("minimongo - reactive count with cached cursor", test => { +Tinytest.add('minimongo - reactive count with cached cursor', test => { const coll = new LocalCollection; const cursor = coll.find({}); let firstAutorunCount, secondAutorunCount; @@ -3359,7 +3354,7 @@ Tinytest.add("minimongo - reactive count with cached cursor", test => { test.equal(secondAutorunCount, 3); }); -Tinytest.add("minimongo - $near operator tests", test => { +Tinytest.add('minimongo - $near operator tests', test => { let coll = new LocalCollection(); coll.insert({ rest: { loc: [2, 3] } }); coll.insert({ rest: { loc: [-3, 3] } }); @@ -3380,31 +3375,31 @@ Tinytest.add("minimongo - $near operator tests", test => { // GeoJSON tests coll = new LocalCollection(); - const data = [{ "category" : "BURGLARY", "descript" : "BURGLARY OF STORE, FORCIBLE ENTRY", "address" : "100 Block of 10TH ST", "location" : { "type" : "Point", "coordinates" : [ -122.415449723856, 37.7749518087273 ] } }, - { "category" : "WEAPON LAWS", "descript" : "POSS OF PROHIBITED WEAPON", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879744156 ] } }, - { "category" : "LARCENY/THEFT", "descript" : "GRAND THEFT OF PROPERTY", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.41538270191, 37.774683628213 ] } }, - { "category" : "LARCENY/THEFT", "descript" : "PETTY THEFT FROM LOCKED AUTO", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415396041221, 37.7747879744156 ] } }, - { "category" : "OTHER OFFENSES", "descript" : "POSSESSION OF BURGLARY TOOLS", "address" : "900 Block of MINNA ST", "location" : { "type" : "Point", "coordinates" : [ -122.415386041221, 37.7747879734156 ] } } + const data = [{ category: 'BURGLARY', descript: 'BURGLARY OF STORE, FORCIBLE ENTRY', address: '100 Block of 10TH ST', location: { type: 'Point', coordinates: [ -122.415449723856, 37.7749518087273 ] } }, + { category: 'WEAPON LAWS', descript: 'POSS OF PROHIBITED WEAPON', address: '900 Block of MINNA ST', location: { type: 'Point', coordinates: [ -122.415386041221, 37.7747879744156 ] } }, + { category: 'LARCENY/THEFT', descript: 'GRAND THEFT OF PROPERTY', address: '900 Block of MINNA ST', location: { type: 'Point', coordinates: [ -122.41538270191, 37.774683628213 ] } }, + { category: 'LARCENY/THEFT', descript: 'PETTY THEFT FROM LOCKED AUTO', address: '900 Block of MINNA ST', location: { type: 'Point', coordinates: [ -122.415396041221, 37.7747879744156 ] } }, + { category: 'OTHER OFFENSES', descript: 'POSSESSION OF BURGLARY TOOLS', address: '900 Block of MINNA ST', location: { type: 'Point', coordinates: [ -122.415386041221, 37.7747879734156 ] } }, ]; data.forEach((x, i) => { coll.insert(Object.assign(x, { x: i })); }); const close15 = coll.find({ location: { $near: { - $geometry: { type: "Point", - coordinates: [-122.4154282, 37.7746115] }, + $geometry: { type: 'Point', + coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 15 } } }).fetch(); test.length(close15, 1); - test.equal(close15[0].descript, "GRAND THEFT OF PROPERTY"); + test.equal(close15[0].descript, 'GRAND THEFT OF PROPERTY'); const close20 = coll.find({ location: { $near: { - $geometry: { type: "Point", - coordinates: [-122.4154282, 37.7746115] }, + $geometry: { type: 'Point', + coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 } } }).fetch(); test.length(close20, 4); - test.equal(close20[0].descript, "GRAND THEFT OF PROPERTY"); - test.equal(close20[1].descript, "PETTY THEFT FROM LOCKED AUTO"); - test.equal(close20[2].descript, "POSSESSION OF BURGLARY TOOLS"); - test.equal(close20[3].descript, "POSS OF PROHIBITED WEAPON"); + test.equal(close20[0].descript, 'GRAND THEFT OF PROPERTY'); + test.equal(close20[1].descript, 'PETTY THEFT FROM LOCKED AUTO'); + test.equal(close20[2].descript, 'POSSESSION OF BURGLARY TOOLS'); + test.equal(close20[3].descript, 'POSS OF PROHIBITED WEAPON'); // Any combinations of $near with $or/$and/$nor/$not should throw an error test.throws(() => { @@ -3412,26 +3407,26 @@ Tinytest.add("minimongo - $near operator tests", test => { $not: { $near: { $geometry: { - type: "Point", - coordinates: [-122.4154282, 37.7746115] + type: 'Point', + coordinates: [-122.4154282, 37.7746115], }, $maxDistance: 20 } } } }); }); test.throws(() => { coll.find({ - $and: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, - { x: 0 }] + $and: [ { location: { $near: { $geometry: { type: 'Point', coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, + { x: 0 }], }); }); test.throws(() => { coll.find({ - $or: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, - { x: 0 }] + $or: [ { location: { $near: { $geometry: { type: 'Point', coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 20 }}}, + { x: 0 }], }); }); test.throws(() => { coll.find({ - $nor: [ { location: { $near: { $geometry: { type: "Point", coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 1 }}}, - { x: 0 }] + $nor: [ { location: { $near: { $geometry: { type: 'Point', coordinates: [-122.4154282, 37.7746115] }, $maxDistance: 1 }}}, + { x: 0 }], }); }); test.throws(() => { @@ -3441,21 +3436,21 @@ Tinytest.add("minimongo - $near operator tests", test => { location: { $near: { $geometry: { - type: "Point", - coordinates: [-122.4154282, 37.7746115] + type: 'Point', + coordinates: [-122.4154282, 37.7746115], }, - $maxDistance: 1 - } - } - }] - }] + $maxDistance: 1, + }, + }, + }], + }], }); }); // array tests coll = new LocalCollection(); coll.insert({ - _id: "x", + _id: 'x', k: 9, a: [ {b: [ @@ -3463,7 +3458,7 @@ Tinytest.add("minimongo - $near operator tests", test => { [1, 1]]}, {b: [150, 150]}]}); coll.insert({ - _id: "y", + _id: 'y', k: 9, a: {b: [5, 5]}}); const testNear = (near, md, expected) => { @@ -3489,15 +3484,15 @@ Tinytest.add("minimongo - $near operator tests", test => { const operations = []; const cbs = log_callbacks(operations); - const handle = coll.find({'a.b': {$near: [7,7]}}).observe(cbs); + const handle = coll.find({'a.b': {$near: [7, 7]}}).observe(cbs); test.length(operations, 2); - test.equal(operations.shift(), ['added', {k:9, a:{b:[5,5]}}, 0, null]); + test.equal(operations.shift(), ['added', {k: 9, a: {b: [5, 5]}}, 0, null]); test.equal(operations.shift(), - ['added', {k: 9, a:[{b:[[100,100],[1,1]]},{b:[150,150]}]}, - 1, null]); + ['added', {k: 9, a: [{b: [[100, 100], [1, 1]]}, {b: [150, 150]}]}, + 1, null]); // This needs to be inserted in the MIDDLE of the two existing ones. - coll.insert({a: {b: [3,3]}}); + coll.insert({a: {b: [3, 3]}}); test.length(operations, 1); test.equal(operations.shift(), ['added', {a: {b: [3, 3]}}, 1, 'x']); @@ -3505,37 +3500,36 @@ Tinytest.add("minimongo - $near operator tests", test => { }); // issue #2077 -Tinytest.add("minimongo - $near and $geometry for legacy coordinates", test => { +Tinytest.add('minimongo - $near and $geometry for legacy coordinates', test => { const coll = new LocalCollection(); coll.insert({ loc: { x: 1, - y: 1 - } + y: 1, + }, }); coll.insert({ - loc: [-1,-1] + loc: [-1, -1], }); coll.insert({ - loc: [40,-10] + loc: [40, -10], }); coll.insert({ loc: { x: -10, - y: 40 - } + y: 40, + }, }); - test.equal(coll.find({ 'loc': { $near: [0, 0], $maxDistance: 4 } }).count(), 2); - test.equal(coll.find({ 'loc': { $near: {$geometry: {type: "Point", coordinates: [0, 0]}}} }).count(), 4); - test.equal(coll.find({ 'loc': { $near: {$geometry: {type: "Point", coordinates: [0, 0]}, $maxDistance:200000}}}).count(), 2); - + test.equal(coll.find({ loc: { $near: [0, 0], $maxDistance: 4 } }).count(), 2); + test.equal(coll.find({ loc: { $near: {$geometry: {type: 'Point', coordinates: [0, 0]}}} }).count(), 4); + test.equal(coll.find({ loc: { $near: {$geometry: {type: 'Point', coordinates: [0, 0]}, $maxDistance: 200000}}}).count(), 2); }); // Regression test for #4377. Previously, "replace" updates didn't clone the // argument. -Tinytest.add("minimongo - update should clone", test => { +Tinytest.add('minimongo - update should clone', test => { const x = []; const coll = new LocalCollection; const id = coll.insert({}); @@ -3545,7 +3539,7 @@ Tinytest.add("minimongo - update should clone", test => { }); // See #2275. -Tinytest.add("minimongo - fetch in observe", test => { +Tinytest.add('minimongo - fetch in observe', test => { const coll = new LocalCollection; let callbackInvoked = false; const observe = coll.find().observeChanges({ @@ -3555,7 +3549,7 @@ Tinytest.add("minimongo - fetch in observe", test => { const doc = coll.findOne({foo: 1}); test.isTrue(doc); test.equal(doc.foo, 1); - } + }, }); test.isFalse(callbackInvoked); const computation = Tracker.autorun(computation => { @@ -3569,16 +3563,16 @@ Tinytest.add("minimongo - fetch in observe", test => { }); // See #2254 -Tinytest.add("minimongo - fine-grained reactivity of observe with fields projection", test => { +Tinytest.add('minimongo - fine-grained reactivity of observe with fields projection', test => { const X = new LocalCollection; - const id = "asdf"; + const id = 'asdf'; X.insert({_id: id, foo: {bar: 123}}); let callbackInvoked = false; const obs = X.find(id, {fields: {'foo.bar': 1}}).observeChanges({ changed(id, fields) { callbackInvoked = true; - } + }, }); test.isFalse(callbackInvoked); @@ -3587,9 +3581,9 @@ Tinytest.add("minimongo - fine-grained reactivity of observe with fields project obs.stop(); }); -Tinytest.add("minimongo - fine-grained reactivity of query with fields projection", test => { +Tinytest.add('minimongo - fine-grained reactivity of query with fields projection', test => { const X = new LocalCollection; - const id = "asdf"; + const id = 'asdf'; X.insert({_id: id, foo: {bar: 123}}); let callbackInvoked = false; @@ -3611,7 +3605,7 @@ Tinytest.add("minimongo - fine-grained reactivity of query with fields projectio // Tests that the logic in `LocalCollection.prototype.update` // correctly deals with count() on a cursor with skip or limit (since // then the result set is an IdMap, not an array) -Tinytest.add("minimongo - reactive skip/limit count while updating", test => { +Tinytest.add('minimongo - reactive skip/limit count while updating', test => { const X = new LocalCollection; let count = -1; @@ -3644,7 +3638,7 @@ Tinytest.add("minimongo - reactive skip/limit count while updating", test => { // Makes sure inserts cannot be performed using field names that have // Mongo restricted characters in them ('.', '$', '\0'): // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot insert using invalid field names", test => { +Tinytest.add('minimongo - cannot insert using invalid field names', test => { const collection = new LocalCollection(); // Quick test to make sure non-dot field inserts are working @@ -3667,12 +3661,12 @@ Tinytest.add("minimongo - cannot insert using invalid field names", test => { // Verify field names starting with $ are prohibited test.throws(() => { - collection.insert({ '$a': 'b' }); + collection.insert({ $a: 'b' }); }, "Key $a must not start with '$'"); // Verify nested field names starting with $ are prohibited test.throws(() => { - collection.insert({ a: { b: { '$c': 'd' } } }); + collection.insert({ a: { b: { $c: 'd' } } }); }, "Key $c must not start with '$'"); // Verify top level fields with null characters are prohibited @@ -3690,11 +3684,11 @@ Tinytest.add("minimongo - cannot insert using invalid field names", test => { // Makes sure $set's cannot be performed using null bytes // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot $set with null bytes", test => { +Tinytest.add('minimongo - cannot $set with null bytes', test => { const collection = new LocalCollection(); // Quick test to make sure non-null byte $set's are working - const id = collection.insert({ a: 'b', 'c': 'd' }); + const id = collection.insert({ a: 'b', c: 'd' }); collection.update({ _id: id }, { $set: { e: 'f' } }); // Verify $set's with null bytes throw an exception @@ -3705,7 +3699,7 @@ Tinytest.add("minimongo - cannot $set with null bytes", test => { // Makes sure $rename's cannot be performed using null bytes // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -Tinytest.add("minimongo - cannot $rename with null bytes", test => { +Tinytest.add('minimongo - cannot $rename with null bytes', test => { const collection = new LocalCollection(); // Quick test to make sure non-null byte $rename's are working diff --git a/packages/minimongo/minimongo_tests_server.js b/packages/minimongo/minimongo_tests_server.js index b7c59ee8b6..eccb8a980a 100644 --- a/packages/minimongo/minimongo_tests_server.js +++ b/packages/minimongo/minimongo_tests_server.js @@ -1,5 +1,5 @@ -Tinytest.add("minimongo - modifier affects selector", test => { - function testSelectorPaths (sel, paths, desc) { +Tinytest.add('minimongo - modifier affects selector', test => { + function testSelectorPaths(sel, paths, desc) { const matcher = new Minimongo.Matcher(sel); test.equal(matcher._getPaths(), paths, desc); } @@ -7,29 +7,29 @@ Tinytest.add("minimongo - modifier affects selector", test => { testSelectorPaths({ foo: { bar: 3, - baz: 42 - } - }, ['foo'], "literal"); + baz: 42, + }, + }, ['foo'], 'literal'); testSelectorPaths({ foo: 42, - bar: 33 - }, ['foo', 'bar'], "literal"); + bar: 33, + }, ['foo', 'bar'], 'literal'); testSelectorPaths({ foo: [ 'something' ], - bar: "asdf" - }, ['foo', 'bar'], "literal"); + bar: 'asdf', + }, ['foo', 'bar'], 'literal'); testSelectorPaths({ a: { $lt: 3 }, - b: "you know, literal", - 'path.is.complicated': { $not: { $regex: 'acme.*corp' } } - }, ['a', 'b', 'path.is.complicated'], "literal + operators"); + b: 'you know, literal', + 'path.is.complicated': { $not: { $regex: 'acme.*corp' } }, + }, ['a', 'b', 'path.is.complicated'], 'literal + operators'); testSelectorPaths({ $or: [{ 'a.b': 1 }, { 'a.b.c': { $lt: 22 } }, - {$and: [{ 'x.d': { $ne: 5, $gte: 433 } }, { 'a.b': 234 }]}] + {$and: [{ 'x.d': { $ne: 5, $gte: 433 } }, { 'a.b': 234 }]}], }, ['a.b', 'a.b.c', 'x.d'], 'group operators + duplicates'); // When top-level value is an object, it is treated as a literal, @@ -42,23 +42,23 @@ Tinytest.add("minimongo - modifier affects selector", test => { testSelectorPaths({ a: { foo: 1, - bar: 2 + bar: 2, }, 'b.c': { - literal: "object", - but: "we still observe any changes in 'b.c'" - } - }, ['a', 'b.c'], "literal object"); + literal: 'object', + but: "we still observe any changes in 'b.c'", + }, + }, ['a', 'b.c'], 'literal object'); // Note that a and b do NOT end up in the path list, but x and y both do. testSelectorPaths({ $or: [ {x: {$elemMatch: {a: 5}}}, - {y: {$elemMatch: {b: 7}}} - ] - }, ['x', 'y'], "$or and elemMatch"); + {y: {$elemMatch: {b: 7}}}, + ], + }, ['x', 'y'], '$or and elemMatch'); - function testSelectorAffectedByModifier (sel, mod, yes, desc) { + function testSelectorAffectedByModifier(sel, mod, yes, desc) { const matcher = new Minimongo.Matcher(sel); test.equal(matcher.affectedByModifier(mod), yes, desc); } @@ -70,292 +70,291 @@ Tinytest.add("minimongo - modifier affects selector", test => { testSelectorAffectedByModifier(sel, mod, false, desc); } - notAffected({ foo: 0 }, { $set: { bar: 1 } }, "simplest"); - affected({ foo: 0 }, { $set: { foo: 1 } }, "simplest"); - affected({ foo: 0 }, { $set: { 'foo.bar': 1 } }, "simplest"); - notAffected({ 'foo.bar': 0 }, { $set: { 'foo.baz': 1 } }, "simplest"); - affected({ 'foo.bar': 0 }, { $set: { 'foo.1': 1 } }, "simplest"); - affected({ 'foo.bar': 0 }, { $set: { 'foo.2.bar': 1 } }, "simplest"); + notAffected({ foo: 0 }, { $set: { bar: 1 } }, 'simplest'); + affected({ foo: 0 }, { $set: { foo: 1 } }, 'simplest'); + affected({ foo: 0 }, { $set: { 'foo.bar': 1 } }, 'simplest'); + notAffected({ 'foo.bar': 0 }, { $set: { 'foo.baz': 1 } }, 'simplest'); + affected({ 'foo.bar': 0 }, { $set: { 'foo.1': 1 } }, 'simplest'); + affected({ 'foo.bar': 0 }, { $set: { 'foo.2.bar': 1 } }, 'simplest'); - notAffected({ 'foo': 0 }, { $set: { 'foobaz': 1 } }, "correct prefix check"); - notAffected({ 'foobar': 0 }, { $unset: { 'foo': 1 } }, "correct prefix check"); - notAffected({ 'foo.bar': 0 }, { $unset: { 'foob': 1 } }, "correct prefix check"); + notAffected({ foo: 0 }, { $set: { foobaz: 1 } }, 'correct prefix check'); + notAffected({ foobar: 0 }, { $unset: { foo: 1 } }, 'correct prefix check'); + notAffected({ 'foo.bar': 0 }, { $unset: { foob: 1 } }, 'correct prefix check'); - notAffected({ 'foo.Infinity.x': 0 }, { $unset: { 'foo.x': 1 } }, "we convert integer fields correctly"); - notAffected({ 'foo.1e3.x': 0 }, { $unset: { 'foo.x': 1 } }, "we convert integer fields correctly"); + notAffected({ 'foo.Infinity.x': 0 }, { $unset: { 'foo.x': 1 } }, 'we convert integer fields correctly'); + notAffected({ 'foo.1e3.x': 0 }, { $unset: { 'foo.x': 1 } }, 'we convert integer fields correctly'); - affected({ 'foo.3.bar': 0 }, { $set: { 'foo.3.bar': 1 } }, "observe for an array element"); + affected({ 'foo.3.bar': 0 }, { $set: { 'foo.3.bar': 1 } }, 'observe for an array element'); - notAffected({ 'foo.4.bar.baz': 0 }, { $unset: { 'foo.3.bar': 1 } }, "delicate work with numeric fields in selector"); - notAffected({ 'foo.4.bar.baz': 0 }, { $unset: { 'foo.bar': 1 } }, "delicate work with numeric fields in selector"); - affected({ 'foo.4.bar.baz': 0 }, { $unset: { 'foo.4.bar': 1 } }, "delicate work with numeric fields in selector"); - affected({ 'foo.bar.baz': 0 }, { $unset: { 'foo.3.bar': 1 } }, "delicate work with numeric fields in selector"); + notAffected({ 'foo.4.bar.baz': 0 }, { $unset: { 'foo.3.bar': 1 } }, 'delicate work with numeric fields in selector'); + notAffected({ 'foo.4.bar.baz': 0 }, { $unset: { 'foo.bar': 1 } }, 'delicate work with numeric fields in selector'); + affected({ 'foo.4.bar.baz': 0 }, { $unset: { 'foo.4.bar': 1 } }, 'delicate work with numeric fields in selector'); + affected({ 'foo.bar.baz': 0 }, { $unset: { 'foo.3.bar': 1 } }, 'delicate work with numeric fields in selector'); - affected({ 'foo.0.bar': 0 }, { $set: { 'foo.0.0.bar': 1 } }, "delicate work with nested arrays and selectors by indecies"); + affected({ 'foo.0.bar': 0 }, { $set: { 'foo.0.0.bar': 1 } }, 'delicate work with nested arrays and selectors by indecies'); - affected({foo: {$elemMatch: {bar: 5}}}, {$set: {'foo.4.bar': 5}}, "$elemMatch"); + affected({foo: {$elemMatch: {bar: 5}}}, {$set: {'foo.4.bar': 5}}, '$elemMatch'); }); -Tinytest.add("minimongo - selector and projection combination", test => { - function testSelProjectionComb (sel, proj, expected, desc) { +Tinytest.add('minimongo - selector and projection combination', test => { + function testSelProjectionComb(sel, proj, expected, desc) { const matcher = new Minimongo.Matcher(sel); test.equal(matcher.combineIntoProjection(proj), expected, desc); } // Test with inclusive projection - testSelProjectionComb({ a: 1, b: 2 }, { b: 1, c: 1, d: 1 }, { a: true, b: true, c: true, d: true }, "simplest incl"); - testSelProjectionComb({ $or: [{ a: 1234, e: {$lt: 5} }], b: 2 }, { b: 1, c: 1, d: 1 }, { a: true, b: true, c: true, d: true, e: true }, "simplest incl, branching"); + testSelProjectionComb({ a: 1, b: 2 }, { b: 1, c: 1, d: 1 }, { a: true, b: true, c: true, d: true }, 'simplest incl'); + testSelProjectionComb({ $or: [{ a: 1234, e: {$lt: 5} }], b: 2 }, { b: 1, c: 1, d: 1 }, { a: true, b: true, c: true, d: true, e: true }, 'simplest incl, branching'); testSelProjectionComb({ 'a.b': { $lt: 3 }, 'y.0': -1, - 'a.c': 15 + 'a.c': 15, }, { - 'd': 1, - 'z': 1 + d: 1, + z: 1, }, { 'a.b': true, - 'y': true, + y: true, 'a.c': true, - 'd': true, - 'z': true - }, "multikey paths in selector - incl"); + d: true, + z: true, + }, 'multikey paths in selector - incl'); testSelProjectionComb({ foo: 1234, - $and: [{ k: -1 }, { $or: [{ b: 15 }] }] + $and: [{ k: -1 }, { $or: [{ b: 15 }] }], }, { 'foo.bar': 1, 'foo.zzz': 1, - 'b.asdf': 1 + 'b.asdf': 1, }, { foo: true, b: true, - k: true - }, "multikey paths in fields - incl"); + k: true, + }, 'multikey paths in fields - incl'); testSelProjectionComb({ 'a.b.c': 123, 'a.b.d': 321, 'b.c.0': 111, - 'a.e': 12345 + 'a.e': 12345, }, { 'a.b.z': 1, 'a.b.d.g': 1, - 'c.c.c': 1 + 'c.c.c': 1, }, { 'a.b.c': true, 'a.b.d': true, 'a.b.z': true, 'b.c': true, 'a.e': true, - 'c.c.c': true - }, "multikey both paths - incl"); + 'c.c.c': true, + }, 'multikey both paths - incl'); testSelProjectionComb({ 'a.b.c.d': 123, 'a.b1.c.d': 421, - 'a.b.c.e': 111 + 'a.b.c.e': 111, }, { - 'a.b': 1 + 'a.b': 1, }, { 'a.b': true, - 'a.b1.c.d': true - }, "shadowing one another - incl"); + 'a.b1.c.d': true, + }, 'shadowing one another - incl'); testSelProjectionComb({ 'a.b': 123, - 'foo.bar': false + 'foo.bar': false, }, { 'a.b.c.d': 1, - 'foo': 1 + foo: 1, }, { 'a.b': true, - 'foo': true - }, "shadowing one another - incl"); + foo: true, + }, 'shadowing one another - incl'); testSelProjectionComb({ - 'a.b.c': 1 + 'a.b.c': 1, }, { - 'a.b.c': 1 + 'a.b.c': 1, }, { - 'a.b.c': true - }, "same paths - incl"); + 'a.b.c': true, + }, 'same paths - incl'); testSelProjectionComb({ 'x.4.y': 42, - 'z.0.1': 33 + 'z.0.1': 33, }, { - 'x.x': 1 + 'x.x': 1, }, { 'x.x': true, 'x.y': true, - 'z': true - }, "numbered keys in selector - incl"); + z: true, + }, 'numbered keys in selector - incl'); testSelProjectionComb({ 'a.b.c': 42, - $where() { return true; } + $where() { return true; }, }, { 'a.b': 1, - 'z.z': 1 - }, {}, "$where in the selector - incl"); + 'z.z': 1, + }, {}, '$where in the selector - incl'); testSelProjectionComb({ $or: [ {'a.b.c': 42}, - {$where() { return true; } } - ] + {$where() { return true; } }, + ], }, { 'a.b': 1, - 'z.z': 1 - }, {}, "$where in the selector - incl"); + 'z.z': 1, + }, {}, '$where in the selector - incl'); // Test with exclusive projection - testSelProjectionComb({ a: 1, b: 2 }, { b: 0, c: 0, d: 0 }, { c: false, d: false }, "simplest excl"); - testSelProjectionComb({ $or: [{ a: 1234, e: {$lt: 5} }], b: 2 }, { b: 0, c: 0, d: 0 }, { c: false, d: false }, "simplest excl, branching"); + testSelProjectionComb({ a: 1, b: 2 }, { b: 0, c: 0, d: 0 }, { c: false, d: false }, 'simplest excl'); + testSelProjectionComb({ $or: [{ a: 1234, e: {$lt: 5} }], b: 2 }, { b: 0, c: 0, d: 0 }, { c: false, d: false }, 'simplest excl, branching'); testSelProjectionComb({ 'a.b': { $lt: 3 }, 'y.0': -1, - 'a.c': 15 + 'a.c': 15, }, { - 'd': 0, - 'z': 0 + d: 0, + z: 0, }, { d: false, - z: false - }, "multikey paths in selector - excl"); + z: false, + }, 'multikey paths in selector - excl'); testSelProjectionComb({ foo: 1234, - $and: [{ k: -1 }, { $or: [{ b: 15 }] }] + $and: [{ k: -1 }, { $or: [{ b: 15 }] }], }, { 'foo.bar': 0, 'foo.zzz': 0, - 'b.asdf': 0 + 'b.asdf': 0, }, { - }, "multikey paths in fields - excl"); + }, 'multikey paths in fields - excl'); testSelProjectionComb({ 'a.b.c': 123, 'a.b.d': 321, 'b.c.0': 111, - 'a.e': 12345 + 'a.e': 12345, }, { 'a.b.z': 0, 'a.b.d.g': 0, - 'c.c.c': 0 + 'c.c.c': 0, }, { 'a.b.z': false, - 'c.c.c': false - }, "multikey both paths - excl"); + 'c.c.c': false, + }, 'multikey both paths - excl'); testSelProjectionComb({ 'a.b.c.d': 123, 'a.b1.c.d': 421, - 'a.b.c.e': 111 + 'a.b.c.e': 111, }, { - 'a.b': 0 + 'a.b': 0, }, { - }, "shadowing one another - excl"); + }, 'shadowing one another - excl'); testSelProjectionComb({ 'a.b': 123, - 'foo.bar': false + 'foo.bar': false, }, { 'a.b.c.d': 0, - 'foo': 0 + foo: 0, }, { - }, "shadowing one another - excl"); + }, 'shadowing one another - excl'); testSelProjectionComb({ - 'a.b.c': 1 + 'a.b.c': 1, }, { - 'a.b.c': 0 + 'a.b.c': 0, }, { - }, "same paths - excl"); + }, 'same paths - excl'); testSelProjectionComb({ 'a.b': 123, 'a.c.d': 222, - 'ddd': 123 + ddd: 123, }, { 'a.b': 0, 'a.c.e': 0, - 'asdf': 0 + asdf: 0, }, { 'a.c.e': false, - 'asdf': false - }, "intercept the selector path - excl"); + asdf: false, + }, 'intercept the selector path - excl'); testSelProjectionComb({ - 'a.b.c': 14 + 'a.b.c': 14, }, { - 'a.b.d': 0 + 'a.b.d': 0, }, { - 'a.b.d': false - }, "different branches - excl"); + 'a.b.d': false, + }, 'different branches - excl'); testSelProjectionComb({ - 'a.b.c.d': "124", - 'foo.bar.baz.que': "some value" + 'a.b.c.d': '124', + 'foo.bar.baz.que': 'some value', }, { 'a.b.c.d.e': 0, - 'foo.bar': 0 + 'foo.bar': 0, }, { - }, "excl on incl paths - excl"); + }, 'excl on incl paths - excl'); testSelProjectionComb({ 'x.4.y': 42, - 'z.0.1': 33 + 'z.0.1': 33, }, { 'x.x': 0, - 'x.y': 0 + 'x.y': 0, }, { 'x.x': false, - }, "numbered keys in selector - excl"); + }, 'numbered keys in selector - excl'); testSelProjectionComb({ 'a.b.c': 42, - $where() { return true; } + $where() { return true; }, }, { 'a.b': 0, - 'z.z': 0 - }, {}, "$where in the selector - excl"); + 'z.z': 0, + }, {}, '$where in the selector - excl'); testSelProjectionComb({ $or: [ {'a.b.c': 42}, - {$where() { return true; } } - ] + {$where() { return true; } }, + ], }, { 'a.b': 0, - 'z.z': 0 - }, {}, "$where in the selector - excl"); - + 'z.z': 0, + }, {}, '$where in the selector - excl'); }); -Tinytest.add("minimongo - sorter and projection combination", test => { - function testSorterProjectionComb (sortSpec, proj, expected, desc) { +Tinytest.add('minimongo - sorter and projection combination', test => { + function testSorterProjectionComb(sortSpec, proj, expected, desc) { const sorter = new Minimongo.Sorter(sortSpec); test.equal(sorter.combineIntoProjection(proj), expected, desc); } // Test with inclusive projection - testSorterProjectionComb({ a: 1, b: 1 }, { b: 1, c: 1, d: 1 }, { a: true, b: true, c: true, d: true }, "simplest incl"); - testSorterProjectionComb({ a: 1, b: -1 }, { b: 1, c: 1, d: 1 }, { a: true, b: true, c: true, d: true }, "simplest incl"); - testSorterProjectionComb({ 'a.c': 1 }, { b: 1 }, { 'a.c': true, b: true }, "dot path incl"); - testSorterProjectionComb({ 'a.1.c': 1 }, { b: 1 }, { 'a.c': true, b: true }, "dot num path incl"); - testSorterProjectionComb({ 'a.1.c': 1 }, { b: 1, a: 1 }, { a: true, b: true }, "dot num path incl overlap"); - testSorterProjectionComb({ 'a.1.c': 1, 'a.2.b': -1 }, { b: 1 }, { 'a.c': true, 'a.b': true, b: true }, "dot num path incl"); - testSorterProjectionComb({ 'a.1.c': 1, 'a.2.b': -1 }, {}, {}, "dot num path with empty incl"); + testSorterProjectionComb({ a: 1, b: 1 }, { b: 1, c: 1, d: 1 }, { a: true, b: true, c: true, d: true }, 'simplest incl'); + testSorterProjectionComb({ a: 1, b: -1 }, { b: 1, c: 1, d: 1 }, { a: true, b: true, c: true, d: true }, 'simplest incl'); + testSorterProjectionComb({ 'a.c': 1 }, { b: 1 }, { 'a.c': true, b: true }, 'dot path incl'); + testSorterProjectionComb({ 'a.1.c': 1 }, { b: 1 }, { 'a.c': true, b: true }, 'dot num path incl'); + testSorterProjectionComb({ 'a.1.c': 1 }, { b: 1, a: 1 }, { a: true, b: true }, 'dot num path incl overlap'); + testSorterProjectionComb({ 'a.1.c': 1, 'a.2.b': -1 }, { b: 1 }, { 'a.c': true, 'a.b': true, b: true }, 'dot num path incl'); + testSorterProjectionComb({ 'a.1.c': 1, 'a.2.b': -1 }, {}, {}, 'dot num path with empty incl'); // Test with exclusive projection - testSorterProjectionComb({ a: 1, b: 1 }, { b: 0, c: 0, d: 0 }, { c: false, d: false }, "simplest excl"); - testSorterProjectionComb({ a: 1, b: -1 }, { b: 0, c: 0, d: 0 }, { c: false, d: false }, "simplest excl"); - testSorterProjectionComb({ 'a.c': 1 }, { b: 0 }, { b: false }, "dot path excl"); - testSorterProjectionComb({ 'a.1.c': 1 }, { b: 0 }, { b: false }, "dot num path excl"); - testSorterProjectionComb({ 'a.1.c': 1 }, { b: 0, a: 0 }, { b: false }, "dot num path excl overlap"); - testSorterProjectionComb({ 'a.1.c': 1, 'a.2.b': -1 }, { b: 0 }, { b: false }, "dot num path excl"); + testSorterProjectionComb({ a: 1, b: 1 }, { b: 0, c: 0, d: 0 }, { c: false, d: false }, 'simplest excl'); + testSorterProjectionComb({ a: 1, b: -1 }, { b: 0, c: 0, d: 0 }, { c: false, d: false }, 'simplest excl'); + testSorterProjectionComb({ 'a.c': 1 }, { b: 0 }, { b: false }, 'dot path excl'); + testSorterProjectionComb({ 'a.1.c': 1 }, { b: 0 }, { b: false }, 'dot num path excl'); + testSorterProjectionComb({ 'a.1.c': 1 }, { b: 0, a: 0 }, { b: false }, 'dot num path excl overlap'); + testSorterProjectionComb({ 'a.1.c': 1, 'a.2.b': -1 }, { b: 0 }, { b: false }, 'dot num path excl'); }); @@ -384,188 +383,187 @@ Tinytest.add("minimongo - sorter and projection combination", test => { const matcher = new Minimongo.Matcher(sel); test.equal(matcher.canBecomeTrueByModifier(mod), expected, desc); }; - function T (sel, mod, desc) { + function T(sel, mod, desc) { oneTest(sel, mod, true, desc); } - function F (sel, mod, desc) { + function F(sel, mod, desc) { oneTest(sel, mod, false, desc); } - Tinytest.add("minimongo - can selector become true by modifier - literals (structured tests)", t => { + Tinytest.add('minimongo - can selector become true by modifier - literals (structured tests)', t => { test = t; const selector = { 'a.b.c': 2, 'foo.bar': { - z: { y: 1 } + z: { y: 1 }, }, - 'foo.baz': [ {ans: 42}, "string", false, undefined ], - 'empty.field': null + 'foo.baz': [ {ans: 42}, 'string', false, undefined ], + 'empty.field': null, }; - T(selector, {$set:{ 'a.b.c': 2 }}); - F(selector, {$unset:{ 'a': 1 }}); - F(selector, {$unset:{ 'a.b': 1 }}); - F(selector, {$unset:{ 'a.b.c': 1 }}); - T(selector, {$set:{ 'a.b': { c: 2 } }}); - F(selector, {$set:{ 'a.b': {} }}); - T(selector, {$set:{ 'a.b': { c: 2, x: 5 } }}); - F(selector, {$set:{ 'a.b.c.k': 3 }}); - F(selector, {$set:{ 'a.b.c.k': {} }}); + T(selector, {$set: { 'a.b.c': 2 }}); + F(selector, {$unset: { a: 1 }}); + F(selector, {$unset: { 'a.b': 1 }}); + F(selector, {$unset: { 'a.b.c': 1 }}); + T(selector, {$set: { 'a.b': { c: 2 } }}); + F(selector, {$set: { 'a.b': {} }}); + T(selector, {$set: { 'a.b': { c: 2, x: 5 } }}); + F(selector, {$set: { 'a.b.c.k': 3 }}); + F(selector, {$set: { 'a.b.c.k': {} }}); - F(selector, {$unset:{ 'foo': 1 }}); - F(selector, {$unset:{ 'foo.bar': 1 }}); - F(selector, {$unset:{ 'foo.bar.z': 1 }}); - F(selector, {$unset:{ 'foo.bar.z.y': 1 }}); - F(selector, {$set:{ 'foo.bar.x': 1 }}); - F(selector, {$set:{ 'foo.bar': {} }}); - F(selector, {$set:{ 'foo.bar': 3 }}); - T(selector, {$set:{ 'foo.bar': { z: { y: 1 } } }}); - T(selector, {$set:{ 'foo.bar.z': { y: 1 } }}); - T(selector, {$set:{ 'foo.bar.z.y': 1 }}); + F(selector, {$unset: { foo: 1 }}); + F(selector, {$unset: { 'foo.bar': 1 }}); + F(selector, {$unset: { 'foo.bar.z': 1 }}); + F(selector, {$unset: { 'foo.bar.z.y': 1 }}); + F(selector, {$set: { 'foo.bar.x': 1 }}); + F(selector, {$set: { 'foo.bar': {} }}); + F(selector, {$set: { 'foo.bar': 3 }}); + T(selector, {$set: { 'foo.bar': { z: { y: 1 } } }}); + T(selector, {$set: { 'foo.bar.z': { y: 1 } }}); + T(selector, {$set: { 'foo.bar.z.y': 1 }}); - F(selector, {$set:{ 'empty.field': {} }}); - T(selector, {$set:{ 'empty': {} }}); - T(selector, {$set:{ 'empty.field': null }}); - T(selector, {$set:{ 'empty.field': undefined }}); - F(selector, {$set:{ 'empty.field.a': 3 }}); + F(selector, {$set: { 'empty.field': {} }}); + T(selector, {$set: { empty: {} }}); + T(selector, {$set: { 'empty.field': null }}); + T(selector, {$set: { 'empty.field': undefined }}); + F(selector, {$set: { 'empty.field.a': 3 }}); }); - Tinytest.add("minimongo - can selector become true by modifier - literals (adhoc tests)", t => { + Tinytest.add('minimongo - can selector become true by modifier - literals (adhoc tests)', t => { test = t; - T({x:1}, {$set:{x:1}}, "simple set scalar"); - T({x:"a"}, {$set:{x:"a"}}, "simple set scalar"); - T({x:false}, {$set:{x:false}}, "simple set scalar"); - F({x:true}, {$set:{x:false}}, "simple set scalar"); - F({x:2}, {$set:{x:3}}, "simple set scalar"); + T({x: 1}, {$set: {x: 1}}, 'simple set scalar'); + T({x: 'a'}, {$set: {x: 'a'}}, 'simple set scalar'); + T({x: false}, {$set: {x: false}}, 'simple set scalar'); + F({x: true}, {$set: {x: false}}, 'simple set scalar'); + F({x: 2}, {$set: {x: 3}}, 'simple set scalar'); - F({'foo.bar.baz': 1, x:1}, {$unset:{'foo.bar.baz': 1}, $set:{x:1}}, "simple unset of the interesting path"); - F({'foo.bar.baz': 1, x:1}, {$unset:{'foo.bar': 1}, $set:{x:1}}, "simple unset of the interesting path prefix"); - F({'foo.bar.baz': 1, x:1}, {$unset:{'foo': 1}, $set:{x:1}}, "simple unset of the interesting path prefix"); - F({'foo.bar.baz': 1}, {$unset:{'foo.baz': 1}}, "simple unset of the interesting path prefix"); - F({'foo.bar.baz': 1}, {$unset:{'foo.bar.bar': 1}}, "simple unset of the interesting path prefix"); + F({'foo.bar.baz': 1, x: 1}, {$unset: {'foo.bar.baz': 1}, $set: {x: 1}}, 'simple unset of the interesting path'); + F({'foo.bar.baz': 1, x: 1}, {$unset: {'foo.bar': 1}, $set: {x: 1}}, 'simple unset of the interesting path prefix'); + F({'foo.bar.baz': 1, x: 1}, {$unset: {foo: 1}, $set: {x: 1}}, 'simple unset of the interesting path prefix'); + F({'foo.bar.baz': 1}, {$unset: {'foo.baz': 1}}, 'simple unset of the interesting path prefix'); + F({'foo.bar.baz': 1}, {$unset: {'foo.bar.bar': 1}}, 'simple unset of the interesting path prefix'); }); - Tinytest.add("minimongo - can selector become true by modifier - regexps", t => { + Tinytest.add('minimongo - can selector become true by modifier - regexps', t => { test = t; // Regexp - T({ 'foo.bar': /^[0-9]+$/i }, { $set: {'foo.bar': '01233'} }, "set of regexp"); + T({ 'foo.bar': /^[0-9]+$/i }, { $set: {'foo.bar': '01233'} }, 'set of regexp'); // XXX this test should be False, should be fixed within improved implementation - T({ 'foo.bar': /^[0-9]+$/i, x: 1 }, { $set: {'foo.bar': '0a1233', x: 1} }, "set of regexp"); + T({ 'foo.bar': /^[0-9]+$/i, x: 1 }, { $set: {'foo.bar': '0a1233', x: 1} }, 'set of regexp'); // XXX this test should be False, should be fixed within improved implementation - T({ 'foo.bar': /^[0-9]+$/i, x: 1 }, { $unset: {'foo.bar': 1}, $set: { x: 1 } }, "unset of regexp"); + T({ 'foo.bar': /^[0-9]+$/i, x: 1 }, { $unset: {'foo.bar': 1}, $set: { x: 1 } }, 'unset of regexp'); T({ 'foo.bar': /^[0-9]+$/i, x: 1 }, { $set: { x: 1 } }, "don't touch regexp"); }); - Tinytest.add("minimongo - can selector become true by modifier - undefined/null", t => { + Tinytest.add('minimongo - can selector become true by modifier - undefined/null', t => { test = t; // Nulls / Undefined - T({ 'foo.bar': null }, {$set:{'foo.bar': null}}, "set of null looking for null"); - T({ 'foo.bar': null }, {$set:{'foo.bar': undefined}}, "set of undefined looking for null"); - T({ 'foo.bar': undefined }, {$set:{'foo.bar': null}}, "set of null looking for undefined"); - T({ 'foo.bar': undefined }, {$set:{'foo.bar': undefined}}, "set of undefined looking for undefined"); - T({ 'foo.bar': null }, {$set:{'foo': null}}, "set of null of parent path looking for null"); - F({ 'foo.bar': null }, {$set:{'foo.bar.baz': null}}, "set of null of different path looking for null"); - T({ 'foo.bar': null }, { $unset: { 'foo': 1 } }, "unset the parent"); - T({ 'foo.bar': null }, { $unset: { 'foo.bar': 1 } }, "unset tracked path"); - T({ 'foo.bar': null }, { $set: { 'foo': 3 } }, "set the parent"); - T({ 'foo.bar': null }, { $set: { 'foo': {baz:1} } }, "set the parent"); - + T({ 'foo.bar': null }, {$set: {'foo.bar': null}}, 'set of null looking for null'); + T({ 'foo.bar': null }, {$set: {'foo.bar': undefined}}, 'set of undefined looking for null'); + T({ 'foo.bar': undefined }, {$set: {'foo.bar': null}}, 'set of null looking for undefined'); + T({ 'foo.bar': undefined }, {$set: {'foo.bar': undefined}}, 'set of undefined looking for undefined'); + T({ 'foo.bar': null }, {$set: {foo: null}}, 'set of null of parent path looking for null'); + F({ 'foo.bar': null }, {$set: {'foo.bar.baz': null}}, 'set of null of different path looking for null'); + T({ 'foo.bar': null }, { $unset: { foo: 1 } }, 'unset the parent'); + T({ 'foo.bar': null }, { $unset: { 'foo.bar': 1 } }, 'unset tracked path'); + T({ 'foo.bar': null }, { $set: { foo: 3 } }, 'set the parent'); + T({ 'foo.bar': null }, { $set: { foo: {baz: 1} } }, 'set the parent'); }); - Tinytest.add("minimongo - can selector become true by modifier - literals with arrays", t => { + Tinytest.add('minimongo - can selector become true by modifier - literals with arrays', t => { test = t; // These tests are incomplete and in theory they all should return true as we // don't support any case with numeric fields yet. - T({'a.1.b': 1, x:1}, {$unset:{'a.1.b': 1}, $set:{x:1}}, "unset of array element's field with exactly the same index as selector"); - F({'a.2.b': 1}, {$unset:{'a.1.b': 1}}, "unset of array element's field with different index as selector"); + T({'a.1.b': 1, x: 1}, {$unset: {'a.1.b': 1}, $set: {x: 1}}, "unset of array element's field with exactly the same index as selector"); + F({'a.2.b': 1}, {$unset: {'a.1.b': 1}}, "unset of array element's field with different index as selector"); // This is false, because if you are looking for array but in reality it is an // object, it just can't get to true. - F({'a.2.b': 1}, {$unset:{'a.b': 1}}, "unset of field while selector is looking for index"); - T({ 'foo.bar': null }, {$set:{'foo.1.bar': null}}, "set array's element's field to null looking for null"); - T({ 'foo.bar': null }, {$set:{'foo.0.bar': 1, 'foo.1.bar': null}}, "set array's element's field to null looking for null"); + F({'a.2.b': 1}, {$unset: {'a.b': 1}}, 'unset of field while selector is looking for index'); + T({ 'foo.bar': null }, {$set: {'foo.1.bar': null}}, "set array's element's field to null looking for null"); + T({ 'foo.bar': null }, {$set: {'foo.0.bar': 1, 'foo.1.bar': null}}, "set array's element's field to null looking for null"); // This is false, because there may remain other array elements that match // but we modified this test as we don't support this case yet - T({'a.b': 1}, {$unset:{'a.1.b': 1}}, "unset of array element's field"); + T({'a.b': 1}, {$unset: {'a.1.b': 1}}, "unset of array element's field"); }); - Tinytest.add("minimongo - can selector become true by modifier - set an object literal whose fields are selected", t => { + Tinytest.add('minimongo - can selector become true by modifier - set an object literal whose fields are selected', t => { test = t; - T({ 'a.b.c': 1 }, { $set: { 'a.b': { c: 1 } } }, "a simple scalar selector and simple set"); - F({ 'a.b.c': 1 }, { $set: { 'a.b': { c: 2 } } }, "a simple scalar selector and simple set to false"); - F({ 'a.b.c': 1 }, { $set: { 'a.b': { d: 1 } } }, "a simple scalar selector and simple set a wrong literal"); - F({ 'a.b.c': 1 }, { $set: { 'a.b': 222 } }, "a simple scalar selector and simple set a wrong type"); + T({ 'a.b.c': 1 }, { $set: { 'a.b': { c: 1 } } }, 'a simple scalar selector and simple set'); + F({ 'a.b.c': 1 }, { $set: { 'a.b': { c: 2 } } }, 'a simple scalar selector and simple set to false'); + F({ 'a.b.c': 1 }, { $set: { 'a.b': { d: 1 } } }, 'a simple scalar selector and simple set a wrong literal'); + F({ 'a.b.c': 1 }, { $set: { 'a.b': 222 } }, 'a simple scalar selector and simple set a wrong type'); }); - Tinytest.add("minimongo - can selector become true by modifier - $-scalar selectors and simple tests", t => { + Tinytest.add('minimongo - can selector become true by modifier - $-scalar selectors and simple tests', t => { test = t; - T({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { c: 4 } } }, "nested $lt"); - F({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { c: 5 } } }, "nested $lt"); - F({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { c: 6 } } }, "nested $lt"); + T({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { c: 4 } } }, 'nested $lt'); + F({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { c: 5 } } }, 'nested $lt'); + F({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { c: 6 } } }, 'nested $lt'); F({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b.d': 7 } }, "nested $lt, the change doesn't matter"); - F({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { d: 7 } } }, "nested $lt, the key disappears"); - T({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { d: 7, c: -1 } } }, "nested $lt"); - F({ a: { $lt: 10, $gt: 3 } }, { $unset: { a: 1 } }, "unset $lt"); - T({ a: { $lt: 10, $gt: 3 } }, { $set: { a: 4 } }, "set between x and y"); - F({ a: { $lt: 10, $gt: 3 } }, { $set: { a: 3 } }, "set between x and y"); - F({ a: { $lt: 10, $gt: 3 } }, { $set: { a: 10 } }, "set between x and y"); - F({ a: { $gt: 10, $lt: 3 } }, { $set: { a: 9 } }, "impossible statement"); - T({ a: { $lte: 10, $gte: 3 } }, { $set: { a: 3 } }, "set between x and y"); - T({ a: { $lte: 10, $gte: 3 } }, { $set: { a: 10 } }, "set between x and y"); - F({ a: { $lte: 10, $gte: 3 } }, { $set: { a: -10 } }, "set between x and y"); - T({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 } }, { $set: { a: 4 } }, "set between x and y"); - F({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 } }, { $set: { a: 3 } }, "set between x and y"); - F({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 } }, { $set: { a: 10 } }, "set between x and y"); - F({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 } }, { $set: { a: Infinity } }, "set between x and y"); - T({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 }, x: 1 }, { $set: { x: 1 } }, "set between x and y - dummy"); - F({ a: { $lte: 10, $gte: 13, $gt: 3, $lt: 9 }, x: 1 }, { $set: { x: 1 } }, "set between x and y - dummy - impossible"); - F({ a: { $lte: 10 } }, { $set: { a: Infinity } }, "Infinity <= 10?"); - T({ a: { $lte: 10 } }, { $set: { a: -Infinity } }, "-Infinity <= 10?"); + F({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { d: 7 } } }, 'nested $lt, the key disappears'); + T({ 'a.b.c': { $lt: 5 } }, { $set: { 'a.b': { d: 7, c: -1 } } }, 'nested $lt'); + F({ a: { $lt: 10, $gt: 3 } }, { $unset: { a: 1 } }, 'unset $lt'); + T({ a: { $lt: 10, $gt: 3 } }, { $set: { a: 4 } }, 'set between x and y'); + F({ a: { $lt: 10, $gt: 3 } }, { $set: { a: 3 } }, 'set between x and y'); + F({ a: { $lt: 10, $gt: 3 } }, { $set: { a: 10 } }, 'set between x and y'); + F({ a: { $gt: 10, $lt: 3 } }, { $set: { a: 9 } }, 'impossible statement'); + T({ a: { $lte: 10, $gte: 3 } }, { $set: { a: 3 } }, 'set between x and y'); + T({ a: { $lte: 10, $gte: 3 } }, { $set: { a: 10 } }, 'set between x and y'); + F({ a: { $lte: 10, $gte: 3 } }, { $set: { a: -10 } }, 'set between x and y'); + T({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 } }, { $set: { a: 4 } }, 'set between x and y'); + F({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 } }, { $set: { a: 3 } }, 'set between x and y'); + F({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 } }, { $set: { a: 10 } }, 'set between x and y'); + F({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 } }, { $set: { a: Infinity } }, 'set between x and y'); + T({ a: { $lte: 10, $gte: 3, $gt: 3, $lt: 10 }, x: 1 }, { $set: { x: 1 } }, 'set between x and y - dummy'); + F({ a: { $lte: 10, $gte: 13, $gt: 3, $lt: 9 }, x: 1 }, { $set: { x: 1 } }, 'set between x and y - dummy - impossible'); + F({ a: { $lte: 10 } }, { $set: { a: Infinity } }, 'Infinity <= 10?'); + T({ a: { $lte: 10 } }, { $set: { a: -Infinity } }, '-Infinity <= 10?'); // XXX is this sufficient? - T({ a: { $gt: 9.99999999999999, $lt: 10 }, x: 1 }, { $set: { x: 1 } }, "very close $gt and $lt"); + T({ a: { $gt: 9.99999999999999, $lt: 10 }, x: 1 }, { $set: { x: 1 } }, 'very close $gt and $lt'); // XXX this test should be F, but since it is so hard to be precise in // floating point math, the current implementation falls back to T - T({ a: { $gt: 9.999999999999999, $lt: 10 }, x: 1 }, { $set: { x: 1 } }, "very close $gt and $lt"); - T({ a: { $eq: 5 } }, { $set: { a: 5 } }, "set of $eq"); - T({ a: { $eq: 5 }, b: { $eq: 7 } }, { $set: { a: 5 } }, "set of $eq with other $eq"); - F({ a: { $eq: 5 } }, { $set: { a: 4 } }, "set below of $eq"); - F({ a: { $eq: 5 } }, { $set: { a: 6 } }, "set above of $eq"); - T({ a: { $ne: 5 } }, { $unset: { a: 1 } }, "unset of $ne"); - T({ a: { $ne: 5 } }, { $set: { a: 1 } }, "set of $ne"); - T({ a: { $ne: "some string" }, x: 1 }, { $set: { x: 1 } }, "$ne dummy"); - T({ a: { $ne: true }, x: 1 }, { $set: { x: 1 } }, "$ne dummy"); - T({ a: { $ne: false }, x: 1 }, { $set: { x: 1 } }, "$ne dummy"); - T({ a: { $ne: null }, x: 1 }, { $set: { x: 1 } }, "$ne dummy"); - T({ a: { $ne: Infinity }, x: 1 }, { $set: { x: 1 } }, "$ne dummy"); - T({ a: { $ne: 5 } }, { $set: { a: -10 } }, "set of $ne"); - T({ a: { $in: [1, 3, 5, 7] } }, { $set: { a: 5 } }, "$in checks"); - F({ a: { $in: [1, 3, 5, 7] } }, { $set: { a: -5 } }, "$in checks"); - T({ a: { $in: [1, 3, 5, 7], $gt: 6 }, x: 1 }, { $set: { x: 1 } }, "$in combination with $gt"); - F({ a: { $lte: 10, $gte: 3 } }, { $set: { 'a.b': -10 } }, "sel between x and y, set its subfield"); - F({ b: { $in: [1, 3, 5, 7] } }, { $set: { 'b.c': 2 } }, "sel $in, set subfield"); - T({ b: { $in: [1, 3, 5, 7] } }, { $set: { 'bd.c': 2, b: 3 } }, "sel $in, set similar subfield"); - F({ 'b.c': { $in: [1, 3, 5, 7] } }, { $set: { b: 2 } }, "sel subfield of set scalar"); + T({ a: { $gt: 9.999999999999999, $lt: 10 }, x: 1 }, { $set: { x: 1 } }, 'very close $gt and $lt'); + T({ a: { $eq: 5 } }, { $set: { a: 5 } }, 'set of $eq'); + T({ a: { $eq: 5 }, b: { $eq: 7 } }, { $set: { a: 5 } }, 'set of $eq with other $eq'); + F({ a: { $eq: 5 } }, { $set: { a: 4 } }, 'set below of $eq'); + F({ a: { $eq: 5 } }, { $set: { a: 6 } }, 'set above of $eq'); + T({ a: { $ne: 5 } }, { $unset: { a: 1 } }, 'unset of $ne'); + T({ a: { $ne: 5 } }, { $set: { a: 1 } }, 'set of $ne'); + T({ a: { $ne: 'some string' }, x: 1 }, { $set: { x: 1 } }, '$ne dummy'); + T({ a: { $ne: true }, x: 1 }, { $set: { x: 1 } }, '$ne dummy'); + T({ a: { $ne: false }, x: 1 }, { $set: { x: 1 } }, '$ne dummy'); + T({ a: { $ne: null }, x: 1 }, { $set: { x: 1 } }, '$ne dummy'); + T({ a: { $ne: Infinity }, x: 1 }, { $set: { x: 1 } }, '$ne dummy'); + T({ a: { $ne: 5 } }, { $set: { a: -10 } }, 'set of $ne'); + T({ a: { $in: [1, 3, 5, 7] } }, { $set: { a: 5 } }, '$in checks'); + F({ a: { $in: [1, 3, 5, 7] } }, { $set: { a: -5 } }, '$in checks'); + T({ a: { $in: [1, 3, 5, 7], $gt: 6 }, x: 1 }, { $set: { x: 1 } }, '$in combination with $gt'); + F({ a: { $lte: 10, $gte: 3 } }, { $set: { 'a.b': -10 } }, 'sel between x and y, set its subfield'); + F({ b: { $in: [1, 3, 5, 7] } }, { $set: { 'b.c': 2 } }, 'sel $in, set subfield'); + T({ b: { $in: [1, 3, 5, 7] } }, { $set: { 'bd.c': 2, b: 3 } }, 'sel $in, set similar subfield'); + F({ 'b.c': { $in: [1, 3, 5, 7] } }, { $set: { b: 2 } }, 'sel subfield of set scalar'); // If modifier tries to set a sub-field of a path expected to be a scalar. - F({ 'a.b': { $gt: 5, $lt: 7}, x: 1 }, { $set: { 'a.b.c': 3, x: 1 } }, "set sub-field of $gt,$lt operator (scalar expected)"); - F({ 'a.b': { $gt: 5, $lt: 7}, x: 1 }, { $set: { x: 1 }, $unset: { 'a.b.c': 1 } }, "unset sub-field of $gt,$lt operator (scalar expected)"); + F({ 'a.b': { $gt: 5, $lt: 7}, x: 1 }, { $set: { 'a.b.c': 3, x: 1 } }, 'set sub-field of $gt,$lt operator (scalar expected)'); + F({ 'a.b': { $gt: 5, $lt: 7}, x: 1 }, { $set: { x: 1 }, $unset: { 'a.b.c': 1 } }, 'unset sub-field of $gt,$lt operator (scalar expected)'); }); - Tinytest.add("minimongo - can selector become true by modifier - $-nonscalar selectors and simple tests", t => { + Tinytest.add('minimongo - can selector become true by modifier - $-nonscalar selectors and simple tests', t => { test = t; - T({ a: { $eq: { x: 5 } } }, { $set: { 'a.x': 5 } }, "set of $eq"); + T({ a: { $eq: { x: 5 } } }, { $set: { 'a.x': 5 } }, 'set of $eq'); // XXX this test should be F, but it is not implemented yet - T({ a: { $eq: { x: 5 } } }, { $set: { 'a.x': 4 } }, "set of $eq"); + T({ a: { $eq: { x: 5 } } }, { $set: { 'a.x': 4 } }, 'set of $eq'); // XXX this test should be F, but it is not implemented yet - T({ a: { $eq: { x: 5 } } }, { $set: { 'a.y': 4 } }, "set of $eq"); - T({ a: { $ne: { x: 5 } } }, { $set: { 'a.x': 3 } }, "set of $ne"); + T({ a: { $eq: { x: 5 } } }, { $set: { 'a.y': 4 } }, 'set of $eq'); + T({ a: { $ne: { x: 5 } } }, { $set: { 'a.x': 3 } }, 'set of $ne'); // XXX this test should be F, but it is not implemented yet - T({ a: { $ne: { x: 5 } } }, { $set: { 'a.x': 5 } }, "set of $ne"); - T({ a: { $in: [{ b: 1 }, { b: 3 }] } }, { $set: { a: { b: 3 } } }, "$in checks"); + T({ a: { $ne: { x: 5 } } }, { $set: { 'a.x': 5 } }, 'set of $ne'); + T({ a: { $in: [{ b: 1 }, { b: 3 }] } }, { $set: { a: { b: 3 } } }, '$in checks'); // XXX this test should be F, but it is not implemented yet - T({ a: { $in: [{ b: 1 }, { b: 3 }] } }, { $set: { a: { v: 3 } } }, "$in checks"); - T({ a: { $ne: { a: 2 } }, x: 1 }, { $set: { x: 1 } }, "$ne dummy"); + T({ a: { $in: [{ b: 1 }, { b: 3 }] } }, { $set: { a: { v: 3 } } }, '$in checks'); + T({ a: { $ne: { a: 2 } }, x: 1 }, { $set: { x: 1 } }, '$ne dummy'); // XXX this test should be F, but it is not implemented yet - T({ a: { $ne: { a: 2 } } }, { $set: { a: { a: 2 } } }, "$ne object"); + T({ a: { $ne: { a: 2 } } }, { $set: { a: { a: 2 } } }, '$ne object'); }); }))(); diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index bc9682ff47..4b00459aae 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'Meteor\'s client-side datastore: a port of MongoDB to Javascript', - version: '1.2.1' + version: '1.2.1', }); Package.onUse(api => { @@ -19,7 +19,7 @@ Package.onUse(api => { 'mongo-id', 'ordered-dict', 'random', - 'tracker' + 'tracker', ]); api.mainModule('main.js', 'client'); @@ -37,7 +37,7 @@ Package.onTest(api => { 'reactive-var', 'test-helpers', 'tinytest', - 'tracker' + 'tracker', ]); api.addFiles('minimongo_tests.js'); diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index 5a4ba04061..223f76ca09 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -4,7 +4,7 @@ import { expandArraysInBranches, isOperatorObject, makeLookupFunction, - regexpElementMatcher + regexpElementMatcher, } from './common.js'; // Give a sort spec, which can be in any of these forms: @@ -21,44 +21,41 @@ import { // first, or 0 if neither object comes before the other. export class Sorter { - constructor (spec, options = {}) { + constructor(spec, options = {}) { this._sortSpecParts = []; this._sortFunction = null; const addSpecPart = (path, ascending) => { - if (!path) - throw Error("sort keys must be non-empty"); - if (path.charAt(0) === '$') - throw Error(`unsupported sort key: ${path}`); + if (!path) {throw Error('sort keys must be non-empty');} + if (path.charAt(0) === '$') {throw Error(`unsupported sort key: ${path}`);} this._sortSpecParts.push({ path, lookup: makeLookupFunction(path, {forSort: true}), - ascending + ascending, }); }; if (spec instanceof Array) { for (let i = 0; i < spec.length; i++) { - if (typeof spec[i] === "string") { + if (typeof spec[i] === 'string') { addSpecPart(spec[i], true); } else { - addSpecPart(spec[i][0], spec[i][1] !== "desc"); + addSpecPart(spec[i][0], spec[i][1] !== 'desc'); } } - } else if (typeof spec === "object") { + } else if (typeof spec === 'object') { Object.keys(spec).forEach(key => { const value = spec[key]; addSpecPart(key, value >= 0); }); - } else if (typeof spec === "function") { + } else if (typeof spec === 'function') { this._sortFunction = spec; } else { throw Error(`Bad sort specification: ${JSON.stringify(spec)}`); } // If a function is specified for sorting, we skip the rest. - if (this._sortFunction) - return; + if (this._sortFunction) {return;} // To implement affectedByModifier, we piggy-back on top of Matcher's // affectedByModifier code; we create a selector that is affected by the same @@ -81,7 +78,7 @@ export class Sorter { options.matcher && this._useWithMatcher(options.matcher); } - getComparator (options) { + getComparator(options) { // If sort is specified or have no distances, just use the comparator from // the source specification (which defaults to "everything is equal". // issue #3599 @@ -95,10 +92,8 @@ export class Sorter { // Return a comparator which compares using $near distances. return (a, b) => { - if (!distances.has(a._id)) - throw Error(`Missing distance for ${a._id}`); - if (!distances.has(b._id)) - throw Error(`Missing distance for ${b._id}`); + if (!distances.has(a._id)) {throw Error(`Missing distance for ${a._id}`);} + if (!distances.has(b._id)) {throw Error(`Missing distance for ${b._id}`);} return distances.get(a._id) - distances.get(b._id); }; } @@ -106,10 +101,10 @@ export class Sorter { // Takes in two keys: arrays whose lengths match the number of spec // parts. Returns negative, 0, or positive based on using the sort spec to // compare fields. - _compareKeys (key1, key2) { + _compareKeys(key1, key2) { if (key1.length !== this._sortSpecParts.length || key2.length !== this._sortSpecParts.length) { - throw Error("Key has wrong length"); + throw Error('Key has wrong length'); } return this._keyComparator(key1, key2); @@ -117,9 +112,8 @@ export class Sorter { // Iterates over each possible "key" from doc (ie, over each branch), calling // 'cb' with the key. - _generateKeysFromDoc (doc, cb) { - if (this._sortSpecParts.length === 0) - throw new Error("can't generate keys without a spec"); + _generateKeysFromDoc(doc, cb) { + if (this._sortSpecParts.length === 0) {throw new Error("can't generate keys without a spec");} // maps index -> ({'' -> value} or {path -> value}) const valuesByIndexAndPath = []; @@ -135,8 +129,7 @@ export class Sorter { // If there are no values for a key (eg, key goes to an empty array), // pretend we found one null value. - if (!branches.length) - branches = [{value: null}]; + if (!branches.length) {branches = [{value: null}];} let usedPaths = false; valuesByIndexAndPath[whichField] = {}; @@ -145,16 +138,14 @@ export class Sorter { // If there are no array indices for a branch, then it must be the // only branch, because the only thing that produces multiple branches // is the use of arrays. - if (branches.length > 1) - throw Error("multiple branches but no array used?"); + if (branches.length > 1) {throw Error('multiple branches but no array used?');} valuesByIndexAndPath[whichField][''] = branch.value; return; } usedPaths = true; const path = pathFromIndices(branch.arrayIndices); - if (valuesByIndexAndPath[whichField].hasOwnProperty(path)) - throw Error(`duplicate path: ${path}`); + if (valuesByIndexAndPath[whichField].hasOwnProperty(path)) {throw Error(`duplicate path: ${path}`);} valuesByIndexAndPath[whichField][path] = branch.value; // If two sort fields both go into arrays, they have to go into the @@ -168,7 +159,7 @@ export class Sorter { // #NestedArraySort // XXX achieve full compatibility here if (knownPaths && !knownPaths.hasOwnProperty(path)) { - throw Error("cannot index parallel arrays"); + throw Error('cannot index parallel arrays'); } }); @@ -177,7 +168,7 @@ export class Sorter { // non-array field. if (!valuesByIndexAndPath[whichField].hasOwnProperty('') && Object.keys(knownPaths).length !== Object.keys(valuesByIndexAndPath[whichField]).length) { - throw Error("cannot index parallel arrays!"); + throw Error('cannot index parallel arrays!'); } } else if (usedPaths) { knownPaths = {}; @@ -190,8 +181,7 @@ export class Sorter { if (!knownPaths) { // Easy case: no use of arrays. const soleKey = valuesByIndexAndPath.map(values => { - if (!values.hasOwnProperty('')) - throw Error("no value in sole key case?"); + if (!values.hasOwnProperty('')) {throw Error('no value in sole key case?');} return values['']; }); cb(soleKey); @@ -200,10 +190,8 @@ export class Sorter { Object.keys(knownPaths).forEach(path => { const key = valuesByIndexAndPath.map(values => { - if (values.hasOwnProperty('')) - return values['']; - if (!values.hasOwnProperty(path)) - throw Error("missing path?"); + if (values.hasOwnProperty('')) {return values[''];} + if (!values.hasOwnProperty(path)) {throw Error('missing path?');} return values[path]; }); cb(key); @@ -212,9 +200,8 @@ export class Sorter { // Returns a comparator that represents the sort specification (but not // including a possible geoquery distance tie-breaker). - _getBaseComparator () { - if (this._sortFunction) - return this._sortFunction; + _getBaseComparator() { + if (this._sortFunction) {return this._sortFunction;} // If we're only sorting on geoquery distance and no specs, just say // everything is equal. @@ -239,12 +226,11 @@ export class Sorter { // you can find along the same paths". ie, for a doc {a: [{x: 0, y: 5}, {x: // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key. - _getMinKeyFromDoc (doc) { + _getMinKeyFromDoc(doc) { let minKey = null; this._generateKeysFromDoc(doc, key => { - if (!this._keyCompatibleWithSelector(key)) - return; + if (!this._keyCompatibleWithSelector(key)) {return;} if (minKey === null) { minKey = key; @@ -257,27 +243,25 @@ export class Sorter { // This could happen if our key filter somehow filters out all the keys even // though somehow the selector matches. - if (minKey === null) - throw Error("sort selector found no keys in doc?"); + if (minKey === null) {throw Error('sort selector found no keys in doc?');} return minKey; } - _getPaths () { + _getPaths() { return this._sortSpecParts.map(part => part.path); } - _keyCompatibleWithSelector (key) { + _keyCompatibleWithSelector(key) { return !this._keyFilter || this._keyFilter(key); } // Given an index 'i', returns a comparator that compares two key arrays based // on field 'i'. - _keyFieldComparator (i) { + _keyFieldComparator(i) { const invert = !this._sortSpecParts[i].ascending; return (key1, key2) => { let compare = LocalCollection._f._cmp(key1[i], key2[i]); - if (invert) - compare = -compare; + if (invert) {compare = -compare;} return compare; }; } @@ -301,22 +285,19 @@ export class Sorter { // skip sort keys that don't match the selector. The logic here is pretty // subtle and undocumented; we've gotten as close as we can figure out based // on our understanding of Mongo's behavior. - _useWithMatcher (matcher) { - if (this._keyFilter) - throw Error("called _useWithMatcher twice?"); + _useWithMatcher(matcher) { + if (this._keyFilter) {throw Error('called _useWithMatcher twice?');} // If we are only sorting by distance, then we're not going to bother to // build a key filter. // XXX figure out how geoqueries interact with this stuff - if (!this._sortSpecParts.length) - return; + if (!this._sortSpecParts.length) {return;} const selector = matcher._selector; // If the user just passed a literal function to find(), then we can't get a // key filter from it. - if (selector instanceof Function) - return; + if (selector instanceof Function) {return;} const constraintsByPath = {}; this._sortSpecParts.forEach((spec, i) => { @@ -328,8 +309,7 @@ export class Sorter { // XXX support $and and $or const constraints = constraintsByPath[key]; - if (!constraints) - return; + if (!constraints) {return;} // XXX it looks like the real MongoDB implementation isn't "does the // regexp match" but "does the value fall into a range named by the @@ -342,8 +322,7 @@ export class Sorter { // index to use, which means it only cares about regexps that match // one range (with a literal prefix), and both 'i' and 'm' prevent the // literal prefix of the regexp from actually meaning one range. - if (subSelector.ignoreCase || subSelector.multiline) - return; + if (subSelector.ignoreCase || subSelector.multiline) {return;} constraints.push(regexpElementMatcher(subSelector)); return; } @@ -378,8 +357,7 @@ export class Sorter { // others; we shouldn't create a key filter unless the first sort field is // restricted, though after that point we can restrict the other sort fields // or not as we wish. - if (!constraintsByPath[this._sortSpecParts[0].path].length) - return; + if (!constraintsByPath[this._sortSpecParts[0].path].length) {return;} this._keyFilter = key => this._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(f => f(key[index]))); } @@ -389,12 +367,11 @@ export class Sorter { // (functions (a,b)->(negative or positive or zero)), returns a single // comparator which uses each comparator in order and returns the first // non-zero value. -function composeComparators (comparatorArray) { +function composeComparators(comparatorArray) { return (a, b) => { for (let i = 0; i < comparatorArray.length; ++i) { const compare = comparatorArray[i](a, b); - if (compare !== 0) - return compare; + if (compare !== 0) {return compare;} } return 0; }; From ed9c8e932ba6d1d425d8462d43fb7afd55077cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 00:41:47 +0200 Subject: [PATCH 12/28] Spreads. --- packages/minimongo/common.js | 2 +- packages/minimongo/local_collection.js | 2 +- packages/minimongo/minimongo_tests_client.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index 5806a91122..89fdfd4362 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -802,7 +802,7 @@ export function makeLookupFunction(key, options = {}) { const result = []; const appendToResult = more => { - Array.prototype.push.apply(result, more); + result.push(...more); }; // Dig deeper: look up the rest of the parts on whatever we've found. diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 237092927e..f4871125c0 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1241,7 +1241,7 @@ const MODIFIERS = { } else { const spliceArguments = [position, 0]; for (let j = 0; j < toPush.length; j++) {spliceArguments.push(toPush[j]);} - Array.prototype.splice.apply(target[field], spliceArguments); + target[field].splice(...spliceArguments); } // Actually sort. diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index d8be2f6d43..f5eacfdbe5 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -2095,7 +2095,7 @@ Tinytest.add('minimongo - array sort', test => { if (doc.hasOwnProperty(field)) {fieldValues.push(doc[field]);} }); test.equal(cursor.fetch().map(doc => doc[field]), - Array.from({length: Math.max.apply(null, fieldValues) + 1}, (x, i) => i)); + Array.from({length: Math.max(...fieldValues) + 1}, (x, i) => i)); }; testCursorMatchesField(c.find({}, {sort: {'a.x': 1}}), 'up'); From f4a59e8bba9130217c67a30d55fc6522a16f3835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 16:41:26 +0200 Subject: [PATCH 13/28] Reorganized entry files. --- packages/minimongo/common.js | 2 +- packages/minimongo/cursor.js | 4 ++-- packages/minimongo/local_collection.js | 6 +++--- packages/minimongo/main.js | 6 ------ packages/minimongo/matcher.js | 4 ++-- packages/minimongo/minimongo_client.js | 1 + packages/minimongo/minimongo_common.js | 10 ++++++++++ .../minimongo/{main_server.js => minimongo_server.js} | 2 +- packages/minimongo/observe_handle.js | 2 +- packages/minimongo/package.js | 4 ++-- packages/minimongo/sorter.js | 2 +- 11 files changed, 24 insertions(+), 19 deletions(-) delete mode 100644 packages/minimongo/main.js create mode 100644 packages/minimongo/minimongo_client.js create mode 100644 packages/minimongo/minimongo_common.js rename packages/minimongo/{main_server.js => minimongo_server.js} (99%) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index 89fdfd4362..fac197c7fc 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -1,4 +1,4 @@ -import {LocalCollection} from './local_collection.js'; +import LocalCollection from './local_collection.js'; // Each element selector contains: // - compileElementSelector, a function with args: diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index 9ce9ef60c8..16753aa6c2 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -1,8 +1,8 @@ -import {LocalCollection} from './local_collection.js'; +import LocalCollection from './local_collection.js'; // Cursor: a specification for a particular subset of documents, w/ // a defined order, limit, and offset. creating a Cursor with LocalCollection.find(), -export class Cursor { +export default class Cursor { // don't call this ctor directly. use LocalCollection.find(). constructor(collection, selector, options = {}) { this.collection = collection; diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index f4871125c0..6b4ae6a2f1 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1,5 +1,5 @@ -import {Cursor} from './cursor.js'; -import {ObserveHandle} from './observe_handle.js'; +import Cursor from './cursor.js'; +import ObserveHandle from './observe_handle.js'; import { isIndexable, isNumericKey, @@ -10,7 +10,7 @@ import { // XXX type checking on selectors (graceful error if malformed) // LocalCollection: a set of documents that supports queries and modifiers. -export class LocalCollection { +export default class LocalCollection { constructor(name) { this.name = name; // _id -> document (also containing id) diff --git a/packages/minimongo/main.js b/packages/minimongo/main.js deleted file mode 100644 index 45cdf002f0..0000000000 --- a/packages/minimongo/main.js +++ /dev/null @@ -1,6 +0,0 @@ -import {LocalCollection as LocalCollection_} from './local_collection.js'; -import {Matcher} from './matcher.js'; -import {Sorter} from './sorter.js'; - -Minimongo = {LocalCollection: LocalCollection_, Matcher, Sorter}; -LocalCollection = LocalCollection_; diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index 6b64c032d7..41367735ec 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -1,4 +1,4 @@ -import {LocalCollection} from './local_collection.js'; +import LocalCollection from './local_collection.js'; import { compileDocumentSelector, nothingMatcher, @@ -24,7 +24,7 @@ import { // Main entry point. // var matcher = new Minimongo.Matcher({a: {$gt: 5}}); // if (matcher.documentMatches({a: 7})) ... -export class Matcher { +export default class Matcher { constructor(selector, isUpdate) { // A set (object mapping string -> *) of all of the document paths looked // at by the selector. Also includes the empty string if it may look at any diff --git a/packages/minimongo/minimongo_client.js b/packages/minimongo/minimongo_client.js new file mode 100644 index 0000000000..cf05a1543a --- /dev/null +++ b/packages/minimongo/minimongo_client.js @@ -0,0 +1 @@ +import './minimongo_common.js'; diff --git a/packages/minimongo/minimongo_common.js b/packages/minimongo/minimongo_common.js new file mode 100644 index 0000000000..15a4e161f0 --- /dev/null +++ b/packages/minimongo/minimongo_common.js @@ -0,0 +1,10 @@ +import LocalCollection_ from './local_collection.js'; +import Matcher from './matcher.js'; +import Sorter from './sorter.js'; + +LocalCollection = LocalCollection_; +Minimongo = { + LocalCollection: LocalCollection_, + Matcher, + Sorter +}; diff --git a/packages/minimongo/main_server.js b/packages/minimongo/minimongo_server.js similarity index 99% rename from packages/minimongo/main_server.js rename to packages/minimongo/minimongo_server.js index 926e5a155e..f4edf32810 100644 --- a/packages/minimongo/main_server.js +++ b/packages/minimongo/minimongo_server.js @@ -1,4 +1,4 @@ -import './main.js'; +import './minimongo_common.js'; import { isNumericKey, isOperatorObject, diff --git a/packages/minimongo/observe_handle.js b/packages/minimongo/observe_handle.js index ae40fc0594..3dc0fe37b5 100644 --- a/packages/minimongo/observe_handle.js +++ b/packages/minimongo/observe_handle.js @@ -1,2 +1,2 @@ // ObserveHandle: the return value of a live query. -export class ObserveHandle {} +export default class ObserveHandle {} diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 4b00459aae..a075652a51 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -22,8 +22,8 @@ Package.onUse(api => { 'tracker', ]); - api.mainModule('main.js', 'client'); - api.mainModule('main_server.js', 'server'); + api.mainModule('minimongo_client.js', 'client'); + api.mainModule('minimongo_server.js', 'server'); }); Package.onTest(api => { diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index 223f76ca09..713b2002e1 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -20,7 +20,7 @@ import { // first object comes first in order, 1 if the second object comes // first, or 0 if neither object comes before the other. -export class Sorter { +export default class Sorter { constructor(spec, options = {}) { this._sortSpecParts = []; this._sortFunction = null; From fe576f60ce019212699af53660d2afbcb3a721e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 18:38:47 +0200 Subject: [PATCH 14/28] Refactored common. --- packages/minimongo/common.js | 578 +++++++++++++++++++---------------- 1 file changed, 309 insertions(+), 269 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index fac197c7fc..509b0ba719 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -21,9 +21,9 @@ export const ELEMENT_OPERATORS = { compileElementSelector(operand) { if (!(Array.isArray(operand) && operand.length === 2 && typeof operand[0] === 'number' - && typeof operand[1] === 'number')) { + && typeof operand[1] === 'number')) throw Error('argument to $mod must be an array of two numbers'); - } + // XXX could require to be ints or round or something const divisor = operand[0]; const remainder = operand[1]; @@ -32,17 +32,23 @@ export const ELEMENT_OPERATORS = { }, $in: { compileElementSelector(operand) { - if (!Array.isArray(operand)) {throw Error('$in needs an array');} + if (!Array.isArray(operand)) + throw Error('$in needs an array'); - const elementMatchers = []; - operand.forEach(option => { - if (option instanceof RegExp) {elementMatchers.push(regexpElementMatcher(option));} else if (isOperatorObject(option)) {throw Error('cannot nest $ under $in');} else {elementMatchers.push(equalityElementMatcher(option));} + const elementMatchers = operand.map(option => { + if (option instanceof RegExp) + return regexpElementMatcher(option); + if (isOperatorObject(option)) + throw Error('cannot nest $ under $in'); + return equalityElementMatcher(option); }); return value => { // Allow {a: {$in: [null]}} to match when 'a' does not exist. - if (value === undefined) {value = null;} - return elementMatchers.some(e => e(value)); + if (value === undefined) + value = null; + + return elementMatchers.some(matcher => matcher(value)); }; }, }, @@ -56,9 +62,9 @@ export const ELEMENT_OPERATORS = { // Don't ask me why, but by experimentation, this seems to be what Mongo // does. operand = 0; - } else if (typeof operand !== 'number') { + } else if (typeof operand !== 'number') throw Error('$size needs a number'); - } + return value => Array.isArray(value) && value.length === operand; }, }, @@ -69,50 +75,51 @@ export const ELEMENT_OPERATORS = { // should *not* include it itself. dontIncludeLeafArrays: true, compileElementSelector(operand) { - if (typeof operand !== 'number') {throw Error('$type needs a number');} - return value => value !== undefined - && LocalCollection._f._type(value) === operand; + if (typeof operand !== 'number') + throw Error('$type needs a number'); + return value => value !== undefined && LocalCollection._f._type(value) === operand; }, }, $bitsAllSet: { compileElementSelector(operand) { - const op = getOperandBitmask(operand, '$bitsAllSet'); + const mask = getOperandBitmask(operand, '$bitsAllSet'); return value => { - const bitmask = getValueBitmask(value, op.length); - return bitmask && op.every((byte, idx) => (bitmask[idx] & byte) == byte); + const bitmask = getValueBitmask(value, mask.length); + return bitmask && mask.every((byte, i) => (bitmask[i] & byte) === byte); }; }, }, $bitsAnySet: { compileElementSelector(operand) { - const query = getOperandBitmask(operand, '$bitsAnySet'); + const mask = getOperandBitmask(operand, '$bitsAnySet'); return value => { - const bitmask = getValueBitmask(value, query.length); - return bitmask && query.some((byte, idx) => (~bitmask[idx] & byte) !== byte); + const bitmask = getValueBitmask(value, mask.length); + return bitmask && mask.some((byte, i) => (~bitmask[i] & byte) !== byte); }; }, }, $bitsAllClear: { compileElementSelector(operand) { - const query = getOperandBitmask(operand, '$bitsAllClear'); + const mask = getOperandBitmask(operand, '$bitsAllClear'); return value => { - const bitmask = getValueBitmask(value, query.length); - return bitmask && query.every((byte, idx) => !(bitmask[idx] & byte)); + const bitmask = getValueBitmask(value, mask.length); + return bitmask && mask.every((byte, i) => !(bitmask[i] & byte)); }; }, }, $bitsAnyClear: { compileElementSelector(operand) { - const query = getOperandBitmask(operand, '$bitsAnyClear'); + const mask = getOperandBitmask(operand, '$bitsAnyClear'); return value => { - const bitmask = getValueBitmask(value, query.length); - return bitmask && query.some((byte, idx) => (bitmask[idx] & byte) !== byte); + const bitmask = getValueBitmask(value, mask.length); + return bitmask && mask.some((byte, i) => (bitmask[i] & byte) !== byte); }; }, }, $regex: { compileElementSelector(operand, valueSelector) { - if (!(typeof operand === 'string' || operand instanceof RegExp)) {throw Error('$regex has to be a string or RegExp');} + if (!(typeof operand === 'string' || operand instanceof RegExp)) + throw Error('$regex has to be a string or RegExp'); let regexp; if (valueSelector.$options !== undefined) { @@ -123,7 +130,8 @@ export const ELEMENT_OPERATORS = { // Be clear that we only support the JS-supported options, not extended // ones (eg, Mongo supports x and s). Ideally we would implement x and s // by transforming the regexp, but not today... - if (/[^gim]/.test(valueSelector.$options)) {throw new Error('Only the i, m, and g regexp options are supported');} + if (/[^gim]/.test(valueSelector.$options)) + throw new Error('Only the i, m, and g regexp options are supported'); const regexSource = operand instanceof RegExp ? operand.source : operand; regexp = new RegExp(regexSource, valueSelector.$options); @@ -132,32 +140,36 @@ export const ELEMENT_OPERATORS = { } else { regexp = new RegExp(operand); } + return regexpElementMatcher(regexp); }, }, $elemMatch: { dontExpandLeafArrays: true, compileElementSelector(operand, valueSelector, matcher) { - if (!LocalCollection._isPlainObject(operand)) {throw Error('$elemMatch need an object');} + if (!LocalCollection._isPlainObject(operand)) + throw Error('$elemMatch need an object'); - let subMatcher, isDocMatcher; - if (isOperatorObject(Object.keys(operand) - .filter(key => !Object.keys(LOGICAL_OPERATORS).includes(key)) - .reduce((a, b) => Object.assign(a, {[b]: operand[b]}), {}), true)) { - subMatcher = compileValueSelector(operand, matcher); - isDocMatcher = false; - } else { + const isDocMatcher = !isOperatorObject( + Object.keys(operand) + .filter(key => !LOGICAL_OPERATORS.hasOwnProperty(key)) + .reduce((a, b) => Object.assign(a, {[b]: operand[b]}), {}), + true); + + if (isDocMatcher) { // This is NOT the same as compileValueSelector(operand), and not just // because of the slightly different calling convention. // {$elemMatch: {x: 3}} means "an element has a field x:3", not // "consists only of a field x:3". Also, regexps and sub-$ are allowed. - subMatcher = compileDocumentSelector(operand, matcher, - {inElemMatch: true}); - isDocMatcher = true; + subMatcher = compileDocumentSelector(operand, matcher, {inElemMatch: true}); + } else { + subMatcher = compileValueSelector(operand, matcher); } return value => { - if (!Array.isArray(value)) {return false;} + if (!Array.isArray(value)) + return false; + for (let i = 0; i < value.length; ++i) { const arrayElement = value[i]; let arg; @@ -165,7 +177,8 @@ export const ELEMENT_OPERATORS = { // We can only match {$elemMatch: {b: 3}} against objects. // (We can also match against arrays, if there's numeric indices, // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) - if (!isIndexable(arrayElement)) {return false;} + if (!isIndexable(arrayElement)) + return false; arg = arrayElement; } else { // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches @@ -173,8 +186,10 @@ export const ELEMENT_OPERATORS = { arg = [{value: arrayElement, dontIterate: true}]; } // XXX support $near in $elemMatch by propagating $distance? - if (subMatcher(arg).result) {return i;} // specially understood to mean "use as arrayIndices" + if (subMatcher(arg).result) + return i; // specially understood to mean "use as arrayIndices" } + return false; }; }, @@ -184,21 +199,20 @@ export const ELEMENT_OPERATORS = { // Operators that appear at the top level of a document selector. const LOGICAL_OPERATORS = { $and(subSelector, matcher, inElemMatch) { - const matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); + const matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); return andDocumentMatchers(matchers); }, $or(subSelector, matcher, inElemMatch) { - const matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); + const matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); // Special case: if there is only one matcher, use it directly, *preserving* // any arrayIndices it returns. - if (matchers.length === 1) {return matchers[0];} + if (matchers.length === 1) + return matchers[0]; return doc => { - const result = matchers.some(f => f(doc).result); + const result = matchers.some(fn => fn(doc).result); // $or does NOT set arrayIndices when it has multiple // sub-expressions. (Tested against MongoDB.) return {result}; @@ -206,10 +220,9 @@ const LOGICAL_OPERATORS = { }, $nor(subSelector, matcher, inElemMatch) { - const matchers = compileArrayOfDocumentSelectors( - subSelector, matcher, inElemMatch); + const matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); return doc => { - const result = matchers.every(f => !f(doc).result); + const result = matchers.every(fn => !fn(doc).result); // Never set arrayIndices, because we only match if nothing in particular // 'matched' (and because this is consistent with MongoDB). return {result}; @@ -220,24 +233,22 @@ const LOGICAL_OPERATORS = { // Record that *any* path may be used. matcher._recordPathUsed(''); matcher._hasWhere = true; + if (!(selectorValue instanceof Function)) { // XXX MongoDB seems to have more complex logic to decide where or or not // to add 'return'; not sure exactly what it is. selectorValue = Function('obj', `return ${selectorValue}`); } - return doc => // We make the document available as both `this` and `obj`. - // XXX not sure what we should do if this throws - ({ - result: selectorValue.call(doc, doc), - }); + + // We make the document available as both `this` and `obj`. + // // XXX not sure what we should do if this throws + return doc => ({result: selectorValue.call(doc, doc)}); }, // This is just used as a comment in the query (in MongoDB, it also ends up in // query logs); it has no effect on the actual selection. $comment() { - return () => ({ - result: true, - }); + return () => ({result: true}); }, }; @@ -247,19 +258,16 @@ const LOGICAL_OPERATORS = { // convertElementMatcherToBranchedMatcher". const VALUE_OPERATORS = { $eq(operand) { - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(operand)); + return convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand)); }, $not(operand, valueSelector, matcher) { return invertBranchedMatcher(compileValueSelector(operand, matcher)); }, $ne(operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( - equalityElementMatcher(operand))); + return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand))); }, $nin(operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher( - ELEMENT_OPERATORS.$in.compileElementSelector(operand))); + return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(ELEMENT_OPERATORS.$in.compileElementSelector(operand))); }, $exists(operand) { const exists = convertElementMatcherToBranchedMatcher(value => value !== undefined); @@ -267,39 +275,47 @@ const VALUE_OPERATORS = { }, // $options just provides options for $regex; its logic is inside $regex $options(operand, valueSelector) { - if (!valueSelector.hasOwnProperty('$regex')) {throw Error('$options needs a $regex');} + if (!valueSelector.hasOwnProperty('$regex')) + throw Error('$options needs a $regex'); return everythingMatcher; }, // $maxDistance is basically an argument to $near $maxDistance(operand, valueSelector) { - if (!valueSelector.$near) {throw Error('$maxDistance needs a $near');} + if (!valueSelector.$near) + throw Error('$maxDistance needs a $near'); return everythingMatcher; }, $all(operand, valueSelector, matcher) { - if (!Array.isArray(operand)) {throw Error('$all requires array');} - // Not sure why, but this seems to be what MongoDB does. - if (operand.length === 0) {return nothingMatcher;} + if (!Array.isArray(operand)) + throw Error('$all requires array'); - const branchedMatchers = []; - operand.forEach(criterion => { + // Not sure why, but this seems to be what MongoDB does. + if (operand.length === 0) + return nothingMatcher; + + const branchedMatchers = operand.map(criterion => { // XXX handle $all/$elemMatch combination - if (isOperatorObject(criterion)) {throw Error('no $ expressions in $all');} + if (isOperatorObject(criterion)) + throw Error('no $ expressions in $all'); + // This is always a regexp or equality selector. - branchedMatchers.push(compileValueSelector(criterion, matcher)); + return compileValueSelector(criterion, matcher); }); + // andBranchedMatchers does NOT require all selectors to return true on the // SAME branch. return andBranchedMatchers(branchedMatchers); }, $near(operand, valueSelector, matcher, isRoot) { - if (!isRoot) {throw Error("$near can't be inside another $ operator");} + if (!isRoot) + throw Error("$near can't be inside another $ operator"); + matcher._hasGeoQuery = true; // There are two kinds of geodata in MongoDB: legacy coordinate pairs and // GeoJSON. They use different distance metrics, too. GeoJSON queries are // marked with a $geometry property, though legacy coordinates can be // matched using $geometry. - let maxDistance, point, distance; if (LocalCollection._isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { // GeoJSON "2dsphere" mode. @@ -309,23 +325,25 @@ const VALUE_OPERATORS = { // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get // a priority. - if (!value) {return null;} - if (!value.type) { - return GeoJSON.pointDistance(point, - { type: 'Point', coordinates: pointToArray(value) }); - } - if (value.type === 'Point') { + if (!value) + return null; + if (!value.type) + return GeoJSON.pointDistance(point, {type: 'Point', coordinates: pointToArray(value)}); + if (value.type === 'Point') return GeoJSON.pointDistance(point, value); - } - return GeoJSON.geometryWithinRadius(value, point, maxDistance) - ? 0 : maxDistance + 1; + return GeoJSON.geometryWithinRadius(value, point, maxDistance) ? 0 : maxDistance + 1; }; } else { maxDistance = valueSelector.$maxDistance; - if (!isIndexable(operand)) {throw Error('$near argument must be coordinate pair or GeoJSON');} + + if (!isIndexable(operand)) + throw Error('$near argument must be coordinate pair or GeoJSON'); + point = pointToArray(operand); + distance = value => { - if (!isIndexable(value)) {return null;} + if (!isIndexable(value)) + return null; return distanceCoordinatePairs(point, value); }; } @@ -339,27 +357,36 @@ const VALUE_OPERATORS = { // Note: This differs from MongoDB's implementation, where a document will // actually show up *multiple times* in the result set, with one entry for // each within-$maxDistance branching point. - branchedValues = expandArraysInBranches(branchedValues); const result = {result: false}; - branchedValues.every(branch => { + expandArraysInBranches(branchedValues).every(branch => { // if operation is an update, don't skip branches, just return the first one (#3599) let curDistance; if (!matcher._isUpdate) { - if (!(typeof branch.value === 'object')) { + if (!(typeof branch.value === 'object')) return true; - } + curDistance = distance(branch.value); + // Skip branches that aren't real points or are too far away. - if (curDistance === null || curDistance > maxDistance) {return true;} + if (curDistance === null || curDistance > maxDistance) + return true; + // Skip anything that's a tie. - if (result.distance !== undefined && result.distance <= curDistance) {return true;} + if (result.distance !== undefined && result.distance <= curDistance) + return true; } + result.result = true; result.distance = curDistance; - if (!branch.arrayIndices) {delete result.arrayIndices;} else {result.arrayIndices = branch.arrayIndices;} - if (matcher._isUpdate) {return false;} - return true; + + if (branch.arrayIndices) + result.arrayIndices = branch.arrayIndices; + else + delete result.arrayIndices; + + return !matcher._isUpdate; }); + return result; }; }, @@ -370,36 +397,40 @@ const VALUE_OPERATORS = { // but the argument is different: for the former it's a whole doc, whereas for // the latter it's an array of 'branched values'. function andSomeMatchers(subMatchers) { - if (subMatchers.length === 0) {return everythingMatcher;} - if (subMatchers.length === 1) {return subMatchers[0];} + if (subMatchers.length === 0) + return everythingMatcher; + + if (subMatchers.length === 1) + return subMatchers[0]; return docOrBranches => { - const ret = {}; - ret.result = subMatchers.every(f => { - const subResult = f(docOrBranches); + const match = {}; + match.result = subMatchers.every(fn => { + const subResult = fn(docOrBranches); + // Copy a 'distance' number out of the first sub-matcher that has // one. Yes, this means that if there are multiple $near fields in a // query, something arbitrary happens; this appears to be consistent with // Mongo. - if (subResult.result && subResult.distance !== undefined - && ret.distance === undefined) { - ret.distance = subResult.distance; - } + if (subResult.result && subResult.distance !== undefined && match.distance === undefined) + match.distance = subResult.distance; + // Similarly, propagate arrayIndices from sub-matchers... but to match // MongoDB behavior, this time the *last* sub-matcher with arrayIndices // wins. - if (subResult.result && subResult.arrayIndices) { - ret.arrayIndices = subResult.arrayIndices; - } + if (subResult.result && subResult.arrayIndices) + match.arrayIndices = subResult.arrayIndices; + return subResult.result; }); // If we didn't actually match, forget any extra metadata we came up with. - if (!ret.result) { - delete ret.distance; - delete ret.arrayIndices; + if (!match.result) { + delete match.distance; + delete match.arrayIndices; } - return ret; + + return match; }; } @@ -407,11 +438,14 @@ const andDocumentMatchers = andSomeMatchers; const andBranchedMatchers = andSomeMatchers; function compileArrayOfDocumentSelectors(selectors, matcher, inElemMatch) { - if (!Array.isArray(selectors) || selectors.length === 0) {throw Error('$and/$or/$nor must be nonempty array');} + if (!Array.isArray(selectors) || selectors.length === 0) + throw Error('$and/$or/$nor must be nonempty array'); + return selectors.map(subSelector => { - if (!LocalCollection._isPlainObject(subSelector)) {throw Error('$or/$and/$nor entries need to be full objects');} - return compileDocumentSelector( - subSelector, matcher, {inElemMatch}); + if (!LocalCollection._isPlainObject(subSelector)) + throw Error('$or/$and/$nor entries need to be full objects'); + + return compileDocumentSelector(subSelector, matcher, {inElemMatch}); }); } @@ -423,29 +457,29 @@ function compileArrayOfDocumentSelectors(selectors, matcher, inElemMatch) { // If this is the root document selector (ie, not wrapped in $and or the like), // then isRoot is true. (This is used by $near.) export function compileDocumentSelector(docSelector, matcher, options = {}) { - let docMatchers = []; - Object.keys(docSelector).forEach(key => { - let subSelector = docSelector[key]; + const docMatchers = Object.keys(docSelector).map(key => { + const subSelector = docSelector[key]; + if (key.substr(0, 1) === '$') { // Outer operators are either logical operators (they recurse back into // this function), or $where. - if (!LOGICAL_OPERATORS.hasOwnProperty(key)) {throw new Error(`Unrecognized logical operator: ${key}`);} + if (!LOGICAL_OPERATORS.hasOwnProperty(key)) + throw new Error(`Unrecognized logical operator: ${key}`); + matcher._isSimple = false; - docMatchers.push(LOGICAL_OPERATORS[key](subSelector, matcher, - options.inElemMatch)); - } else { - // Record this path, but only if we aren't in an elemMatcher, since in an - // elemMatch this is a path inside an object in an array, not in the doc - // root. - if (!options.inElemMatch) {matcher._recordPathUsed(key);} - let lookUpByIndex = makeLookupFunction(key); - let valueMatcher = - compileValueSelector(subSelector, matcher, options.isRoot); - docMatchers.push(doc => { - let branchValues = lookUpByIndex(doc); - return valueMatcher(branchValues); - }); + return LOGICAL_OPERATORS[key](subSelector, matcher, options.inElemMatch); } + + // Record this path, but only if we aren't in an elemMatcher, since in an + // elemMatch this is a path inside an object in an array, not in the doc + // root. + if (!options.inElemMatch) + matcher._recordPathUsed(key); + + const lookUpByIndex = makeLookupFunction(key); + const valueMatcher = compileValueSelector(subSelector, matcher, options.isRoot); + + return doc => valueMatcher(lookUpByIndex(doc)); }); return andDocumentMatchers(docMatchers); @@ -458,13 +492,12 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) { function compileValueSelector(valueSelector, matcher, isRoot) { if (valueSelector instanceof RegExp) { matcher._isSimple = false; - return convertElementMatcherToBranchedMatcher( - regexpElementMatcher(valueSelector)); - } else if (isOperatorObject(valueSelector)) { - return operatorBranchedMatcher(valueSelector, matcher, isRoot); + return convertElementMatcherToBranchedMatcher(regexpElementMatcher(valueSelector)); } - return convertElementMatcherToBranchedMatcher( - equalityElementMatcher(valueSelector)); + if (isOperatorObject(valueSelector)) + return operatorBranchedMatcher(valueSelector, matcher, isRoot); + + return convertElementMatcherToBranchedMatcher(equalityElementMatcher(valueSelector)); } // Given an element matcher (which evaluates a single value), returns a branched @@ -472,13 +505,12 @@ function compileValueSelector(valueSelector, matcher, isRoot) { // more structured return value possibly including arrayIndices). function convertElementMatcherToBranchedMatcher(elementMatcher, options = {}) { return branches => { - let expanded = branches; - if (!options.dontExpandLeafArrays) { - expanded = expandArraysInBranches( - branches, options.dontIncludeLeafArrays); - } - const ret = {}; - ret.result = expanded.some(element => { + const expanded = options.dontExpandLeafArrays + ? branches + : expandArraysInBranches(branches, options.dontIncludeLeafArrays); + + const match = {}; + match.result = expanded.some(element => { let matched = elementMatcher(element.value); // Special case for $elemMatch: it means "true, and use this as an array @@ -487,41 +519,44 @@ function convertElementMatcherToBranchedMatcher(elementMatcher, options = {}) { // XXX This code dates from when we only stored a single array index // (for the outermost array). Should we be also including deeper array // indices from the $elemMatch match? - if (!element.arrayIndices) {element.arrayIndices = [matched];} + if (!element.arrayIndices) + element.arrayIndices = [matched]; + matched = true; } // If some element matched, and it's tagged with array indices, include // those indices in our result object. - if (matched && element.arrayIndices) {ret.arrayIndices = element.arrayIndices;} + if (matched && element.arrayIndices) + match.arrayIndices = element.arrayIndices; return matched; }); - return ret; + + return match; }; } // Helpers for $near. function distanceCoordinatePairs(a, b) { - a = pointToArray(a); - b = pointToArray(b); - const x = a[0] - b[0]; - const y = a[1] - b[1]; - if (Number.isNaN(x) || Number.isNaN(y)) {return null;} - return Math.sqrt(x * x + y * y); + const pointA = pointToArray(a); + const pointB = pointToArray(b); + + return Math.hypot(pointA[0] - pointB[0], pointA[1] - pointB[1]); } // Takes something that is not an operator object and returns an element matcher // for equality with that thing. export function equalityElementMatcher(elementSelector) { - if (isOperatorObject(elementSelector)) {throw Error("Can't create equalityValueSelector for operator object");} + if (isOperatorObject(elementSelector)) + throw Error("Can't create equalityValueSelector for operator object"); // Special-case: null and undefined are equal (if you got undefined in there // somewhere, or if you got it due to some branch being non-existent in the // weird special case), even though they aren't with EJSON.equals. - if (elementSelector == null) { // undefined or null - return value => // undefined or null - value == null; + // undefined or null + if (elementSelector == null) { + return value => value == null; } return value => LocalCollection._f._equal(elementSelector, value); @@ -533,27 +568,24 @@ function everythingMatcher(docOrBranchedValues) { export function expandArraysInBranches(branches, skipTheArrays) { const branchesOut = []; + branches.forEach(branch => { const thisIsArray = Array.isArray(branch.value); + // We include the branch itself, *UNLESS* we it's an array that we're going // to iterate and we're told to skip arrays. (That's right, we include some // arrays even skipTheArrays is true: these are arrays that were found via // explicit numerical indices.) - if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { - branchesOut.push({ - value: branch.value, - arrayIndices: branch.arrayIndices, - }); - } + if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) + branchesOut.push({arrayIndices: branch.arrayIndices, value: branch.value}); + if (thisIsArray && !branch.dontIterate) { - branch.value.forEach((leaf, i) => { - branchesOut.push({ - value: leaf, - arrayIndices: (branch.arrayIndices || []).concat(i), - }); + branch.value.forEach((value, i) => { + branchesOut.push({arrayIndices: (branch.arrayIndices || []).concat(i), value}); }); } }); + return branchesOut; } @@ -562,26 +594,28 @@ function getOperandBitmask(operand, selector) { // numeric bitmask // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. // Otherwise, $bitsAllSet will return an error. - if (Number.isInteger(operand) && operand >= 0) { + if (Number.isInteger(operand) && operand >= 0) return new Uint8Array(new Int32Array([operand]).buffer); - } + // bindata bitmask // You can also use an arbitrarily large BinData instance as a bitmask. - else if (EJSON.isBinary(operand)) { + if (EJSON.isBinary(operand)) return new Uint8Array(operand.buffer); - } + // position list // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. - else if (Array.isArray(operand) && operand.every(e => Number.isInteger(e) && e >= 0)) { + if (Array.isArray(operand) && operand.every(x => Number.isInteger(x) && x >= 0)) { const buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1); const view = new Uint8Array(buffer); + operand.forEach(x => { view[x >> 3] |= 1 << (x & 0x7); }); + return view; } - // bad operand + // bad operand throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`); } @@ -592,22 +626,26 @@ function getValueBitmask(value, length) { // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. const buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); + let view = new Uint32Array(buffer, 0, 2); view[0] = value % ((1 << 16) * (1 << 16)) | 0; view[1] = value / ((1 << 16) * (1 << 16)) | 0; + // sign extension if (value < 0) { view = new Uint8Array(buffer, 2); - view.forEach((byte, idx) => { - view[idx] = 0xff; + view.forEach((byte, i) => { + view[i] = 0xff; }); } + return new Uint8Array(buffer); } + // bindata - else if (EJSON.isBinary(value)) { + if (EJSON.isBinary(value)) return new Uint8Array(value.buffer); - } + // no match return false; } @@ -617,11 +655,10 @@ function getValueBitmask(value, length) { // means that ALL branch values need to fail to match innerBranchedMatcher. function invertBranchedMatcher(branchedMatcher) { return branchValues => { - const invertMe = branchedMatcher(branchValues); // We explicitly choose to strip arrayIndices here: it doesn't make sense to // say "update the array element that does not match something", at least // in mongo-land. - return {result: !invertMe.result}; + return {result: !branchedMatcher(branchValues).result}; }; } @@ -637,19 +674,24 @@ export function isNumericKey(s) { // with $. Unless inconsistentOK is set, throws if some keys begin with $ and // others don't. export function isOperatorObject(valueSelector, inconsistentOK) { - if (!LocalCollection._isPlainObject(valueSelector)) {return false;} + if (!LocalCollection._isPlainObject(valueSelector)) + return false; let theseAreOperators = undefined; Object.keys(valueSelector).forEach(selKey => { const thisIsOperator = selKey.substr(0, 1) === '$'; + if (theseAreOperators === undefined) { theseAreOperators = thisIsOperator; } else if (theseAreOperators !== thisIsOperator) { - if (!inconsistentOK) {throw new Error(`Inconsistent operator: ${JSON.stringify(valueSelector)}`);} + if (!inconsistentOK) + throw new Error(`Inconsistent operator: ${JSON.stringify(valueSelector)}`); + theseAreOperators = false; } }); - return !!theseAreOperators; // {} has no operators + + return !!theseAreOperators; // {} has no operators } // Helper for $lt/$gt/$lte/$gte. @@ -660,21 +702,25 @@ function makeInequality(cmpValueComparator) { // XXX This was behavior we observed in pre-release MongoDB 2.5, but // it seems to have been reverted. // See https://jira.mongodb.org/browse/SERVER-11444 - if (Array.isArray(operand)) { + if (Array.isArray(operand)) return () => false; - } // Special case: consider undefined and null the same (so true with // $gte/$lte). - if (operand === undefined) {operand = null;} + if (operand === undefined) + operand = null; const operandType = LocalCollection._f._type(operand); return value => { - if (value === undefined) {value = null;} + if (value === undefined) + value = null; + // Comparisons are never true among things of different type (except // null vs undefined). - if (LocalCollection._f._type(value) !== operandType) {return false;} + if (LocalCollection._f._type(value) !== operandType) + return false; + return cmpValueComparator(LocalCollection._f._cmp(value, operand)); }; }, @@ -736,29 +782,27 @@ function makeInequality(cmpValueComparator) { export function makeLookupFunction(key, options = {}) { const parts = key.split('.'); const firstPart = parts.length ? parts[0] : ''; - const firstPartIsNumeric = isNumericKey(firstPart); - const nextPartIsNumeric = parts.length >= 2 && isNumericKey(parts[1]); - let lookupRest; - if (parts.length > 1) { - lookupRest = makeLookupFunction(parts.slice(1).join('.')); - } + const lookupRest = parts.length > 1 && makeLookupFunction(parts.slice(1).join('.')); - const omitUnnecessaryFields = retVal => { - if (!retVal.dontIterate) {delete retVal.dontIterate;} - if (retVal.arrayIndices && !retVal.arrayIndices.length) {delete retVal.arrayIndices;} - return retVal; + const omitUnnecessaryFields = result => { + if (!result.dontIterate) + delete result.dontIterate; + + if (result.arrayIndices && !result.arrayIndices.length) + delete result.arrayIndices; + + return result; }; // Doc will always be a plain object or an array. // apply an explicit numeric index, an array. - return (doc, arrayIndices) => { - if (!arrayIndices) {arrayIndices = [];} - + return (doc, arrayIndices = []) => { if (Array.isArray(doc)) { // If we're being asked to do an invalid lookup into an array (non-integer // or out-of-bounds), return no results (which is different from returning // a single undefined result, in that `null` equality checks won't match). - if (!(firstPartIsNumeric && firstPart < doc.length)) {return [];} + if (!(isNumericKey(firstPart) && firstPart < doc.length)) + return []; // Remember that we used this array index. Include an 'x' to indicate that // the previous index came from being considered as an explicit array @@ -783,9 +827,10 @@ export function makeLookupFunction(key, options = {}) { // So in that case, we mark the return value as 'don't iterate'. if (!lookupRest) { return [omitUnnecessaryFields({ - value: firstLevel, + arrayIndices, dontIterate: Array.isArray(doc) && Array.isArray(firstLevel), - arrayIndices})]; + value: firstLevel + })]; } // We need to dig deeper. But if we can't, because what we've found is not @@ -795,9 +840,10 @@ export function makeLookupFunction(key, options = {}) { // return a single `undefined` (which can, for example, match via equality // with `null`). if (!isIndexable(firstLevel)) { - if (Array.isArray(doc)) {return [];} - return [omitUnnecessaryFields({value: undefined, - arrayIndices})]; + if (Array.isArray(doc)) + return []; + + return [omitUnnecessaryFields({arrayIndices, value: undefined})]; } const result = []; @@ -825,13 +871,10 @@ export function makeLookupFunction(key, options = {}) { // selector), we skip the branching: we ONLY allow the numeric part to mean // 'look up this index' in that case, not 'also look up this index in all // the elements of the array'. - if (Array.isArray(firstLevel) && !(nextPartIsNumeric && options.forSort)) { + if (Array.isArray(firstLevel) && !(isNumericKey(parts[1]) && options.forSort)) { firstLevel.forEach((branch, arrayIndex) => { - if (LocalCollection._isPlainObject(branch)) { - appendToResult(lookupRest( - branch, - arrayIndices.concat(arrayIndex))); - } + if (LocalCollection._isPlainObject(branch)) + appendToResult(lookupRest(branch, arrayIndices.concat(arrayIndex))); }); } @@ -847,9 +890,9 @@ MinimongoError = (message, options = {}) => { message += ` for field '${options.field}'`; } - const e = new Error(message); - e.name = 'MinimongoError'; - return e; + const error = new Error(message); + error.name = 'MinimongoError'; + return error; }; export function nothingMatcher(docOrBranchedValues) { @@ -862,33 +905,25 @@ function operatorBranchedMatcher(valueSelector, matcher, isRoot) { // Each valueSelector works separately on the various branches. So one // operator can match one branch and another can match another branch. This // is OK. - - const operatorMatchers = []; - Object.keys(valueSelector).forEach(operator => { + const operatorMatchers = Object.keys(valueSelector).map(operator => { const operand = valueSelector[operator]; - const simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && - typeof operand === 'number'; + + const simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && typeof operand === 'number'; const simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); - const simpleInclusion = ['$in', '$nin'].includes(operator) && - Array.isArray(operand) && !operand.some(x => x === Object(x)); + const simpleInclusion = ['$in', '$nin'].includes(operator) && Array.isArray(operand) && !operand.some(x => x === Object(x)); - if (! (simpleRange || simpleInclusion || simpleEquality)) { + if (!(simpleRange || simpleInclusion || simpleEquality)) matcher._isSimple = false; + + if (VALUE_OPERATORS.hasOwnProperty(operator)) + return VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot); + + if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { + const options = ELEMENT_OPERATORS[operator]; + return convertElementMatcherToBranchedMatcher(options.compileElementSelector(operand, valueSelector, matcher), options); } - if (VALUE_OPERATORS.hasOwnProperty(operator)) { - operatorMatchers.push( - VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot)); - } else if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { - const options = ELEMENT_OPERATORS[operator]; - operatorMatchers.push( - convertElementMatcherToBranchedMatcher( - options.compileElementSelector( - operand, valueSelector, matcher), - options)); - } else { - throw new Error(`Unrecognized operator: ${operator}`); - } + throw new Error(`Unrecognized operator: ${operator}`); }); return andBranchedMatchers(operatorMatchers); @@ -903,32 +938,38 @@ function operatorBranchedMatcher(valueSelector, matcher, isRoot) { // conflict resolution. // initial tree - Optional Object: starting tree. // @returns - Object: tree represented as a set of nested objects -export function pathsToTree(paths, newLeafFn, conflictFn, tree = {}) { - paths.forEach(keyPath => { - let treePos = tree; - const pathArr = keyPath.split('.'); +export function pathsToTree(paths, newLeafFn, conflictFn, root = {}) { + paths.forEach(path => { + const pathArray = path.split('.'); + let tree = root; // use .every just for iteration with break - const success = pathArr.slice(0, -1).every((key, idx) => { - if (!treePos.hasOwnProperty(key)) {treePos[key] = {};} else if (treePos[key] !== Object(treePos[key])) { - treePos[key] = conflictFn(treePos[key], - pathArr.slice(0, idx + 1).join('.'), - keyPath); + const success = pathArray.slice(0, -1).every((key, i) => { + if (!tree.hasOwnProperty(key)) { + tree[key] = {}; + } else if (tree[key] !== Object(tree[key])) { + tree[key] = conflictFn(tree[key], pathArray.slice(0, i + 1).join('.'), path); + // break out of loop if we are failing for this path - if (treePos[key] !== Object(treePos[key])) {return false;} + if (tree[key] !== Object(tree[key])) + return false; } - treePos = treePos[key]; + tree = tree[key]; + return true; }); if (success) { - const lastKey = pathArr[pathArr.length - 1]; - if (!treePos.hasOwnProperty(lastKey)) {treePos[lastKey] = newLeafFn(keyPath);} else {treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath);} + const lastKey = pathArray[pathArray.length - 1]; + if (tree.hasOwnProperty(lastKey)) + tree[lastKey] = conflictFn(tree[lastKey], path, path); + else + tree[lastKey] = newLeafFn(path); } }); - return tree; + return root; } // Makes sure we get 2 elements array and assume the first one to be x and @@ -956,20 +997,22 @@ export function projectionDetails(fields) { // inclusive and exclusive fields. If _id is not the only field in the // projection and is exclusive, remove it so it can be handled later by a // special case, since exclusive _id is always allowed. - if (fieldsKeys.length > 0 && - !(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && - !(fieldsKeys.includes('_id') && fields._id)) {fieldsKeys = fieldsKeys.filter(key => key !== '_id');} + if (!(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && + !(fieldsKeys.includes('_id') && fields._id)) + fieldsKeys = fieldsKeys.filter(key => key !== '_id'); let including = null; // Unknown fieldsKeys.forEach(keyPath => { const rule = !!fields[keyPath]; - if (including === null) {including = rule;} - if (including !== rule) - // This error message is copied from MongoDB shell - {throw MinimongoError('You cannot currently mix including and excluding fields.');} - }); + if (including === null) + including = rule; + + // This error message is copied from MongoDB shell + if (including !== rule) + throw MinimongoError('You cannot currently mix including and excluding fields.'); + }); const projectionRulesTree = pathsToTree( fieldsKeys, @@ -991,26 +1034,23 @@ export function projectionDetails(fields) { // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } // // Note, how second time the return set of keys is different. - const currentPath = fullPath; const anotherPath = path; throw MinimongoError(`both ${currentPath} and ${anotherPath} found in fields option, using both of them may trigger unexpected behavior. Did you mean to use only one of them?`); }); - return { - tree: projectionRulesTree, - including, - }; + return {including, tree: projectionRulesTree}; } // Takes a RegExp object and returns an element matcher. export function regexpElementMatcher(regexp) { return value => { - if (value instanceof RegExp) { + if (value instanceof RegExp) return value.toString() === regexp.toString(); - } + // Regexps only work against strings. - if (typeof value !== 'string') {return false;} + if (typeof value !== 'string') + return false; // Reset regexp's state to avoid inconsistent matching for objects with the // same value on consecutive calls of regexp.test. This happens only if the From a686b93e06034f6436d28c11af7f4efc3c8d64a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 18:39:01 +0200 Subject: [PATCH 15/28] Refactored Cursor. --- packages/minimongo/cursor.js | 163 ++++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 69 deletions(-) diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index 16753aa6c2..b649818926 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -17,13 +17,12 @@ export default class Cursor { this._selectorId = selector._id; } else { this._selectorId = undefined; - if (this.matcher.hasGeoQuery() || options.sort) { - this.sorter = new Minimongo.Sorter(options.sort || [], - { matcher: this.matcher }); - } + + if (this.matcher.hasGeoQuery() || options.sort) + this.sorter = new Minimongo.Sorter(options.sort || [], {matcher: this.matcher}); } - this.skip = options.skip; + this.skip = options.skip || 0; this.limit = options.limit; this.fields = options.fields; @@ -32,7 +31,8 @@ export default class Cursor { this._transform = LocalCollection.wrapTransform(options.transform); // by default, queries register w/ Tracker when it is available. - if (typeof Tracker !== 'undefined') {this.reactive = options.reactive === undefined ? true : options.reactive;} + if (typeof Tracker !== 'undefined') + this.reactive = options.reactive === undefined ? true : options.reactive; } /** @@ -44,10 +44,8 @@ export default class Cursor { * @returns {Number} */ count() { - if (this.reactive) { - this._depend({added: true, removed: true}, - true /* allow the observe to be unordered */); - } + if (this.reactive) + this._depend({added: true, removed: true}, true /* allow the observe to be unordered */); return this._getRawObjects({ordered: true}).length; } @@ -61,11 +59,13 @@ export default class Cursor { * @returns {Object[]} */ fetch() { - const res = []; + const result = []; + this.forEach(doc => { - res.push(doc); + result.push(doc); }); - return res; + + return result; } /** @@ -93,12 +93,14 @@ export default class Cursor { movedBefore: true}); } - objects.forEach((elt, i) => { + objects.forEach((element, i) => { // This doubles as a clone operation. - elt = this._projectionFn(elt); + element = this._projectionFn(element); - if (this._transform) {elt = this._transform(elt);} - callback.call(thisArg, elt, i, this); + if (this._transform) + element = this._transform(element); + + callback.call(thisArg, element, i, this); }); } @@ -116,11 +118,13 @@ export default class Cursor { * @param {Any} [thisArg] An object which will be the value of `this` inside `callback`. */ map(callback, thisArg) { - const res = []; - this.forEach((doc, index) => { - res.push(callback.call(thisArg, doc, index, this)); + const result = []; + + this.forEach((doc, i) => { + result.push(callback.call(thisArg, doc, i, this)); }); - return res; + + return result; } // options to contain: @@ -169,21 +173,23 @@ export default class Cursor { // unordered observe. eg, update's EJSON.clone, and the "there are several" // comment in _modifyAndNotify // XXX allow skip/limit with unordered observe - if (!options._allow_unordered && !ordered && (this.skip || this.limit)) {throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit");} + if (!options._allow_unordered && !ordered && (this.skip || this.limit)) + throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); - if (this.fields && (this.fields._id === 0 || this.fields._id === false)) {throw Error('You may not observe a cursor with {fields: {_id: 0}}');} + if (this.fields && (this.fields._id === 0 || this.fields._id === false)) + throw Error('You may not observe a cursor with {fields: {_id: 0}}'); const query = { dirty: false, matcher: this.matcher, // not fast pathed sorter: ordered && this.sorter, - distances: - this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap, + distances: this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap, resultsSnapshot: null, ordered, cursor: this, projectionFn: this._projectionFn, }; + let qid; // Non-reactive queries call added[Before] and then never call anything @@ -192,9 +198,11 @@ export default class Cursor { qid = this.collection.next_qid++; this.collection.queries[qid] = query; } - query.results = this._getRawObjects({ - ordered, distances: query.distances}); - if (this.collection.paused) {query.resultsSnapshot = ordered ? [] : new LocalCollection._IdMap;} + + query.results = this._getRawObjects({ordered, distances: query.distances}); + + if (this.collection.paused) + query.resultsSnapshot = ordered ? [] : new LocalCollection._IdMap; // wrap callbacks we were passed. callbacks only fire when not paused and // are never undefined @@ -203,45 +211,54 @@ export default class Cursor { // furthermore, callbacks enqueue until the operation we're working on is // done. - const wrapCallback = f => { - if (!f) {return () => {};} + const wrapCallback = fn => { + if (!fn) + return () => {}; + const self = this; return function(/* args*/) { const args = arguments; - if (self.collection.paused) {return;} + if (self.collection.paused) + return; self.collection._observeQueue.queueTask(() => { - f.apply(this, args); + fn.apply(this, args); }); }; }; + query.added = wrapCallback(options.added); query.changed = wrapCallback(options.changed); query.removed = wrapCallback(options.removed); + if (ordered) { query.addedBefore = wrapCallback(options.addedBefore); query.movedBefore = wrapCallback(options.movedBefore); } if (!options._suppress_initial && !this.collection.paused) { - const results = query.results._map || query.results; + const results = ordered ? query.results : query.results._map; + Object.keys(results).forEach(key => { const doc = results[key]; const fields = EJSON.clone(doc); delete fields._id; - if (ordered) {query.addedBefore(doc._id, this._projectionFn(fields), null);} + + if (ordered) + query.addedBefore(doc._id, this._projectionFn(fields), null); + query.added(doc._id, this._projectionFn(fields)); }); } - const handle = new LocalCollection.ObserveHandle; - Object.assign(handle, { + const handle = Object.assign(new LocalCollection.ObserveHandle, { collection: this.collection, stop: () => { - if (this.reactive) {delete this.collection.queries[qid];} - }, + if (this.reactive) + delete this.collection.queries[qid]; + } }); if (this.reactive && Tracker.active) { @@ -254,6 +271,7 @@ export default class Cursor { handle.stop(); }); } + // run the observe callbacks resulting from the initial contents // before we leave the observe. this.collection._observeQueue.drain(); @@ -271,16 +289,16 @@ export default class Cursor { // anything changed. _depend(changers, _allow_unordered) { if (Tracker.active) { - const v = new Tracker.Dependency; - v.depend(); - const notifyChange = v.changed.bind(v); + const dependency = new Tracker.Dependency; + const notify = dependency.changed.bind(dependency); - const options = { - _suppress_initial: true, - _allow_unordered, - }; - ['added', 'changed', 'removed', 'addedBefore', 'movedBefore'].forEach(fnName => { - if (changers[fnName]) {options[fnName] = notifyChange;} + dependency.depend(); + + const options = {_allow_unordered, _suppress_initial: true}; + + ['added', 'addedBefore', 'changed', 'movedBefore', 'removed'].forEach(fn => { + if (changers[fn]) + options[fn] = notify; }); // observeChanges will stop() when this computation is invalidated @@ -317,12 +335,18 @@ export default class Cursor { // If you have non-zero skip and ask for a single id, you get // nothing. This is so it matches the behavior of the '{_id: foo}' // path. - if (this.skip) {return results;} + if (this.skip) + return results; const selectedDoc = this.collection._docs.get(this._selectorId); + if (selectedDoc) { - if (options.ordered) {results.push(selectedDoc);} else {results.set(this._selectorId, selectedDoc);} + if (options.ordered) + results.push(selectedDoc); + else + results.set(this._selectorId, selectedDoc); } + return results; } @@ -343,42 +367,43 @@ export default class Cursor { this.collection._docs.forEach((doc, id) => { const matchResult = this.matcher.documentMatches(doc); + if (matchResult.result) { if (options.ordered) { results.push(doc); - if (distances && matchResult.distance !== undefined) {distances.set(id, matchResult.distance);} + + if (distances && matchResult.distance !== undefined) + distances.set(id, matchResult.distance); } else { results.set(id, doc); } } + // Fast path for limited unsorted queries. // XXX 'length' check here seems wrong for ordered - if (this.limit && !this.skip && !this.sorter && - results.length === this.limit) {return false;} // break - return true; // continue + return !this.limit || this.skip || this.sorter || results.length !== this.limit; }); - if (!options.ordered) {return results;} + if (!options.ordered) + return results; - if (this.sorter) { - const comparator = this.sorter.getComparator({distances}); - results.sort(comparator); - } + if (this.sorter) + results.sort(this.sorter.getComparator({distances})); - const idx_start = this.skip || 0; - const idx_end = this.limit ? this.limit + idx_start : results.length; - return results.slice(idx_start, idx_end); + if (!this.limit && !this.skip) + return results; + + return results.slice(this.skip, this.limit ? this.limit + this.skip : results.length); } - _publishCursor(sub) { - if (! this.collection.name) {throw new Error("Can't publish a cursor from a collection without a name.");} - const collection = this.collection.name; - + _publishCursor(subscription) { // XXX minimongo should not depend on mongo-livedata! - if (! Package.mongo) { - throw new Error("Can't publish from Minimongo without the `mongo` package."); - } + if (!Package.mongo) + throw new Error('Can\'t publish from Minimongo without the `mongo` package.'); - return Package.mongo.Mongo.Collection._publishCursor(this, sub, collection); + if (!this.collection.name) + throw new Error('Can\'t publish a cursor from a collection without a name.'); + + return Package.mongo.Mongo.Collection._publishCursor(this, subscription, this.collection.name); } } From 2c5094fa0cdd7b7ae649a2d843cb73888eff0c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 19:46:10 +0200 Subject: [PATCH 16/28] Refactored LocalCollection. --- packages/minimongo/local_collection.js | 1090 ++++++++++++++---------- 1 file changed, 646 insertions(+), 444 deletions(-) diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 6b4ae6a2f1..95634e011e 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -59,13 +59,15 @@ export default class LocalCollection { // default syntax for everything is to omit the selector argument. // but if selector is explicitly passed in as false or undefined, we // want a selector that matches nothing. - if (arguments.length === 0) {selector = {};} + if (arguments.length === 0) + selector = {}; return new LocalCollection.Cursor(this, selector, options); } - findOne(selector, options) { - if (arguments.length === 0) {selector = {};} + findOne(selector, options = {}) { + if (arguments.length === 0) + selector = {}; // NOTE: by setting limit 1 here, we end up using very inefficient // code that recomputes the whole query on each update. The upside is @@ -75,7 +77,6 @@ export default class LocalCollection { // this might not be a big deal. In most cases, invalidation causes // the called to re-query anyway, so this should be a net performance // improvement. - options = options || {}; options.limit = 1; return this.find(selector, options).fetch()[0]; @@ -88,34 +89,46 @@ export default class LocalCollection { assertHasValidFieldNames(doc); - if (!doc.hasOwnProperty('_id')) { - // if you really want to use ObjectIDs, set this global. - // Mongo.Collection specifies its own ids and does not use this code. - doc._id = LocalCollection._useOID ? new MongoID.ObjectID() - : Random.id(); - } + // if you really want to use ObjectIDs, set this global. + // Mongo.Collection specifies its own ids and does not use this code. + if (!doc.hasOwnProperty('_id')) + doc._id = LocalCollection._useOID ? new MongoID.ObjectID() : Random.id(); + const id = doc._id; - if (this._docs.has(id)) {throw MinimongoError(`Duplicate _id '${id}'`);} + if (this._docs.has(id)) + throw MinimongoError(`Duplicate _id '${id}'`); this._saveOriginal(id, undefined); this._docs.set(id, doc); const queriesToRecompute = []; + // trigger live queries that match for (let qid in this.queries) { const query = this.queries[qid]; - if (query.dirty) continue; + + if (query.dirty) + continue; + const matchResult = query.matcher.documentMatches(doc); + if (matchResult.result) { - if (query.distances && matchResult.distance !== undefined) {query.distances.set(id, matchResult.distance);} - if (query.cursor.skip || query.cursor.limit) {queriesToRecompute.push(qid);} else {LocalCollection._insertInResults(query, doc);} + if (query.distances && matchResult.distance !== undefined) + query.distances.set(id, matchResult.distance); + + if (query.cursor.skip || query.cursor.limit) + queriesToRecompute.push(qid); + else + LocalCollection._insertInResults(query, doc); } } queriesToRecompute.forEach(qid => { - if (this.queries[qid]) {this._recomputeResults(this.queries[qid]);} + if (this.queries[qid]) + this._recomputeResults(this.queries[qid]); }); + this._observeQueue.drain(); // Defer because the caller likely doesn't expect the callback to be run @@ -125,6 +138,7 @@ export default class LocalCollection { callback(null, id); }); } + return id; } @@ -132,7 +146,8 @@ export default class LocalCollection { // 'resumeObservers' is called. pauseObservers() { // No-op if already paused. - if (this.paused) {return;} + if (this.paused) + return; // Set the 'paused' flag such that new observer messages don't fire. this.paused = true; @@ -151,42 +166,56 @@ export default class LocalCollection { // everything directly. if (this.paused && !this._savedOriginals && EJSON.equals(selector, {})) { const result = this._docs.size(); + this._docs.clear(); - Object.keys(this.queries).forEach(qid => { + + for (let qid in this.queries) { const query = this.queries[qid]; - if (query.ordered) { + + if (query.ordered) query.results = []; - } else { + else query.results.clear(); - } - }); + } + if (callback) { Meteor.defer(() => { callback(null, result); }); } + return result; } const matcher = new Minimongo.Matcher(selector); const remove = []; + this._eachPossiblyMatchingDoc(selector, (doc, id) => { - if (matcher.documentMatches(doc).result) {remove.push(id);} + if (matcher.documentMatches(doc).result) + remove.push(id); }); const queriesToRecompute = []; const queryRemove = []; + for (let i = 0; i < remove.length; i++) { const removeId = remove[i]; const removeDoc = this._docs.get(removeId); - Object.keys(this.queries).forEach(qid => { + + for (let qid in this.queries) { const query = this.queries[qid]; - if (query.dirty) return; + + if (query.dirty) + return; if (query.matcher.documentMatches(removeDoc).result) { - if (query.cursor.skip || query.cursor.limit) {queriesToRecompute.push(qid);} else {queryRemove.push({qid, doc: removeDoc});} + if (query.cursor.skip || query.cursor.limit) + queriesToRecompute.push(qid); + else + queryRemove.push({qid, doc: removeDoc}); } - }); + } + this._saveOriginal(removeId, removeDoc); this._docs.remove(removeId); } @@ -194,22 +223,30 @@ export default class LocalCollection { // run live query callbacks _after_ we've removed the documents. queryRemove.forEach(remove => { const query = this.queries[remove.qid]; + if (query) { query.distances && query.distances.remove(remove.doc._id); LocalCollection._removeFromResults(query, remove.doc); } }); + queriesToRecompute.forEach(qid => { const query = this.queries[qid]; - if (query) {this._recomputeResults(query);} + + if (query) + this._recomputeResults(query); }); + this._observeQueue.drain(); + const result = remove.length; + if (callback) { Meteor.defer(() => { callback(null, result); }); } + return result; } @@ -219,7 +256,8 @@ export default class LocalCollection { // happened during the pause, it is a smarter 'coalesced' diff. resumeObservers() { // No-op if not paused. - if (!this.paused) {return;} + if (!this.paused) + return; // Unset the 'paused' flag. Make sure to do this first, otherwise // observer methods won't actually fire when we trigger them. @@ -227,27 +265,32 @@ export default class LocalCollection { for (let qid in this.queries) { const query = this.queries[qid]; + if (query.dirty) { query.dirty = false; + // re-compute results will perform `LocalCollection._diffQueryChanges` automatically. this._recomputeResults(query, query.resultsSnapshot); } else { // Diff the current results against the snapshot and send to observers. // pass the query object for its observer callbacks. - LocalCollection._diffQueryChanges( - query.ordered, query.resultsSnapshot, query.results, query, - {projectionFn: query.projectionFn}); + LocalCollection._diffQueryChanges(query.ordered, query.resultsSnapshot, query.results, query, {projectionFn: query.projectionFn}); } + query.resultsSnapshot = null; } + this._observeQueue.drain(); } retrieveOriginals() { - if (!this._savedOriginals) {throw new Error('Called retrieveOriginals without saveOriginals');} + if (!this._savedOriginals) + throw new Error('Called retrieveOriginals without saveOriginals'); const originals = this._savedOriginals; + this._savedOriginals = null; + return originals; } @@ -259,7 +302,9 @@ export default class LocalCollection { // is the value.) You must alternate between calls to saveOriginals() and // retrieveOriginals(). saveOriginals() { - if (this._savedOriginals) {throw new Error('Called saveOriginals twice without retrieveOriginals');} + if (this._savedOriginals) + throw new Error('Called saveOriginals twice without retrieveOriginals'); + this._savedOriginals = new LocalCollection._IdMap; } @@ -270,7 +315,9 @@ export default class LocalCollection { callback = options; options = null; } - if (!options) options = {}; + + if (!options) + options = {}; const matcher = new Minimongo.Matcher(selector, true); @@ -280,12 +327,14 @@ export default class LocalCollection { // they already have a resultsSnapshot and we won't be diffing in // _recomputeResults.) const qidToOriginalResults = {}; + // We should only clone each document once, even if it appears in multiple queries const docMap = new LocalCollection._IdMap; const idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector); - Object.keys(this.queries).forEach(qid => { + for (let qid in this.queries) { const query = this.queries[qid]; + if ((query.cursor.skip || query.cursor.limit) && ! this.paused) { // Catch the case of a reactive `count()` on a cursor with skip // or limit, which registers an unordered observe. This is a @@ -297,53 +346,58 @@ export default class LocalCollection { return; } - if (!(query.results instanceof Array)) { + if (!(query.results instanceof Array)) throw new Error('Assertion failed: query.results not an array'); - } // Clones a document to be stored in `qidToOriginalResults` // because it may be modified before the new and old result sets // are diffed. But if we know exactly which document IDs we're // going to modify, then we only need to clone those. const memoizedCloneIfNeeded = doc => { - if (docMap.has(doc._id)) { + if (docMap.has(doc._id)) return docMap.get(doc._id); - } - let docToMemoize; - if (idsMatchedBySelector && !idsMatchedBySelector.some(id => EJSON.equals(id, doc._id))) { - docToMemoize = doc; - } else { - docToMemoize = EJSON.clone(doc); - } + const docToMemoize = idsMatchedBySelector && !idsMatchedBySelector.some(id => EJSON.equals(id, doc._id)) + ? doc + : EJSON.clone(doc); docMap.set(doc._id, docToMemoize); + return docToMemoize; }; qidToOriginalResults[qid] = query.results.map(memoizedCloneIfNeeded); } - }); + } + const recomputeQids = {}; let updateCount = 0; this._eachPossiblyMatchingDoc(selector, (doc, id) => { const queryResult = matcher.documentMatches(doc); + if (queryResult.result) { // XXX Should we save the original even if mod ends up being a no-op? this._saveOriginal(id, doc); this._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices); + ++updateCount; - if (!options.multi) {return false;} // break + + if (!options.multi) + return false; // break } + return true; }); Object.keys(recomputeQids).forEach(qid => { const query = this.queries[qid]; - if (query) {this._recomputeResults(query, qidToOriginalResults[qid]);} + + if (query) + this._recomputeResults(query, qidToOriginalResults[qid]); }); + this._observeQueue.drain(); // If we are doing an upsert, and we didn't modify any documents yet, then @@ -351,25 +405,23 @@ export default class LocalCollection { // generate an id for it. let insertedId; if (updateCount === 0 && options.upsert) { - let selectorModifier = LocalCollection._selectorIsId(selector) - ? { _id: selector } - : selector; + const selectorModifier = LocalCollection._removeDollarOperators(LocalCollection._selectorIsId(selector) ? {_id: selector} : selector); + const doc = {}; - selectorModifier = LocalCollection._removeDollarOperators(selectorModifier); - - const newDoc = {}; if (selectorModifier._id) { - newDoc._id = selectorModifier._id; + doc._id = selectorModifier._id; delete selectorModifier._id; } // This double _modify call is made to help work around an issue where collection // upserts won't work properly, with nested properties (see issue #8631). - LocalCollection._modify(newDoc, {$set: selectorModifier}); - LocalCollection._modify(newDoc, mod, {isInsert: true}); + LocalCollection._modify(doc, {$set: selectorModifier}); + LocalCollection._modify(doc, mod, {isInsert: true}); - if (! newDoc._id && options.insertedId) {newDoc._id = options.insertedId;} - insertedId = this.insert(newDoc); + if (! doc._id && options.insertedId) + doc._id = options.insertedId; + + insertedId = this.insert(doc); updateCount = 1; } @@ -378,10 +430,10 @@ export default class LocalCollection { // inserted, if any. let result; if (options._returnObject) { - result = { - numberAffected: updateCount, - }; - if (insertedId !== undefined) {result.insertedId = insertedId;} + result = {numberAffected: updateCount}; + + if (insertedId !== undefined) + result.insertedId = insertedId; } else { result = updateCount; } @@ -391,6 +443,7 @@ export default class LocalCollection { callback(null, result); }); } + return result; } @@ -398,41 +451,41 @@ export default class LocalCollection { // equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject: // true }). upsert(selector, mod, options, callback) { - if (! callback && typeof options === 'function') { + if (!callback && typeof options === 'function') { callback = options; options = {}; } - return this.update(selector, mod, Object.assign({}, options, { - upsert: true, - _returnObject: true, - }), callback); + + return this.update(selector, mod, Object.assign({}, options, {upsert: true, _returnObject: true}), callback); } // Iterates over a subset of documents that could match selector; calls - // f(doc, id) on each of them. Specifically, if selector specifies + // fn(doc, id) on each of them. Specifically, if selector specifies // specific _id's, it only looks at those. doc is *not* cloned: it is the // same object that is in _docs. - _eachPossiblyMatchingDoc(selector, f) { + _eachPossiblyMatchingDoc(selector, fn) { const specificIds = LocalCollection._idsMatchedBySelector(selector); + if (specificIds) { - for (let i = 0; i < specificIds.length; ++i) { - const id = specificIds[i]; + specificIds.some(id => { const doc = this._docs.get(id); - if (doc) { - const breakIfFalse = f(doc, id); - if (breakIfFalse === false) {break;} - } - } + + if (doc) + return fn(doc, id) === false; + }); } else { - this._docs.forEach(f); + this._docs.forEach(fn); } } _modifyAndNotify(doc, mod, recomputeQids, arrayIndices) { const matched_before = {}; + for (let qid in this.queries) { const query = this.queries[qid]; - if (query.dirty) continue; + + if (query.dirty) + continue; if (query.ordered) { matched_before[qid] = query.matcher.documentMatches(doc).result; @@ -449,12 +502,16 @@ export default class LocalCollection { for (let qid in this.queries) { const query = this.queries[qid]; - if (query.dirty) continue; - const before = matched_before[qid]; + if (query.dirty) + continue; + const afterMatch = query.matcher.documentMatches(doc); const after = afterMatch.result; - if (after && query.distances && afterMatch.distance !== undefined) {query.distances.set(doc._id, afterMatch.distance);} + const before = matched_before[qid]; + + if (after && query.distances && afterMatch.distance !== undefined) + query.distances.set(doc._id, afterMatch.distance); if (query.cursor.skip || query.cursor.limit) { // We need to recompute any query where the doc may have been in the @@ -464,7 +521,8 @@ export default class LocalCollection { // applied... but if they are false, then the document definitely is NOT // in the output. So it's safe to skip recompute if neither before or // after are true.) - if (before || after) {recomputeQids[qid] = true;} + if (before || after) + recomputeQids[qid] = true; } else if (before && !after) { LocalCollection._removeFromResults(query, doc); } else if (!before && after) { @@ -494,25 +552,29 @@ export default class LocalCollection { return; } - if (! this.paused && ! oldResults) {oldResults = query.results;} - if (query.distances) {query.distances.clear();} - query.results = query.cursor._getRawObjects({ - ordered: query.ordered, distances: query.distances}); + if (!this.paused && !oldResults) + oldResults = query.results; - if (! this.paused) { - LocalCollection._diffQueryChanges( - query.ordered, oldResults, query.results, query, - { projectionFn: query.projectionFn }); - } + if (query.distances) + query.distances.clear(); + + query.results = query.cursor._getRawObjects({distances: query.distances, ordered: query.ordered}); + + if (!this.paused) + LocalCollection._diffQueryChanges(query.ordered, oldResults, query.results, query, {projectionFn: query.projectionFn}); } _saveOriginal(id, doc) { // Are we even trying to save originals? - if (!this._savedOriginals) {return;} + if (!this._savedOriginals) + return; + // Have we previously mutated the original (and so 'doc' is not actually // original)? (Note the 'has' check rather than truth: we store undefined // here for inserted docs!) - if (this._savedOriginals.has(id)) {return;} + if (this._savedOriginals.has(id)) + return; + this._savedOriginals.set(id, EJSON.clone(doc)); } } @@ -532,16 +594,19 @@ LocalCollection.ObserveHandle = ObserveHandle; // available as `this` to those callbacks. LocalCollection._CachingChangeObserver = class _CachingChangeObserver { constructor(options = {}) { - const orderedFromCallbacks = options.callbacks && - LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); + const orderedFromCallbacks = options.callbacks && LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); + if (options.hasOwnProperty('ordered')) { this.ordered = options.ordered; - if (options.callbacks && options.ordered !== orderedFromCallbacks) {throw Error("ordered option doesn't match callbacks");} + + if (options.callbacks && options.ordered !== orderedFromCallbacks) + throw Error('ordered option doesn\'t match callbacks'); } else if (options.callbacks) { this.ordered = orderedFromCallbacks; } else { throw Error('must provide ordered or callbacks'); } + const callbacks = options.callbacks || {}; if (this.ordered) { @@ -549,11 +614,16 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { this.applyChange = { addedBefore: (id, fields, before) => { const doc = EJSON.clone(fields); + doc._id = id; - callbacks.addedBefore && callbacks.addedBefore.call( - this, id, fields, before); + + if (callbacks.addedBefore) + callbacks.addedBefore.call(this, id, fields, before); + // This line triggers if we provide added with movedBefore. - callbacks.added && callbacks.added.call(this, id, fields); + if (callbacks.added) + callbacks.added.call(this, id, fields); + // XXX could `before` be a falsy ID? Technically // idStringify seems to allow for them -- though // OrderedDict won't call stringify on a falsy arg. @@ -561,7 +631,10 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { }, movedBefore: (id, before) => { const doc = this.docs.get(id); - callbacks.movedBefore && callbacks.movedBefore.call(this, id, before); + + if (callbacks.movedBefore) + callbacks.movedBefore.call(this, id, before); + this.docs.moveBefore(id, before || null); }, }; @@ -570,8 +643,12 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { this.applyChange = { added: (id, fields) => { const doc = EJSON.clone(fields); - callbacks.added && callbacks.added.call(this, id, fields); + + if (callbacks.added) + callbacks.added.call(this, id, fields); + doc._id = id; + this.docs.set(id, doc); }, }; @@ -581,13 +658,20 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { // identical. this.applyChange.changed = (id, fields) => { const doc = this.docs.get(id); - if (!doc) {throw new Error(`Unknown id for changed: ${id}`);} - callbacks.changed && callbacks.changed.call( - this, id, EJSON.clone(fields)); + + if (!doc) + throw new Error(`Unknown id for changed: ${id}`); + + if (callbacks.changed) + callbacks.changed.call(this, id, EJSON.clone(fields)); + DiffSequence.applyChanges(doc, fields); }; + this.applyChange.removed = id => { - callbacks.removed && callbacks.removed.call(this, id); + if (callbacks.removed) + callbacks.removed.call(this, id); + this.docs.remove(id); }; } @@ -609,10 +693,12 @@ LocalCollection._IdMap = class _IdMap extends IdMap { // original _id field // - If the return value doesn't have an _id field, add it back. LocalCollection.wrapTransform = transform => { - if (! transform) {return null;} + if (!transform) + return null; // No need to doubly-wrap transforms. - if (transform.__wrappedTransform__) {return transform;} + if (transform.__wrappedTransform__) + return transform; const wrapped = doc => { if (!doc.hasOwnProperty('_id')) { @@ -622,23 +708,25 @@ LocalCollection.wrapTransform = transform => { } const id = doc._id; + // XXX consider making tracker a weak dependency and checking Package.tracker here const transformed = Tracker.nonreactive(() => transform(doc)); - if (!LocalCollection._isPlainObject(transformed)) { + if (!LocalCollection._isPlainObject(transformed)) throw new Error('transform must return object'); - } if (transformed.hasOwnProperty('_id')) { - if (!EJSON.equals(transformed._id, id)) { + if (!EJSON.equals(transformed._id, id)) throw new Error("transformed document can't have different _id"); - } } else { transformed._id = id; } + return transformed; }; + wrapped.__wrappedTransform__ = true; + return wrapped; }; @@ -651,28 +739,38 @@ LocalCollection.wrapTransform = transform => { // This binary search puts a value between any equal values, and the first // lesser value. LocalCollection._binarySearch = (cmp, array, value) => { - let first = 0, rangeLength = array.length; + let first = 0; + let range = array.length; + + while (range > 0) { + const halfRange = Math.floor(range / 2); - while (rangeLength > 0) { - const halfRange = Math.floor(rangeLength / 2); if (cmp(value, array[first + halfRange]) >= 0) { first += halfRange + 1; - rangeLength -= halfRange + 1; + range -= halfRange + 1; } else { - rangeLength = halfRange; + range = halfRange; } } + return first; }; LocalCollection._checkSupportedProjection = fields => { - if (fields !== Object(fields) || Array.isArray(fields)) {throw MinimongoError('fields option must be an object');} + if (fields !== Object(fields) || Array.isArray(fields)) + throw MinimongoError('fields option must be an object'); Object.keys(fields).forEach(keyPath => { - const val = fields[keyPath]; - if (keyPath.split('.').includes('$')) {throw MinimongoError("Minimongo doesn't support $ operator in projections yet.");} - if (typeof val === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => Object.keys(val).includes(key))) {throw MinimongoError("Minimongo doesn't support operators in projections yet.");} - if (![1, 0, true, false].includes(val)) {throw MinimongoError('Projection values should be one of 1, 0, true, or false');} + if (keyPath.split('.').includes('$')) + throw MinimongoError('Minimongo doesn\'t support $ operator in projections yet.'); + + const value = fields[keyPath]; + + if (typeof value === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => value.hasOwnProperty(key))) + throw MinimongoError('Minimongo doesn\'t support operators in projections yet.'); + + if (![1, 0, true, false].includes(value)) + throw MinimongoError('Projection values should be one of 1, 0, true, or false'); }); }; @@ -692,28 +790,41 @@ LocalCollection._compileProjection = fields => { // returns transformed doc according to ruleTree const transform = (doc, ruleTree) => { // Special case for "sets" - if (Array.isArray(doc)) {return doc.map(subdoc => transform(subdoc, ruleTree));} + if (Array.isArray(doc)) + return doc.map(subdoc => transform(subdoc, ruleTree)); + + const result = details.including ? {} : EJSON.clone(doc); - const res = details.including ? {} : EJSON.clone(doc); Object.keys(ruleTree).forEach(key => { + if (!doc.hasOwnProperty(key)) + return; + const rule = ruleTree[key]; - if (!doc.hasOwnProperty(key)) {return;} + if (rule === Object(rule)) { // For sub-objects/subsets we branch - if (doc[key] === Object(doc[key])) {res[key] = transform(doc[key], rule);} - // Otherwise we don't even touch this subfield - } else if (details.including) {res[key] = EJSON.clone(doc[key]);} else {delete res[key];} + if (doc[key] === Object(doc[key])) + result[key] = transform(doc[key], rule); + } else if (details.including) { // Otherwise we don't even touch this subfield + result[key] = EJSON.clone(doc[key]); + } else { + delete result[key]; + } }); - return res; + return result; }; - return obj => { - const res = transform(obj, details.tree); + return doc => { + const result = transform(doc, details.tree); - if (_idProjection && obj.hasOwnProperty('_id')) {res._id = obj._id;} - if (!_idProjection && res.hasOwnProperty('_id')) {delete res._id;} - return res; + if (_idProjection && doc.hasOwnProperty('_id')) + result._id = doc._id; + + if (!_idProjection && result.hasOwnProperty('_id')) + delete result._id; + + return result; }; }; @@ -738,10 +849,14 @@ LocalCollection._diffQueryUnorderedChanges = (oldResults, newResults, observer, }; LocalCollection._findInOrderedResults = (query, doc) => { - if (!query.ordered) {throw new Error("Can't call _findInOrderedResults on unordered query");} + if (!query.ordered) + throw new Error('Can\'t call _findInOrderedResults on unordered query'); + for (let i = 0; i < query.results.length; i++) { - if (query.results[i] === doc) {return i;} + if (query.results[i] === doc) + return i; } + throw Error('object missing from query'); }; @@ -752,30 +867,37 @@ LocalCollection._findInOrderedResults = (query, doc) => { // access-controlled update and remove. LocalCollection._idsMatchedBySelector = selector => { // Is the selector just an ID? - if (LocalCollection._selectorIsId(selector)) {return [selector];} - if (!selector) {return null;} + if (LocalCollection._selectorIsId(selector)) + return [selector]; + + if (!selector) + return null; // Do we have an _id clause? if (selector.hasOwnProperty('_id')) { // Is the _id clause just an ID? - if (LocalCollection._selectorIsId(selector._id)) {return [selector._id];} + if (LocalCollection._selectorIsId(selector._id)) + return [selector._id]; + // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? - if (selector._id && selector._id.$in + if (selector._id && Array.isArray(selector._id.$in) && selector._id.$in.length - && selector._id.$in.every(LocalCollection._selectorIsId)) { + && selector._id.$in.every(LocalCollection._selectorIsId)) return selector._id.$in; - } + return null; } // If this is a top-level $and, and any of the clauses constrain their // documents, then the whole selector is constrained by any one clause's // constraint. (Well, by their intersection, but that seems unlikely.) - if (selector.$and && Array.isArray(selector.$and)) { + if (Array.isArray(selector.$and)) { for (let i = 0; i < selector.$and.length; ++i) { const subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); - if (subIds) {return subIds;} + + if (subIds) + return subIds; } } @@ -784,19 +906,25 @@ LocalCollection._idsMatchedBySelector = selector => { LocalCollection._insertInResults = (query, doc) => { const fields = EJSON.clone(doc); + delete fields._id; + if (query.ordered) { if (!query.sorter) { query.addedBefore(doc._id, query.projectionFn(fields), null); query.results.push(doc); } else { - const i = LocalCollection._insertInSortedList( - query.sorter.getComparator({distances: query.distances}), - query.results, doc); + const i = LocalCollection._insertInSortedList(query.sorter.getComparator({distances: query.distances}), query.results, doc); + let next = query.results[i + 1]; - if (next) {next = next._id;} else {next = null;} + if (next) + next = next._id; + else + next = null; + query.addedBefore(doc._id, query.projectionFn(fields), next); } + query.added(doc._id, query.projectionFn(fields)); } else { query.added(doc._id, query.projectionFn(fields)); @@ -810,9 +938,11 @@ LocalCollection._insertInSortedList = (cmp, array, value) => { return 0; } - const idx = LocalCollection._binarySearch(cmp, array, value); - array.splice(idx, 0, value); - return idx; + const i = LocalCollection._binarySearch(cmp, array, value); + + array.splice(i, 0, value); + + return i; }; // XXX maybe this should be EJSON.isObject, though EJSON doesn't know about @@ -834,72 +964,70 @@ LocalCollection._isPlainObject = x => { // - isInsert is set when _modify is being called to compute the document to // insert as part of an upsert operation. We use this primarily to figure // out when to set the fields in $setOnInsert, if present. -LocalCollection._modify = (doc, mod, options) => { - options = options || {}; - if (!LocalCollection._isPlainObject(mod)) {throw MinimongoError('Modifier must be an object');} +LocalCollection._modify = (doc, modifier, options = {}) => { + if (!LocalCollection._isPlainObject(modifier)) + throw MinimongoError('Modifier must be an object'); // Make sure the caller can't mutate our data structures. - mod = EJSON.clone(mod); + modifier = EJSON.clone(modifier); - const isModifier = isOperatorObject(mod); + const isModifier = isOperatorObject(modifier); + const newDoc = isModifier ? EJSON.clone(doc) : modifier; - let newDoc; - - if (!isModifier) { - if (mod._id && !EJSON.equals(doc._id, mod._id)) {throw MinimongoError('Cannot change the _id of a document');} - - // replace the whole document - assertHasValidFieldNames(mod); - newDoc = mod; - } else { + if (isModifier) { // apply modifiers to the doc. - newDoc = EJSON.clone(doc); - - Object.keys(mod).forEach(op => { - const operand = mod[op]; - let modFunc = MODIFIERS[op]; + Object.keys(modifier).forEach(operator => { // Treat $setOnInsert as $set if this is an insert. - if (options.isInsert && op === '$setOnInsert') {modFunc = MODIFIERS.$set;} - if (!modFunc) {throw MinimongoError(`Invalid modifier specified ${op}`);} + const modFunc = MODIFIERS[options.isInsert && operator === '$setOnInsert' ? '$set' : operator]; + const operand = modifier[operator]; + + if (!modFunc) + throw MinimongoError(`Invalid modifier specified ${operator}`); + Object.keys(operand).forEach(keypath => { const arg = operand[keypath]; - if (keypath === '') { - throw MinimongoError('An empty update path is not valid.'); - } - if (keypath === '_id' && op !== '$setOnInsert') { + if (keypath === '') + throw MinimongoError('An empty update path is not valid.'); + + if (keypath === '_id' && operator !== '$setOnInsert') throw MinimongoError('Mod on _id not allowed'); - } const keyparts = keypath.split('.'); - if (!keyparts.every(Boolean)) { - throw MinimongoError( - `The update path '${keypath}' contains an empty field name, which is not allowed.`); - } + if (!keyparts.every(Boolean)) + throw MinimongoError(`The update path '${keypath}' contains an empty field name, which is not allowed.`); - const noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(op); - const forbidArray = op === '$rename'; + const noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(operator); + const forbidArray = operator === '$rename'; const target = findModTarget(newDoc, keyparts, { - noCreate: NO_CREATE_MODIFIERS[op], - forbidArray: op === '$rename', arrayIndices: options.arrayIndices, + forbidArray: operator === '$rename', + noCreate: NO_CREATE_MODIFIERS[operator] }); - const field = keyparts.pop(); - modFunc(target, field, arg, keypath, newDoc); + + modFunc(target, keyparts.pop(), arg, keypath, newDoc); }); }); + } else { + if (modifier._id && !EJSON.equals(doc._id, modifier._id)) + throw MinimongoError('Cannot change the _id of a document'); + + // replace the whole document + assertHasValidFieldNames(modifier); } // move new document into place. - Object.keys(doc).forEach(k => { - // Note: this used to be for (var k in doc) however, this does not + Object.keys(doc).forEach(key => { + // Note: this used to be for (var key in doc) however, this does not // work right in Opera. Deleting from a doc while iterating over it // would sometimes cause opera to skip some keys. - if (k !== '_id') {delete doc[k];} + if (key !== '_id') + delete doc[key]; }); - Object.keys(newDoc).forEach(k => { - doc[k] = newDoc[k]; + + Object.keys(newDoc).forEach(key => { + doc[key] = newDoc[key]; }); }; @@ -914,101 +1042,112 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { // need absolute indices benefit from the other features of this API -- // relative order, transforms, and applyChanges -- without the speed hit. const indices = !observeCallbacks._no_indices; + observeChangesCallbacks = { addedBefore(id, fields, before) { - if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) {return;} + if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) + return; + const doc = transform(Object.assign(fields, {_id: id})); - if (observeCallbacks.addedAt) { - const index = indices - ? before ? this.docs.indexOf(before) : this.docs.size() : -1; - observeCallbacks.addedAt(doc, index, before); - } else { + + if (observeCallbacks.addedAt) + observeCallbacks.addedAt(doc, indices ? before ? this.docs.indexOf(before) : this.docs.size() : -1, before); + else observeCallbacks.added(doc); - } }, changed(id, fields) { - if (!(observeCallbacks.changedAt || observeCallbacks.changed)) {return;} + if (!(observeCallbacks.changedAt || observeCallbacks.changed)) + return; + let doc = EJSON.clone(this.docs.get(id)); - if (!doc) {throw new Error(`Unknown id for changed: ${id}`);} + if (!doc) + throw new Error(`Unknown id for changed: ${id}`); + const oldDoc = transform(EJSON.clone(doc)); + DiffSequence.applyChanges(doc, fields); - doc = transform(doc); - if (observeCallbacks.changedAt) { - const index = indices ? this.docs.indexOf(id) : -1; - observeCallbacks.changedAt(doc, oldDoc, index); - } else { - observeCallbacks.changed(doc, oldDoc); - } + + if (observeCallbacks.changedAt) + observeCallbacks.changedAt(transform(doc), oldDoc, indices ? this.docs.indexOf(id) : -1); + else + observeCallbacks.changed(transform(doc), oldDoc); }, movedBefore(id, before) { - if (!observeCallbacks.movedTo) {return;} - const from = indices ? this.docs.indexOf(id) : -1; + if (!observeCallbacks.movedTo) + return; + + const from = indices ? this.docs.indexOf(id) : -1; + let to = indices ? before ? this.docs.indexOf(before) : this.docs.size() : -1; - let to = indices - ? before ? this.docs.indexOf(before) : this.docs.size() : -1; // When not moving backwards, adjust for the fact that removing the // document slides everything back one slot. - if (to > from) {--to;} - observeCallbacks.movedTo(transform(EJSON.clone(this.docs.get(id))), - from, to, before || null); + if (to > from) + --to; + + observeCallbacks.movedTo(transform(EJSON.clone(this.docs.get(id))), from, to, before || null); }, removed(id) { - if (!(observeCallbacks.removedAt || observeCallbacks.removed)) {return;} + if (!(observeCallbacks.removedAt || observeCallbacks.removed)) + return; + // technically maybe there should be an EJSON.clone here, but it's about // to be removed from this.docs! const doc = transform(this.docs.get(id)); - if (observeCallbacks.removedAt) { - const index = indices ? this.docs.indexOf(id) : -1; - observeCallbacks.removedAt(doc, index); - } else { + + if (observeCallbacks.removedAt) + observeCallbacks.removedAt(doc, indices ? this.docs.indexOf(id) : -1); + else observeCallbacks.removed(doc); - } }, }; } else { observeChangesCallbacks = { added(id, fields) { - if (!suppressed && observeCallbacks.added) { - const doc = Object.assign(fields, {_id: id}); - observeCallbacks.added(transform(doc)); - } + if (!suppressed && observeCallbacks.added) + observeCallbacks.added(transform(Object.assign(fields, {_id: id}))); }, changed(id, fields) { if (observeCallbacks.changed) { const oldDoc = this.docs.get(id); const doc = EJSON.clone(oldDoc); + DiffSequence.applyChanges(doc, fields); - observeCallbacks.changed(transform(doc), - transform(EJSON.clone(oldDoc))); + + observeCallbacks.changed(transform(doc), transform(EJSON.clone(oldDoc))); } }, removed(id) { - if (observeCallbacks.removed) { + if (observeCallbacks.removed) observeCallbacks.removed(transform(this.docs.get(id))); - } }, }; } - const changeObserver = new LocalCollection._CachingChangeObserver( - {callbacks: observeChangesCallbacks}); + const changeObserver = new LocalCollection._CachingChangeObserver({callbacks: observeChangesCallbacks}); const handle = cursor.observeChanges(changeObserver.applyChange); + suppressed = false; return handle; }; LocalCollection._observeCallbacksAreOrdered = callbacks => { - if (callbacks.addedAt && callbacks.added) {throw new Error('Please specify only one of added() and addedAt()');} - if (callbacks.changedAt && callbacks.changed) {throw new Error('Please specify only one of changed() and changedAt()');} - if (callbacks.removed && callbacks.removedAt) {throw new Error('Please specify only one of removed() and removedAt()');} + if (callbacks.added && callbacks.addedAt) + throw new Error('Please specify only one of added() and addedAt()'); - return !!(callbacks.addedAt || callbacks.movedTo || callbacks.changedAt - || callbacks.removedAt); + if (callbacks.changed && callbacks.changedAt) + throw new Error('Please specify only one of changed() and changedAt()'); + + if (callbacks.removed && callbacks.removedAt) + throw new Error('Please specify only one of removed() and removedAt()'); + + return !!(callbacks.addedAt || callbacks.changedAt || callbacks.movedTo || callbacks.removedAt); }; LocalCollection._observeChangesCallbacksAreOrdered = callbacks => { - if (callbacks.added && callbacks.addedBefore) {throw new Error('Please specify only one of added() and addedBefore()');} + if (callbacks.added && callbacks.addedBefore) + throw new Error('Please specify only one of added() and addedBefore()'); + return !!(callbacks.addedBefore || callbacks.movedBefore); }; @@ -1021,29 +1160,31 @@ LocalCollection._observeChangesCallbacksAreOrdered = callbacks => { // should not actually be stripped. // See https://github.com/meteor/meteor/issues/8806. LocalCollection._removeDollarOperators = selector => { - let cleansed = {}; + const cleansed = {}; + Object.keys(selector).forEach((key) => { const value = selector[key]; + if (key.charAt(0) !== '$' && !objectOnlyHasDollarKeys(value)) { - if (value !== null - && value.constructor - && Object.getPrototypeOf(value) === Object.prototype) { + if (value !== null && value.constructor && Object.getPrototypeOf(value) === Object.prototype) cleansed[key] = LocalCollection._removeDollarOperators(value); - } else { + else cleansed[key] = value; - } } }); + return cleansed; }; LocalCollection._removeFromResults = (query, doc) => { if (query.ordered) { const i = LocalCollection._findInOrderedResults(query, doc); + query.removed(doc._id); query.results.splice(i, 1); } else { const id = doc._id; // in case callback mutates doc + query.removed(doc._id); query.results.remove(id); } @@ -1051,47 +1192,53 @@ LocalCollection._removeFromResults = (query, doc) => { // Is this selector just shorthand for lookup by _id? LocalCollection._selectorIsId = selector => { - return typeof selector === 'string' || - typeof selector === 'number' || - selector instanceof MongoID.ObjectID; + return typeof selector === 'number' || typeof selector === 'string' || selector instanceof MongoID.ObjectID; }; // Is the selector just lookup by _id (shorthand or not)? LocalCollection._selectorIsIdPerhapsAsObject = selector => { - return LocalCollection._selectorIsId(selector) || - selector && typeof selector === 'object' && - selector._id && LocalCollection._selectorIsId(selector._id) && - Object.keys(selector).length === 1; + if (LocalCollection._selectorIsId(selector)) + return true; + + return LocalCollection._selectorIsId(selector && selector._id) && Object.keys(selector).length === 1; }; LocalCollection._updateInResults = (query, doc, old_doc) => { - if (!EJSON.equals(doc._id, old_doc._id)) {throw new Error("Can't change a doc's _id while updating");} + if (!EJSON.equals(doc._id, old_doc._id)) + throw new Error('Can\'t change a doc\'s _id while updating'); + const projectionFn = query.projectionFn; - const changedFields = DiffSequence.makeChangedFields( - projectionFn(doc), projectionFn(old_doc)); + const changedFields = DiffSequence.makeChangedFields(projectionFn(doc), projectionFn(old_doc)); if (!query.ordered) { if (Object.keys(changedFields).length) { query.changed(doc._id, changedFields); query.results.set(doc._id, doc); } + return; } - const orig_idx = LocalCollection._findInOrderedResults(query, doc); + const old_idx = LocalCollection._findInOrderedResults(query, doc); - if (Object.keys(changedFields).length) {query.changed(doc._id, changedFields);} - if (!query.sorter) {return;} + if (Object.keys(changedFields).length) + query.changed(doc._id, changedFields); - // just take it out and put it back in again, and see if the index - // changes - query.results.splice(orig_idx, 1); - const new_idx = LocalCollection._insertInSortedList( - query.sorter.getComparator({distances: query.distances}), - query.results, doc); - if (orig_idx !== new_idx) { + if (!query.sorter) + return; + + // just take it out and put it back in again, and see if the index changes + query.results.splice(old_idx, 1); + + const new_idx = LocalCollection._insertInSortedList(query.sorter.getComparator({distances: query.distances}), query.results, doc); + + if (old_idx !== new_idx) { let next = query.results[new_idx + 1]; - if (next) {next = next._id;} else {next = null;} + if (next) + next = next._id; + else + next = null; + query.movedBefore && query.movedBefore(doc._id, next); } }; @@ -1099,56 +1246,50 @@ LocalCollection._updateInResults = (query, doc, old_doc) => { const MODIFIERS = { $currentDate(target, field, arg) { if (typeof arg === 'object' && arg.hasOwnProperty('$type')) { - if (arg.$type !== 'date') { - throw MinimongoError( - 'Minimongo does currently only support the date type ' + - 'in $currentDate modifiers', - { field }); - } + if (arg.$type !== 'date') + throw MinimongoError('Minimongo does currently only support the date type in $currentDate modifiers', {field}); } else if (arg !== true) { - throw MinimongoError('Invalid $currentDate modifier', { field }); + throw MinimongoError('Invalid $currentDate modifier', {field}); } + target[field] = new Date(); }, $min(target, field, arg) { - if (typeof arg !== 'number') { - throw MinimongoError('Modifier $min allowed for numbers only', { field }); - } + if (typeof arg !== 'number') + throw MinimongoError('Modifier $min allowed for numbers only', {field}); + if (field in target) { - if (typeof target[field] !== 'number') { - throw MinimongoError( - 'Cannot apply $min modifier to non-number', { field }); - } - if (target[field] > arg) { + if (typeof target[field] !== 'number') + throw MinimongoError('Cannot apply $min modifier to non-number', {field}); + + if (target[field] > arg) target[field] = arg; - } } else { target[field] = arg; } }, $max(target, field, arg) { - if (typeof arg !== 'number') { - throw MinimongoError('Modifier $max allowed for numbers only', { field }); - } + if (typeof arg !== 'number') + throw MinimongoError('Modifier $max allowed for numbers only', {field}); + if (field in target) { - if (typeof target[field] !== 'number') { - throw MinimongoError( - 'Cannot apply $max modifier to non-number', { field }); - } - if (target[field] < arg) { + if (typeof target[field] !== 'number') + throw MinimongoError('Cannot apply $max modifier to non-number', {field}); + + if (target[field] < arg) target[field] = arg; - } } else { target[field] = arg; } }, $inc(target, field, arg) { - if (typeof arg !== 'number') {throw MinimongoError('Modifier $inc allowed for numbers only', { field });} + if (typeof arg !== 'number') + throw MinimongoError('Modifier $inc allowed for numbers only', {field}); + if (field in target) { - if (typeof target[field] !== 'number') { - throw MinimongoError( - 'Cannot apply $inc modifier to non-number', { field }); - } + if (typeof target[field] !== 'number') + throw MinimongoError('Cannot apply $inc modifier to non-number', { field }); + target[field] += arg; } else { target[field] = arg; @@ -1156,17 +1297,19 @@ const MODIFIERS = { }, $set(target, field, arg) { if (target !== Object(target)) { // not an array or an object - const e = MinimongoError( - 'Cannot set property on non-object field', { field }); - e.setPropertyError = true; - throw e; + const error = MinimongoError('Cannot set property on non-object field', {field}); + error.setPropertyError = true; + throw error; } + if (target === null) { - const e = MinimongoError('Cannot set property on null', { field }); - e.setPropertyError = true; - throw e; + const error = MinimongoError('Cannot set property on null', {field}); + error.setPropertyError = true; + throw error; } + assertHasValidFieldNames(arg); + target[field] = arg; }, $setOnInsert(target, field, arg) { @@ -1175,45 +1318,55 @@ const MODIFIERS = { $unset(target, field, arg) { if (target !== undefined) { if (target instanceof Array) { - if (field in target) {target[field] = null;} - } else {delete target[field];} + if (field in target) + target[field] = null; + } else { + delete target[field]; + } } }, $push(target, field, arg) { - if (target[field] === undefined) {target[field] = [];} - if (!(target[field] instanceof Array)) { - throw MinimongoError( - 'Cannot apply $push modifier to non-array', { field }); - } + if (target[field] === undefined) + target[field] = []; + + if (!(target[field] instanceof Array)) + throw MinimongoError('Cannot apply $push modifier to non-array', {field}); if (!(arg && arg.$each)) { // Simple mode: not $each assertHasValidFieldNames(arg); + target[field].push(arg); + return; } // Fancy mode: $each (and maybe $slice and $sort and $position) const toPush = arg.$each; - if (!(toPush instanceof Array)) {throw MinimongoError('$each must be an array', { field });} + if (!(toPush instanceof Array)) + throw MinimongoError('$each must be an array', {field}); + assertHasValidFieldNames(toPush); // Parse $position let position = undefined; if ('$position' in arg) { - if (typeof arg.$position !== 'number') {throw MinimongoError('$position must be a numeric value', { field });} + if (typeof arg.$position !== 'number') + throw MinimongoError('$position must be a numeric value', {field}); + // XXX should check to make sure integer - if (arg.$position < 0) { - throw MinimongoError( - '$position in $push must be zero or positive', { field }); - } + if (arg.$position < 0) + throw MinimongoError('$position in $push must be zero or positive', {field}); + position = arg.$position; } // Parse $slice. let slice = undefined; if ('$slice' in arg) { - if (typeof arg.$slice !== 'number') {throw MinimongoError('$slice must be a numeric value', { field });} + if (typeof arg.$slice !== 'number') + throw MinimongoError('$slice must be a numeric value', {field}); + // XXX should check to make sure integer slice = arg.$slice; } @@ -1221,51 +1374,70 @@ const MODIFIERS = { // Parse $sort. let sortFunction = undefined; if (arg.$sort) { - if (slice === undefined) {throw MinimongoError('$sort requires $slice to be present', { field });} + if (slice === undefined) + throw MinimongoError('$sort requires $slice to be present', {field}); + // XXX this allows us to use a $sort whose value is an array, but that's // actually an extension of the Node driver, so it won't work // server-side. Could be confusing! // XXX is it correct that we don't do geo-stuff here? sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); - for (let i = 0; i < toPush.length; i++) { - if (LocalCollection._f._type(toPush[i]) !== 3) { - throw MinimongoError('$push like modifiers using $sort ' + - 'require all elements to be objects', { field }); - } - } + + toPush.forEach(element => { + if (LocalCollection._f._type(element) !== 3) + throw MinimongoError('$push like modifiers using $sort require all elements to be objects', {field}); + }); } // Actually push. if (position === undefined) { - for (let j = 0; j < toPush.length; j++) {target[field].push(toPush[j]);} + toPush.forEach(element => { + target[field].push(element); + }); } else { const spliceArguments = [position, 0]; - for (let j = 0; j < toPush.length; j++) {spliceArguments.push(toPush[j]);} + + toPush.forEach(element => { + spliceArguments.push(element); + }); + target[field].splice(...spliceArguments); } // Actually sort. - if (sortFunction) {target[field].sort(sortFunction);} + if (sortFunction) + target[field].sort(sortFunction); // Actually slice. if (slice !== undefined) { - if (slice === 0) {target[field] = [];} // differs from Array.slice! - else if (slice < 0) {target[field] = target[field].slice(slice);} else {target[field] = target[field].slice(0, slice);} + if (slice === 0) { + target[field] = []; // differs from Array.slice! + } else if (slice < 0) { + target[field] = target[field].slice(slice); + } else { + target[field] = target[field].slice(0, slice); + } } }, $pushAll(target, field, arg) { - if (!(typeof arg === 'object' && arg instanceof Array)) {throw MinimongoError('Modifier $pushAll/pullAll allowed for arrays only');} + if (!(typeof arg === 'object' && arg instanceof Array)) + throw MinimongoError('Modifier $pushAll/pullAll allowed for arrays only'); + assertHasValidFieldNames(arg); - const x = target[field]; - if (x === undefined) {target[field] = arg;} else if (!(x instanceof Array)) { - throw MinimongoError( - 'Cannot apply $pushAll modifier to non-array', { field }); + + const toPush = target[field]; + + if (toPush === undefined) { + target[field] = arg; + } else if (!(toPush instanceof Array)) { + throw MinimongoError('Cannot apply $pushAll modifier to non-array', {field}); } else { - for (let i = 0; i < arg.length; i++) {x.push(arg[i]);} + toPush.push(...arg); } }, $addToSet(target, field, arg) { let isEach = false; + if (typeof arg === 'object') { // check if first key is '$each' const keys = Object.keys(arg); @@ -1273,132 +1445,141 @@ const MODIFIERS = { isEach = true; } } + const values = isEach ? arg.$each : [arg]; + assertHasValidFieldNames(values); - const x = target[field]; - if (x === undefined) {target[field] = values;} else if (!(x instanceof Array)) { - throw MinimongoError( - 'Cannot apply $addToSet modifier to non-array', { field }); + + const toAdd = target[field]; + if (toAdd === undefined) { + target[field] = values; + } else if (!(toAdd instanceof Array)) { + throw MinimongoError('Cannot apply $addToSet modifier to non-array', {field}); } else { values.forEach(value => { - for (let i = 0; i < x.length; i++) { - if (LocalCollection._f._equal(value, x[i])) {return;} - } - x.push(value); + if (toAdd.some(element => LocalCollection._f._equal(value, element))) + return; + + toAdd.push(value); }); } }, $pop(target, field, arg) { - if (target === undefined) {return;} - const x = target[field]; - if (x === undefined) {return;} else if (!(x instanceof Array)) { - throw MinimongoError( - 'Cannot apply $pop modifier to non-array', { field }); - } else { - if (typeof arg === 'number' && arg < 0) {x.splice(0, 1);} else {x.pop();} - } + if (target === undefined) + return; + + const toPop = target[field]; + + if (toPop === undefined) + return; + + if (!(toPop instanceof Array)) + throw MinimongoError('Cannot apply $pop modifier to non-array', {field}); + + if (typeof arg === 'number' && arg < 0) + toPop.splice(0, 1); + else + toPop.pop(); }, $pull(target, field, arg) { - if (target === undefined) {return;} - const x = target[field]; - if (x === undefined) {return;} else if (!(x instanceof Array)) { - throw MinimongoError( - 'Cannot apply $pull/pullAll modifier to non-array', { field }); - } else { - const out = []; - if (arg != null && typeof arg === 'object' && !(arg instanceof Array)) { - // XXX would be much nicer to compile this once, rather than - // for each document we modify.. but usually we're not - // modifying that many documents, so we'll let it slide for - // now + if (target === undefined) + return; - // XXX Minimongo.Matcher isn't up for the job, because we need - // to permit stuff like {$pull: {a: {$gt: 4}}}.. something - // like {$gt: 4} is not normally a complete selector. - // same issue as $elemMatch possibly? - const matcher = new Minimongo.Matcher(arg); - for (let i = 0; i < x.length; i++) { - if (!matcher.documentMatches(x[i]).result) {out.push(x[i]);} - } - } else { - for (let i = 0; i < x.length; i++) { - if (!LocalCollection._f._equal(x[i], arg)) {out.push(x[i]);} - } - } - target[field] = out; + const toPull = target[field]; + if (toPull === undefined) + return; + + if (!(toPull instanceof Array)) + throw MinimongoError('Cannot apply $pull/pullAll modifier to non-array', {field}); + + let out; + if (arg != null && typeof arg === 'object' && !(arg instanceof Array)) { + // XXX would be much nicer to compile this once, rather than + // for each document we modify.. but usually we're not + // modifying that many documents, so we'll let it slide for + // now + + // XXX Minimongo.Matcher isn't up for the job, because we need + // to permit stuff like {$pull: {a: {$gt: 4}}}.. something + // like {$gt: 4} is not normally a complete selector. + // same issue as $elemMatch possibly? + const matcher = new Minimongo.Matcher(arg); + + out = toPull.filter(element => !matcher.documentMatches(element).result); + } else { + out = toPull.filter(element => !LocalCollection._f._equal(element, arg)); } + + target[field] = out; }, $pullAll(target, field, arg) { - if (!(typeof arg === 'object' && arg instanceof Array)) { - throw MinimongoError( - 'Modifier $pushAll/pullAll allowed for arrays only', { field }); - } - if (target === undefined) {return;} - const x = target[field]; - if (x === undefined) {return;} else if (!(x instanceof Array)) { - throw MinimongoError( - 'Cannot apply $pull/pullAll modifier to non-array', { field }); - } else { - const out = []; - for (let i = 0; i < x.length; i++) { - let exclude = false; - for (let j = 0; j < arg.length; j++) { - if (LocalCollection._f._equal(x[i], arg[j])) { - exclude = true; - break; - } - } - if (!exclude) {out.push(x[i]);} - } - target[field] = out; - } + if (!(typeof arg === 'object' && arg instanceof Array)) + throw MinimongoError('Modifier $pushAll/pullAll allowed for arrays only', {field}); + + if (target === undefined) + return; + + const toPull = target[field]; + + if (toPull === undefined) + return; + + if (!(toPull instanceof Array)) + throw MinimongoError('Cannot apply $pull/pullAll modifier to non-array', {field}); + + target[field] = toPull.filter(object => !arg.some(element => LocalCollection._f._equal(object, element))); }, $rename(target, field, arg, keypath, doc) { - if (keypath === arg) // no idea why mongo has this restriction.. - {throw MinimongoError('$rename source must differ from target', { field });} - if (target === null) {throw MinimongoError('$rename source field invalid', { field });} - if (typeof arg !== 'string') {throw MinimongoError('$rename target must be a string', { field });} + if (keypath === arg) + throw MinimongoError('$rename source must differ from target', {field}); + + if (target === null) + throw MinimongoError('$rename source field invalid', {field}); + + if (typeof arg !== 'string') + throw MinimongoError('$rename target must be a string', {field}); + if (arg.includes('\0')) { // Null bytes are not allowed in Mongo field names // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names - throw MinimongoError( - "The 'to' field for $rename cannot contain an embedded null byte", - { field }); + throw MinimongoError('The \'to\' field for $rename cannot contain an embedded null byte', {field}); } - if (target === undefined) {return;} - const v = target[field]; + + if (target === undefined) + return; + + const object = target[field]; + delete target[field]; const keyparts = arg.split('.'); const target2 = findModTarget(doc, keyparts, {forbidArray: true}); - if (target2 === null) {throw MinimongoError('$rename target field invalid', { field });} - const field2 = keyparts.pop(); - target2[field2] = v; + + if (target2 === null) + throw MinimongoError('$rename target field invalid', {field}); + + target2[keyparts.pop()] = object; }, $bit(target, field, arg) { // XXX mongo only supports $bit on integers, and we only support // native javascript numbers (doubles) so far, so we can't support $bit - throw MinimongoError('$bit is not supported', { field }); + throw MinimongoError('$bit is not supported', {field}); }, }; const NO_CREATE_MODIFIERS = { - $unset: true, $pop: true, - $rename: true, $pull: true, $pullAll: true, + $rename: true, + $unset: true }; // Make sure field names do not contain Mongo restricted // characters ('.', '$', '\0'). // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -const invalidCharMsg = { - '.': "contain '.'", - $: "start with '$'", - '\0': 'contain null bytes', -}; +const invalidCharMsg = {$: 'start with \'$\'', '.': 'contain \'.\'', '\0': 'contain null bytes'}; // checks if all field names in an object are valid function assertHasValidFieldNames(doc) { @@ -1412,9 +1593,8 @@ function assertHasValidFieldNames(doc) { function assertIsValidFieldName(key) { let match; - if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { + if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); - } } // for a.b.c.2.d.e, keyparts should be ['a', 'b', 'c', '2', 'd', 'e'], @@ -1436,51 +1616,73 @@ function assertIsValidFieldName(key) { // the path. function findModTarget(doc, keyparts, options = {}) { let usedArrayIndex = false; + for (let i = 0; i < keyparts.length; i++) { const last = i === keyparts.length - 1; let keypart = keyparts[i]; - const indexable = isIndexable(doc); - if (!indexable) { - if (options.noCreate) {return undefined;} - const e = MinimongoError( - `cannot use the part '${keypart}' to traverse ${doc}`); - e.setPropertyError = true; - throw e; + + if (!isIndexable(doc)) { + if (options.noCreate) + return undefined; + + const error = MinimongoError(`cannot use the part '${keypart}' to traverse ${doc}`); + error.setPropertyError = true; + throw error; } + if (doc instanceof Array) { - if (options.forbidArray) {return null;} + if (options.forbidArray) + return null; + if (keypart === '$') { - if (usedArrayIndex) {throw MinimongoError("Too many positional (i.e. '$') elements");} - if (!options.arrayIndices || !options.arrayIndices.length) { - throw MinimongoError('The positional operator did not find the ' + - 'match needed from the query'); - } + if (usedArrayIndex) + throw MinimongoError('Too many positional (i.e. \'$\') elements'); + + if (!options.arrayIndices || !options.arrayIndices.length) + throw MinimongoError('The positional operator did not find the match needed from the query'); + keypart = options.arrayIndices[0]; usedArrayIndex = true; } else if (isNumericKey(keypart)) { keypart = parseInt(keypart); } else { - if (options.noCreate) {return undefined;} - throw MinimongoError( - `can't append to array using string field name [${keypart}]`); + if (options.noCreate) + return undefined; + + throw MinimongoError(`can't append to array using string field name [${keypart}]`); } + if (last) - // handle 'a.01' - {keyparts[i] = keypart;} - if (options.noCreate && keypart >= doc.length) {return undefined;} - while (doc.length < keypart) {doc.push(null);} + keyparts[i] = keypart; // handle 'a.01' + + if (options.noCreate && keypart >= doc.length) + return undefined; + + while (doc.length < keypart) + doc.push(null); + if (!last) { - if (doc.length === keypart) {doc.push({});} else if (typeof doc[keypart] !== 'object') {throw MinimongoError(`can't modify field '${keyparts[i + 1]}' of list value ${JSON.stringify(doc[keypart])}`);} + if (doc.length === keypart) { + doc.push({}); + } else if (typeof doc[keypart] !== 'object') { + throw MinimongoError(`can't modify field '${keyparts[i + 1]}' of list value ${JSON.stringify(doc[keypart])}`); + } } } else { assertIsValidFieldName(keypart); + if (!(keypart in doc)) { - if (options.noCreate) {return undefined;} - if (!last) {doc[keypart] = {};} + if (options.noCreate) + return undefined; + + if (!last) + doc[keypart] = {}; } } - if (last) {return doc;} + if (last) + return doc; + doc = doc[keypart]; } From 7233d7aabbe4e7c386ca799502d1685ce8f6bf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 19:51:43 +0200 Subject: [PATCH 17/28] Refactored Matcher. --- packages/minimongo/matcher.js | 147 +++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 46 deletions(-) diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index 41367735ec..a119f09c82 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -54,7 +54,8 @@ export default class Matcher { } documentMatches(doc) { - if (doc !== Object(doc)) {throw Error('documentMatches needs a document');} + if (doc !== Object(doc)) + throw Error('documentMatches needs a document'); return this._docMatcher(doc); } @@ -79,6 +80,7 @@ export default class Matcher { this._isSimple = false; this._selector = selector; this._recordPathUsed(''); + return doc => ({result: !!selector.call(doc)}); } @@ -86,6 +88,7 @@ export default class Matcher { if (LocalCollection._selectorIsId(selector)) { this._selector = {_id: selector}; this._recordPathUsed('_id'); + return doc => ({result: EJSON.equals(doc._id, selector)}); } @@ -98,9 +101,8 @@ export default class Matcher { } // Top level can't be an array or true or binary. - if (Array.isArray(selector) || - EJSON.isBinary(selector) || - typeof selector === 'boolean') {throw new Error(`Invalid selector: ${selector}`);} + if (Array.isArray(selector) || EJSON.isBinary(selector) || typeof selector === 'boolean') + throw new Error(`Invalid selector: ${selector}`); this._selector = EJSON.clone(selector); @@ -121,21 +123,40 @@ export default class Matcher { // helpers used by compiled selector code LocalCollection._f = { // XXX for _all and _in, consider building 'inquery' at compile time.. - _type(v) { - if (typeof v === 'number') {return 1;} - if (typeof v === 'string') {return 2;} - if (typeof v === 'boolean') {return 8;} - if (Array.isArray(v)) {return 4;} - if (v === null) {return 10;} - if (v instanceof RegExp) + if (typeof v === 'number') + return 1; + + if (typeof v === 'string') + return 2; + + if (typeof v === 'boolean') + return 8; + + if (Array.isArray(v)) + return 4; + + if (v === null) + return 10; + // note that typeof(/x/) === "object" - {return 11;} - if (typeof v === 'function') {return 13;} - if (v instanceof Date) {return 9;} - if (EJSON.isBinary(v)) {return 5;} - if (v instanceof MongoID.ObjectID) {return 7;} - return 3; // object + if (v instanceof RegExp) + return 11; + + if (typeof v === 'function') + return 13; + + if (v instanceof Date) + return 9; + + if (EJSON.isBinary(v)) + return 5; + + if (v instanceof MongoID.ObjectID) + return 7; + + // object + return 3; // XXX support some/all of these: // 14, symbol @@ -151,14 +172,14 @@ LocalCollection._f = { return EJSON.equals(a, b, {keyOrderSensitive: true}); }, - // maps a type code to a value that can be used to sort values of - // different types + // maps a type code to a value that can be used to sort values of different types _typeorder(t) { // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types // XXX what is the correct sort position for Javascript code? // ('100' in the matrix below) // XXX minkey/maxkey - return [-1, // (not a type) + return [ + -1, // (not a type) 1, // number 2, // string 3, // object @@ -176,7 +197,7 @@ LocalCollection._f = { 100, // JS code 1, // 32-bit int 8, // Mongo timestamp - 1, // 64-bit int + 1 // 64-bit int ][t]; }, @@ -185,23 +206,32 @@ LocalCollection._f = { // any other value.) return negative if a is less, positive if b is // less, or 0 if equal _cmp(a, b) { - if (a === undefined) {return b === undefined ? 0 : -1;} - if (b === undefined) {return 1;} + if (a === undefined) + return b === undefined ? 0 : -1; + + if (b === undefined) + return 1; + let ta = LocalCollection._f._type(a); let tb = LocalCollection._f._type(b); + const oa = LocalCollection._f._typeorder(ta); const ob = LocalCollection._f._typeorder(tb); - if (oa !== ob) {return oa < ob ? -1 : 1;} + + if (oa !== ob) + return oa < ob ? -1 : 1; + + // XXX need to implement this if we implement Symbol or integers, or Timestamp if (ta !== tb) - // XXX need to implement this if we implement Symbol or integers, or - // Timestamp - {throw Error('Missing type coercion logic in _cmp');} + throw Error('Missing type coercion logic in _cmp'); + if (ta === 7) { // ObjectID // Convert to string. ta = tb = 2; a = a.toHexString(); b = b.toHexString(); } + if (ta === 9) { // Date // Convert to millis. ta = tb = 1; @@ -210,47 +240,71 @@ LocalCollection._f = { } if (ta === 1) // double - {return a - b;} + return a - b; + if (tb === 2) // string - {return a < b ? -1 : a === b ? 0 : 1;} + return a < b ? -1 : a === b ? 0 : 1; + if (ta === 3) { // Object // this could be much more efficient in the expected case ... - const to_array = obj => { - const ret = []; - for (let key in obj) { - ret.push(key); - ret.push(obj[key]); + const toArray = object => { + const result = []; + + for (let key in object) { + result.push(key); + result.push(object[key]); } - return ret; + + return result; }; - return LocalCollection._f._cmp(to_array(a), to_array(b)); + + return LocalCollection._f._cmp(toArray(a), toArray(b)); } + if (ta === 4) { // Array for (let i = 0; ; i++) { - if (i === a.length) {return i === b.length ? 0 : -1;} - if (i === b.length) {return 1;} + if (i === a.length) + return i === b.length ? 0 : -1; + + if (i === b.length) + return 1; + const s = LocalCollection._f._cmp(a[i], b[i]); - if (s !== 0) {return s;} + if (s !== 0) + return s; } } + if (ta === 5) { // binary // Surprisingly, a small binary blob is always less than a large one in // Mongo. - if (a.length !== b.length) {return a.length - b.length;} + if (a.length !== b.length) + return a.length - b.length; + for (let i = 0; i < a.length; i++) { - if (a[i] < b[i]) {return -1;} - if (a[i] > b[i]) {return 1;} + if (a[i] < b[i]) + return -1; + + if (a[i] > b[i]) + return 1; } + return 0; } + if (ta === 8) { // boolean - if (a) return b ? 0 : 1; + if (a) + return b ? 0 : 1; + return b ? -1 : 0; } + if (ta === 10) // null - {return 0;} + return 0; + if (ta === 11) // regexp - {throw Error('Sorting not supported on regular expression');} // XXX + throw Error('Sorting not supported on regular expression'); // XXX + // 13: javascript code // 14: symbol // 15: javascript code with scope @@ -260,7 +314,8 @@ LocalCollection._f = { // 255: minkey // 127: maxkey if (ta === 13) // javascript code - {throw Error('Sorting not supported on Javascript code');} // XXX + throw Error('Sorting not supported on Javascript code'); // XXX + throw Error('Unknown type to sort'); }, }; From df15f09d6a69d7ecdccbef1f0deeeba67f34a825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 20:03:31 +0200 Subject: [PATCH 18/28] Refactored server. --- packages/minimongo/minimongo_server.js | 175 +++++++++++++++---------- 1 file changed, 105 insertions(+), 70 deletions(-) diff --git a/packages/minimongo/minimongo_server.js b/packages/minimongo/minimongo_server.js index f4edf32810..f83147cbab 100644 --- a/packages/minimongo/minimongo_server.js +++ b/packages/minimongo/minimongo_server.js @@ -6,9 +6,7 @@ import { projectionDetails, } from './common.js'; -Minimongo._pathsElidingNumericKeys = function(paths) { - return paths.map(path => path.split('.').filter(part => !isNumericKey(part)).join('.')); -}; +Minimongo._pathsElidingNumericKeys = paths => paths.map(path => path.split('.').filter(part => !isNumericKey(part)).join('.')); // Returns true if the modifier applied to some document may change the result // of matching the document by selector @@ -20,27 +18,40 @@ Minimongo._pathsElidingNumericKeys = function(paths) { // - 'abc.d': 1 Minimongo.Matcher.prototype.affectedByModifier = function(modifier) { // safe check for $set/$unset being objects - modifier = Object.assign({ $set: {}, $unset: {} }, modifier); + modifier = Object.assign({$set: {}, $unset: {}}, modifier); + const modifiedPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); const meaningfulPaths = this._getPaths(); return modifiedPaths.some(path => { const mod = path.split('.'); + return meaningfulPaths.some(meaningfulPath => { const sel = meaningfulPath.split('.'); + let i = 0, j = 0; while (i < sel.length && j < mod.length) { if (isNumericKey(sel[i]) && isNumericKey(mod[j])) { // foo.4.bar selector affected by foo.4 modifier // foo.3.bar selector unaffected by foo.4 modifier - if (sel[i] === mod[j]) {i++, j++;} else {return false;} + if (sel[i] === mod[j]) { + i++; + j++; + } else { + return false; + } } else if (isNumericKey(sel[i])) { // foo.4.bar selector unaffected by foo.bar modifier return false; } else if (isNumericKey(mod[j])) { j++; - } else if (sel[i] === mod[j]) {i++, j++;} else {return false;} + } else if (sel[i] === mod[j]) { + i++; + j++; + } else { + return false; + } } // One is a prefix of another, taking numeric fields into account @@ -58,15 +69,18 @@ Minimongo.Matcher.prototype.affectedByModifier = function(modifier) { // stay 'false'. // Currently doesn't support $-operators and numeric indices precisely. Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { - if (!this.affectedByModifier(modifier)) {return false;} + if (!this.affectedByModifier(modifier)) + return false; + + if (!this.isSimple()) + return true; modifier = Object.assign({$set: {}, $unset: {}}, modifier); + const modifierPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); - if (!this.isSimple()) {return true;} - - if (this._getPaths().some(pathHasNumericKeys) || - modifierPaths.some(pathHasNumericKeys)) {return true;} + if (this._getPaths().some(pathHasNumericKeys) || modifierPaths.some(pathHasNumericKeys)) + return true; // check if there is a $set or $unset that indicates something is an // object rather than a scalar in the actual object where we saw $-operator @@ -74,12 +88,14 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { // Example: for selector {'a.b': {$gt: 5}} the modifier {'a.b.c':7} would // definitely set the result to false as 'a.b' appears to be an object. const expectedScalarIsObject = Object.keys(this._selector).some(path => { - const sel = this._selector[path]; - if (! isOperatorObject(sel)) {return false;} - return modifierPaths.some(modifierPath => startsWith(modifierPath, `${path}.`)); + if (!isOperatorObject(this._selector[path])) + return false; + + return modifierPaths.some(modifierPath => modifierPath.startsWith(`${path}.`)); }); - if (expectedScalarIsObject) {return false;} + if (expectedScalarIsObject) + return false; // See if we can apply the modifier on the ideally matching object. If it // still matches the selector, then the modifier could have turned the real @@ -87,11 +103,12 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { const matchingDocument = EJSON.clone(this.matchingDocument()); // The selector is too complex, anything can happen. - if (matchingDocument === null) {return true;} + if (matchingDocument === null) + return true; try { LocalCollection._modify(matchingDocument, modifier); - } catch (e) { + } catch (error) { // Couldn't set a property on a field which is a scalar or null in the // selector. // Example: @@ -102,8 +119,10 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { // We don't know what real document was like but from the error raised by // $set on a scalar field we can reason that the structure of real document // is completely different. - if (e.name === 'MinimongoError' && e.setPropertyError) {return false;} - throw e; + if (error.name === 'MinimongoError' && error.setPropertyError) + return false; + + throw error; } return this.documentMatches(matchingDocument).result; @@ -119,7 +138,8 @@ Minimongo.Matcher.prototype.combineIntoProjection = function(projection) { // on all fields of the document. getSelectorPaths returns a list of paths // selector depends on. If one of the paths is '' (empty string) representing // the root or the whole document, complete projection should be returned. - if (selectorPaths.includes('')) {return {};} + if (selectorPaths.includes('')) + return {}; return combineImportantPathsIntoProjection(selectorPaths, projection); }; @@ -130,55 +150,73 @@ Minimongo.Matcher.prototype.combineIntoProjection = function(projection) { // => { a: { b: { ans: 42 } }, foo: { bar: null, baz: "something" } } Minimongo.Matcher.prototype.matchingDocument = function() { // check if it was computed before - if (this._matchingDocument !== undefined) {return this._matchingDocument;} + if (this._matchingDocument !== undefined) + return this._matchingDocument; - // If the analysis of this selector is too hard for our implementation - // fallback to "YES" + // If the analysis of this selector is too hard for our implementation fallback to "YES" let fallback = false; - this._matchingDocument = pathsToTree(this._getPaths(), + + this._matchingDocument = pathsToTree( + this._getPaths(), path => { const valueSelector = this._selector[path]; + if (isOperatorObject(valueSelector)) { // if there is a strict equality, there is a good // chance we can use one of those as "matching" // dummy value if (valueSelector.$eq) { return valueSelector.$eq; - } else if (valueSelector.$in) { - const matcher = new Minimongo.Matcher({ placeholder: valueSelector }); + } + + if (valueSelector.$in) { + const matcher = new Minimongo.Matcher({placeholder: valueSelector}); // Return anything from $in that matches the whole selector for this // path. If nothing matches, returns `undefined` as nothing can make // this selector into `true`. - return valueSelector.$in.find(x => matcher.documentMatches({ placeholder: x }).result); - } else if (onlyContainsKeys(valueSelector, ['$gt', '$gte', '$lt', '$lte'])) { - let lowerBound = -Infinity, upperBound = Infinity; + return valueSelector.$in.find(placeholder => matcher.documentMatches({placeholder}).result); + } + + if (onlyContainsKeys(valueSelector, ['$gt', '$gte', '$lt', '$lte'])) { + let lowerBound = -Infinity; + let upperBound = Infinity; + ['$lte', '$lt'].forEach(op => { - if (valueSelector.hasOwnProperty(op) && valueSelector[op] < upperBound) {upperBound = valueSelector[op];} + if (valueSelector.hasOwnProperty(op) && valueSelector[op] < upperBound) + upperBound = valueSelector[op]; }); + ['$gte', '$gt'].forEach(op => { - if (valueSelector.hasOwnProperty(op) && valueSelector[op] > lowerBound) {lowerBound = valueSelector[op];} + if (valueSelector.hasOwnProperty(op) && valueSelector[op] > lowerBound) + lowerBound = valueSelector[op]; }); const middle = (lowerBound + upperBound) / 2; - const matcher = new Minimongo.Matcher({ placeholder: valueSelector }); - if (!matcher.documentMatches({ placeholder: middle }).result && - (middle === lowerBound || middle === upperBound)) {fallback = true;} + const matcher = new Minimongo.Matcher({placeholder: valueSelector}); + + if (!matcher.documentMatches({placeholder: middle}).result && (middle === lowerBound || middle === upperBound)) + fallback = true; return middle; - } else if (onlyContainsKeys(valueSelector, ['$nin', '$ne'])) { + } + + if (onlyContainsKeys(valueSelector, ['$nin', '$ne'])) { // Since this._isSimple makes sure $nin and $ne are not combined with // objects or arrays, we can confidently return an empty object as it // never matches any scalar. return {}; } + fallback = true; } + return this._selector[path]; }, x => x); - if (fallback) {this._matchingDocument = null;} + if (fallback) + this._matchingDocument = null; return this._matchingDocument; }; @@ -190,51 +228,51 @@ Minimongo.Sorter.prototype.affectedByModifier = function(modifier) { }; Minimongo.Sorter.prototype.combineIntoProjection = function(projection) { - const specPaths = Minimongo._pathsElidingNumericKeys(this._getPaths()); - return combineImportantPathsIntoProjection(specPaths, projection); + return combineImportantPathsIntoProjection(Minimongo._pathsElidingNumericKeys(this._getPaths()), projection); }; function combineImportantPathsIntoProjection(paths, projection) { - const prjDetails = projectionDetails(projection); - let tree = prjDetails.tree; - let mergedProjection = {}; + const details = projectionDetails(projection); // merge the paths to include - tree = pathsToTree(paths, - path => true, - (node, path, fullPath) => true, - tree); - mergedProjection = treeToPaths(tree); - if (prjDetails.including) { + const tree = pathsToTree(paths, path => true, (node, path, fullPath) => true, details.tree); + const mergedProjection = treeToPaths(tree); + + if (details.including) { // both selector and projection are pointing on fields to include // so we can just return the merged tree return mergedProjection; } + // selector is pointing at fields to include // projection is pointing at fields to exclude // make sure we don't exclude important paths const mergedExclProjection = {}; + Object.keys(mergedProjection).forEach(path => { - const incl = mergedProjection[path]; - if (!incl) {mergedExclProjection[path] = false;} + if (!mergedProjection[path]) + mergedExclProjection[path] = false; }); return mergedExclProjection; } -function getPaths(sel) { - return Object.keys(new Minimongo.Matcher(sel)._paths); - return Object.keys(sel).map(k => { - const v = sel[k]; - // we don't know how to handle $where because it can be anything - if (k === '$where') {return '';} // matches everything - // we branch from $or/$and/$nor operator - if (['$or', '$and', '$nor'].includes(k)) {return v.map(getPaths);} - // the value is a literal or some comparison operator - return k; - }) - .reduce((a, b) => a.concat(b), []) - .filter((a, b, c) => c.indexOf(a) === b); +function getPaths(selector) { + return Object.keys(new Minimongo.Matcher(selector)._paths); + + // XXX remove it? + // return Object.keys(selector).map(k => { + // // we don't know how to handle $where because it can be anything + // if (k === '$where') + // return ''; // matches everything + + // // we branch from $or/$and/$nor operator + // if (['$or', '$and', '$nor'].includes(k)) + // return selector[k].map(getPaths); + + // // the value is a literal or some comparison operator + // return k; + // }).reduce((a, b) => a.concat(b), []).filter((a, b, c) => c.indexOf(a) === b); } // A helper to ensure object has only certain keys @@ -246,20 +284,17 @@ function pathHasNumericKeys(path) { return path.split('.').some(isNumericKey); } -// XXX from Underscore.String (http://epeli.github.com/underscore.string/) -function startsWith(str, starts) { - return str.length >= starts.length && - str.substring(0, starts.length) === starts; -} - // Returns a set of key paths similar to // { 'foo.bar': 1, 'a.b.c': 1 } function treeToPaths(tree, prefix = '') { const result = {}; Object.keys(tree).forEach(key => { - const val = tree[key]; - if (val === Object(val)) {Object.assign(result, treeToPaths(val, `${prefix + key}.`));} else {result[prefix + key] = val;} + const value = tree[key]; + if (value === Object(value)) + Object.assign(result, treeToPaths(value, `${prefix + key}.`)); + else + result[prefix + key] = value; }); return result; From 7c45a9c91219367a43ced061c2217842cd8b888e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 12 Jul 2017 20:14:13 +0200 Subject: [PATCH 19/28] Refactored Sorter. --- packages/minimongo/sorter.js | 175 +++++++++++++++++++++-------------- 1 file changed, 103 insertions(+), 72 deletions(-) diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index 713b2002e1..7130697dad 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -26,27 +26,26 @@ export default class Sorter { this._sortFunction = null; const addSpecPart = (path, ascending) => { - if (!path) {throw Error('sort keys must be non-empty');} - if (path.charAt(0) === '$') {throw Error(`unsupported sort key: ${path}`);} - this._sortSpecParts.push({ - path, - lookup: makeLookupFunction(path, {forSort: true}), - ascending, - }); + if (!path) + throw Error('sort keys must be non-empty'); + + if (path.charAt(0) === '$') + throw Error(`unsupported sort key: ${path}`); + + this._sortSpecParts.push({ascending, lookup: makeLookupFunction(path, {forSort: true}), path}); }; if (spec instanceof Array) { - for (let i = 0; i < spec.length; i++) { - if (typeof spec[i] === 'string') { - addSpecPart(spec[i], true); + spec.forEach(element => { + if (typeof element === 'string') { + addSpecPart(element, true); } else { - addSpecPart(spec[i][0], spec[i][1] !== 'desc'); + addSpecPart(element[0], element[1] !== 'desc'); } - } + }); } else if (typeof spec === 'object') { Object.keys(spec).forEach(key => { - const value = spec[key]; - addSpecPart(key, value >= 0); + addSpecPart(key, spec[key] >= 0); }); } else if (typeof spec === 'function') { this._sortFunction = spec; @@ -55,27 +54,31 @@ export default class Sorter { } // If a function is specified for sorting, we skip the rest. - if (this._sortFunction) {return;} + if (this._sortFunction) + return; // To implement affectedByModifier, we piggy-back on top of Matcher's // affectedByModifier code; we create a selector that is affected by the same // modifiers as this sort order. This is only implemented on the server. if (this.affectedByModifier) { const selector = {}; + this._sortSpecParts.forEach(spec => { selector[spec.path] = 1; }); + this._selectorForAffectedByModifier = new Minimongo.Matcher(selector); } - this._keyComparator = composeComparators( - this._sortSpecParts.map((spec, i) => this._keyFieldComparator(i))); + this._keyComparator = composeComparators(this._sortSpecParts.map((spec, i) => this._keyFieldComparator(i))); // If you specify a matcher for this Sorter, _keyFilter may be set to a // function which selects whether or not a given "sort key" (tuple of values // for the different sort spec fields) is compatible with the selector. this._keyFilter = null; - options.matcher && this._useWithMatcher(options.matcher); + + if (options.matcher) + this._useWithMatcher(options.matcher); } getComparator(options) { @@ -84,16 +87,19 @@ export default class Sorter { // issue #3599 // https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation // sort effectively overrides $near - if (this._sortSpecParts.length || !options || !options.distances) { + if (this._sortSpecParts.length || !options || !options.distances) return this._getBaseComparator(); - } const distances = options.distances; // Return a comparator which compares using $near distances. return (a, b) => { - if (!distances.has(a._id)) {throw Error(`Missing distance for ${a._id}`);} - if (!distances.has(b._id)) {throw Error(`Missing distance for ${b._id}`);} + if (!distances.has(a._id)) + throw Error(`Missing distance for ${a._id}`); + + if (!distances.has(b._id)) + throw Error(`Missing distance for ${b._id}`); + return distances.get(a._id) - distances.get(b._id); }; } @@ -102,10 +108,8 @@ export default class Sorter { // parts. Returns negative, 0, or positive based on using the sort spec to // compare fields. _compareKeys(key1, key2) { - if (key1.length !== this._sortSpecParts.length || - key2.length !== this._sortSpecParts.length) { + if (key1.length !== this._sortSpecParts.length || key2.length !== this._sortSpecParts.length) throw Error('Key has wrong length'); - } return this._keyComparator(key1, key2); } @@ -113,40 +117,47 @@ export default class Sorter { // Iterates over each possible "key" from doc (ie, over each branch), calling // 'cb' with the key. _generateKeysFromDoc(doc, cb) { - if (this._sortSpecParts.length === 0) {throw new Error("can't generate keys without a spec");} - - // maps index -> ({'' -> value} or {path -> value}) - const valuesByIndexAndPath = []; + if (this._sortSpecParts.length === 0) + throw new Error('can\'t generate keys without a spec'); const pathFromIndices = indices => `${indices.join(',')},`; let knownPaths = null; - this._sortSpecParts.forEach((spec, whichField) => { + // maps index -> ({'' -> value} or {path -> value}) + const valuesByIndexAndPath = this._sortSpecParts.map(spec => { // Expand any leaf arrays that we find, and ignore those arrays // themselves. (We never sort based on an array itself.) let branches = expandArraysInBranches(spec.lookup(doc), true); // If there are no values for a key (eg, key goes to an empty array), // pretend we found one null value. - if (!branches.length) {branches = [{value: null}];} + if (!branches.length) + branches = [{value: null}]; + const element = {}; let usedPaths = false; - valuesByIndexAndPath[whichField] = {}; + branches.forEach(branch => { if (!branch.arrayIndices) { // If there are no array indices for a branch, then it must be the // only branch, because the only thing that produces multiple branches // is the use of arrays. - if (branches.length > 1) {throw Error('multiple branches but no array used?');} - valuesByIndexAndPath[whichField][''] = branch.value; + if (branches.length > 1) + throw Error('multiple branches but no array used?'); + + element[''] = branch.value; return; } usedPaths = true; + const path = pathFromIndices(branch.arrayIndices); - if (valuesByIndexAndPath[whichField].hasOwnProperty(path)) {throw Error(`duplicate path: ${path}`);} - valuesByIndexAndPath[whichField][path] = branch.value; + + if (element.hasOwnProperty(path)) + throw Error(`duplicate path: ${path}`); + + element[path] = branch.value; // If two sort fields both go into arrays, they have to go into the // exact same arrays and we have to find the same paths. This is @@ -158,42 +169,51 @@ export default class Sorter { // and 'a.x.y' are both arrays, but we don't allow this for now. // #NestedArraySort // XXX achieve full compatibility here - if (knownPaths && !knownPaths.hasOwnProperty(path)) { + if (knownPaths && !knownPaths.hasOwnProperty(path)) throw Error('cannot index parallel arrays'); - } }); if (knownPaths) { // Similarly to above, paths must match everywhere, unless this is a // non-array field. - if (!valuesByIndexAndPath[whichField].hasOwnProperty('') && - Object.keys(knownPaths).length !== Object.keys(valuesByIndexAndPath[whichField]).length) { + if (!element.hasOwnProperty('') && Object.keys(knownPaths).length !== Object.keys(element).length) throw Error('cannot index parallel arrays!'); - } } else if (usedPaths) { knownPaths = {}; - Object.keys(valuesByIndexAndPath[whichField]).forEach(path => { + + Object.keys(element).forEach(path => { knownPaths[path] = true; }); } + + return element; }); if (!knownPaths) { // Easy case: no use of arrays. const soleKey = valuesByIndexAndPath.map(values => { - if (!values.hasOwnProperty('')) {throw Error('no value in sole key case?');} + if (!values.hasOwnProperty('')) + throw Error('no value in sole key case?'); + return values['']; }); + cb(soleKey); + return; } Object.keys(knownPaths).forEach(path => { const key = valuesByIndexAndPath.map(values => { - if (values.hasOwnProperty('')) {return values[''];} - if (!values.hasOwnProperty(path)) {throw Error('missing path?');} + if (values.hasOwnProperty('')) + return values['']; + + if (!values.hasOwnProperty(path)) + throw Error('missing path?'); + return values[path]; }); + cb(key); }); } @@ -201,13 +221,13 @@ export default class Sorter { // Returns a comparator that represents the sort specification (but not // including a possible geoquery distance tie-breaker). _getBaseComparator() { - if (this._sortFunction) {return this._sortFunction;} + if (this._sortFunction) + return this._sortFunction; // If we're only sorting on geoquery distance and no specs, just say // everything is equal. - if (!this._sortSpecParts.length) { + if (!this._sortSpecParts.length) return (doc1, doc2) => 0; - } return (doc1, doc2) => { const key1 = this._getMinKeyFromDoc(doc1); @@ -230,20 +250,23 @@ export default class Sorter { let minKey = null; this._generateKeysFromDoc(doc, key => { - if (!this._keyCompatibleWithSelector(key)) {return;} + if (!this._keyCompatibleWithSelector(key)) + return; if (minKey === null) { minKey = key; return; } - if (this._compareKeys(key, minKey) < 0) { + + if (this._compareKeys(key, minKey) < 0) minKey = key; - } }); // This could happen if our key filter somehow filters out all the keys even // though somehow the selector matches. - if (minKey === null) {throw Error('sort selector found no keys in doc?');} + if (minKey === null) + throw Error('sort selector found no keys in doc?'); + return minKey; } @@ -259,10 +282,10 @@ export default class Sorter { // on field 'i'. _keyFieldComparator(i) { const invert = !this._sortSpecParts[i].ascending; + return (key1, key2) => { - let compare = LocalCollection._f._cmp(key1[i], key2[i]); - if (invert) {compare = -compare;} - return compare; + const compare = LocalCollection._f._cmp(key1[i], key2[i]); + return invert ? -compare : compare; }; } @@ -286,30 +309,35 @@ export default class Sorter { // subtle and undocumented; we've gotten as close as we can figure out based // on our understanding of Mongo's behavior. _useWithMatcher(matcher) { - if (this._keyFilter) {throw Error('called _useWithMatcher twice?');} + if (this._keyFilter) + throw Error('called _useWithMatcher twice?'); // If we are only sorting by distance, then we're not going to bother to // build a key filter. // XXX figure out how geoqueries interact with this stuff - if (!this._sortSpecParts.length) {return;} + if (!this._sortSpecParts.length) + return; const selector = matcher._selector; // If the user just passed a literal function to find(), then we can't get a // key filter from it. - if (selector instanceof Function) {return;} + if (selector instanceof Function) + return; const constraintsByPath = {}; - this._sortSpecParts.forEach((spec, i) => { + + this._sortSpecParts.forEach(spec => { constraintsByPath[spec.path] = []; }); Object.keys(selector).forEach(key => { const subSelector = selector[key]; - // XXX support $and and $or + // XXX support $and and $or const constraints = constraintsByPath[key]; - if (!constraints) {return;} + if (!constraints) + return; // XXX it looks like the real MongoDB implementation isn't "does the // regexp match" but "does the value fall into a range named by the @@ -322,7 +350,9 @@ export default class Sorter { // index to use, which means it only cares about regexps that match // one range (with a literal prefix), and both 'i' and 'm' prevent the // literal prefix of the regexp from actually meaning one range. - if (subSelector.ignoreCase || subSelector.multiline) {return;} + if (subSelector.ignoreCase || subSelector.multiline) + return; + constraints.push(regexpElementMatcher(subSelector)); return; } @@ -330,22 +360,20 @@ export default class Sorter { if (isOperatorObject(subSelector)) { Object.keys(subSelector).forEach(operator => { const operand = subSelector[operator]; + if (['$lt', '$lte', '$gt', '$gte'].includes(operator)) { // XXX this depends on us knowing that these operators don't use any // of the arguments to compileElementSelector other than operand. - constraints.push( - ELEMENT_OPERATORS[operator].compileElementSelector(operand)); + constraints.push(ELEMENT_OPERATORS[operator].compileElementSelector(operand)); } // See comments in the RegExp block above. - if (operator === '$regex' && !subSelector.$options) { - constraints.push( - ELEMENT_OPERATORS.$regex.compileElementSelector( - operand, subSelector)); - } + if (operator === '$regex' && !subSelector.$options) + constraints.push(ELEMENT_OPERATORS.$regex.compileElementSelector(operand, subSelector)); // XXX support {$exists: true}, $mod, $type, $in, $elemMatch }); + return; } @@ -357,9 +385,10 @@ export default class Sorter { // others; we shouldn't create a key filter unless the first sort field is // restricted, though after that point we can restrict the other sort fields // or not as we wish. - if (!constraintsByPath[this._sortSpecParts[0].path].length) {return;} + if (!constraintsByPath[this._sortSpecParts[0].path].length) + return; - this._keyFilter = key => this._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(f => f(key[index]))); + this._keyFilter = key => this._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(fn => fn(key[index]))); } } @@ -371,8 +400,10 @@ function composeComparators(comparatorArray) { return (a, b) => { for (let i = 0; i < comparatorArray.length; ++i) { const compare = comparatorArray[i](a, b); - if (compare !== 0) {return compare;} + if (compare !== 0) + return compare; } + return 0; }; } From 92555c32ba40e969a0bd905975961326d2365f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Thu, 13 Jul 2017 16:46:08 +0200 Subject: [PATCH 20/28] Further small optimizations. --- packages/minimongo/cursor.js | 31 +++++++++++++------------------ packages/minimongo/matcher.js | 10 ++++++---- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index b649818926..786cfc9632 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -9,12 +9,9 @@ export default class Cursor { this.sorter = null; this.matcher = new Minimongo.Matcher(selector); - if (LocalCollection._selectorIsId(selector)) { - // stash for fast path - this._selectorId = selector; - } else if (LocalCollection._selectorIsIdPerhapsAsObject(selector)) { - // also do the fast path for { _id: idString } - this._selectorId = selector._id; + if (LocalCollection._selectorIsIdPerhapsAsObject(selector)) { + // stash for fast _id and { _id } + this._selectorId = selector._id || selector; } else { this._selectorId = undefined; @@ -83,8 +80,6 @@ export default class Cursor { * @param {Any} [thisArg] An object which will be the value of `this` inside `callback`. */ forEach(callback, thisArg) { - const objects = this._getRawObjects({ordered: true}); - if (this.reactive) { this._depend({ addedBefore: true, @@ -93,7 +88,7 @@ export default class Cursor { movedBefore: true}); } - objects.forEach((element, i) => { + this._getRawObjects({ordered: true}).forEach((element, i) => { // This doubles as a clone operation. element = this._projectionFn(element); @@ -174,20 +169,20 @@ export default class Cursor { // comment in _modifyAndNotify // XXX allow skip/limit with unordered observe if (!options._allow_unordered && !ordered && (this.skip || this.limit)) - throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit"); + throw new Error('must use ordered observe (ie, \'addedBefore\' instead of \'added\') with skip or limit'); if (this.fields && (this.fields._id === 0 || this.fields._id === false)) throw Error('You may not observe a cursor with {fields: {_id: 0}}'); const query = { - dirty: false, - matcher: this.matcher, // not fast pathed - sorter: ordered && this.sorter, - distances: this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap, - resultsSnapshot: null, - ordered, cursor: this, + dirty: false, + distances: this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap, + matcher: this.matcher, // not fast pathed + ordered, projectionFn: this._projectionFn, + resultsSnapshot: null, + sorter: ordered && this.sorter }; let qid; @@ -217,11 +212,11 @@ export default class Cursor { const self = this; return function(/* args*/) { - const args = arguments; - if (self.collection.paused) return; + const args = arguments; + self.collection._observeQueue.queueTask(() => { fn.apply(this, args); }); diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index a119f09c82..926610453a 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -84,12 +84,14 @@ export default class Matcher { return doc => ({result: !!selector.call(doc)}); } - // shorthand -- scalars match _id - if (LocalCollection._selectorIsId(selector)) { - this._selector = {_id: selector}; + // shorthand -- scalar _id and {_id} + if (LocalCollection._selectorIsIdPerhapsAsObject(selector)) { + const _id = selector._id || selector; + + this._selector = {_id}; this._recordPathUsed('_id'); - return doc => ({result: EJSON.equals(doc._id, selector)}); + return doc => ({result: EJSON.equals(doc._id, _id)}); } // protect against dangerous selectors. falsey and {_id: falsey} are both From a082de950aa0946eb5cb3de30d146bd1edbb2686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Thu, 13 Jul 2017 20:23:22 +0200 Subject: [PATCH 21/28] Short path for {_id} selectors (!). --- packages/minimongo/local_collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 95634e011e..b2e29abd85 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1200,7 +1200,7 @@ LocalCollection._selectorIsIdPerhapsAsObject = selector => { if (LocalCollection._selectorIsId(selector)) return true; - return LocalCollection._selectorIsId(selector && selector._id) && Object.keys(selector).length === 1; + return LocalCollection._selectorIsId(selector && selector._id); }; LocalCollection._updateInResults = (query, doc, old_doc) => { From f5b0de54173216047ea36df009edf8dd0081bebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Thu, 13 Jul 2017 21:14:26 +0200 Subject: [PATCH 22/28] Normalized strings. --- packages/minimongo/common.js | 4 ++-- packages/minimongo/local_collection.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index 509b0ba719..8697febc8b 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -308,7 +308,7 @@ const VALUE_OPERATORS = { }, $near(operand, valueSelector, matcher, isRoot) { if (!isRoot) - throw Error("$near can't be inside another $ operator"); + throw Error('$near can\'t be inside another $ operator'); matcher._hasGeoQuery = true; @@ -549,7 +549,7 @@ function distanceCoordinatePairs(a, b) { // for equality with that thing. export function equalityElementMatcher(elementSelector) { if (isOperatorObject(elementSelector)) - throw Error("Can't create equalityValueSelector for operator object"); + throw Error('Can\'t create equalityValueSelector for operator object'); // Special-case: null and undefined are equal (if you got undefined in there // somewhere, or if you got it due to some branch being non-existent in the diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index b2e29abd85..b29ab44a5b 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -717,7 +717,7 @@ LocalCollection.wrapTransform = transform => { if (transformed.hasOwnProperty('_id')) { if (!EJSON.equals(transformed._id, id)) - throw new Error("transformed document can't have different _id"); + throw new Error('transformed document can\'t have different _id'); } else { transformed._id = id; } From 97f1234e9957cecb65b17edf7a1a202362e893eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 19 Jul 2017 21:11:04 +0200 Subject: [PATCH 23/28] Fixed failing test. --- packages/minimongo/local_collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 1448e12055..a3f0fb16f3 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1049,7 +1049,7 @@ LocalCollection._modify = (doc, modifier, options = {}) => { if (doc._id && !EJSON.equals(doc._id, newDoc._id)) throw MinimongoError(`After applying the update to the document {_id: "${doc._id}", ...}, the (immutable) field '_id' was found to have been altered to _id: "${newDoc._id}"`); } else { - if (modifier._id && !EJSON.equals(doc._id, modifier._id)) + if (doc._id && modifier._id && !EJSON.equals(doc._id, modifier._id)) throw MinimongoError(`The _id field cannot be changed from {_id: "${doc._id}"} to {_id: "${modifier._id}"}`); // replace the whole document From b5071495ccbdd237cd9ad8cacf76b192b5fec996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 19 Jul 2017 22:20:51 +0200 Subject: [PATCH 24/28] Fixed failing test. --- packages/minimongo/local_collection.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index a3f0fb16f3..16727489a1 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1034,8 +1034,6 @@ LocalCollection._modify = (doc, modifier, options = {}) => { if (!keyparts.every(Boolean)) throw MinimongoError(`The update path '${keypath}' contains an empty field name, which is not allowed.`); - const noCreate = NO_CREATE_MODIFIERS.hasOwnProperty(operator); - const forbidArray = operator === '$rename'; const target = findModTarget(newDoc, keyparts, { arrayIndices: options.arrayIndices, forbidArray: operator === '$rename', @@ -1214,7 +1212,7 @@ LocalCollection._selectorIsIdPerhapsAsObject = selector => { if (LocalCollection._selectorIsId(selector)) return true; - return LocalCollection._selectorIsId(selector && selector._id); + return LocalCollection._selectorIsId(selector && selector._id) && Object.keys(selector).length === 1; }; LocalCollection._updateInResults = (query, doc, old_doc) => { From 0813aa104c9a29726beef98f7045a360d02ee3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Mon, 24 Jul 2017 17:33:13 +0200 Subject: [PATCH 25/28] Wrapped lines at 80. --- packages/minimongo/common.js | 229 ++++++++++---- packages/minimongo/cursor.js | 118 +++++--- packages/minimongo/local_collection.js | 398 ++++++++++++++++++------- packages/minimongo/matcher.js | 10 +- packages/minimongo/minimongo_server.js | 53 +++- packages/minimongo/package.js | 6 +- packages/minimongo/sorter.js | 38 ++- 7 files changed, 620 insertions(+), 232 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index e80a657090..3d3bc499bc 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -27,7 +27,9 @@ export const ELEMENT_OPERATORS = { // XXX could require to be ints or round or something const divisor = operand[0]; const remainder = operand[1]; - return value => typeof value === 'number' && value % divisor === remainder; + return value => ( + typeof value === 'number' && value % divisor === remainder + ); }, }, $in: { @@ -77,7 +79,9 @@ export const ELEMENT_OPERATORS = { compileElementSelector(operand) { if (typeof operand !== 'number') throw Error('$type needs a number'); - return value => value !== undefined && LocalCollection._f._type(value) === operand; + return value => ( + value !== undefined && LocalCollection._f._type(value) === operand + ); }, }, $bitsAllSet: { @@ -132,8 +136,8 @@ export const ELEMENT_OPERATORS = { if (/[^gim]/.test(valueSelector.$options)) throw new Error('Only the i, m, and g regexp options are supported'); - const regexSource = operand instanceof RegExp ? operand.source : operand; - regexp = new RegExp(regexSource, valueSelector.$options); + const source = operand instanceof RegExp ? operand.source : operand; + regexp = new RegExp(source, valueSelector.$options); } else if (operand instanceof RegExp) { regexp = operand; } else { @@ -160,7 +164,8 @@ export const ELEMENT_OPERATORS = { // because of the slightly different calling convention. // {$elemMatch: {x: 3}} means "an element has a field x:3", not // "consists only of a field x:3". Also, regexps and sub-$ are allowed. - subMatcher = compileDocumentSelector(operand, matcher, {inElemMatch: true}); + subMatcher = + compileDocumentSelector(operand, matcher, {inElemMatch: true}); } else { subMatcher = compileValueSelector(operand, matcher); } @@ -198,12 +203,17 @@ export const ELEMENT_OPERATORS = { // Operators that appear at the top level of a document selector. const LOGICAL_OPERATORS = { $and(subSelector, matcher, inElemMatch) { - const matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); - return andDocumentMatchers(matchers); + return andDocumentMatchers( + compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch) + ); }, $or(subSelector, matcher, inElemMatch) { - const matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); + const matchers = compileArrayOfDocumentSelectors( + subSelector, + matcher, + inElemMatch + ); // Special case: if there is only one matcher, use it directly, *preserving* // any arrayIndices it returns. @@ -219,7 +229,11 @@ const LOGICAL_OPERATORS = { }, $nor(subSelector, matcher, inElemMatch) { - const matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch); + const matchers = compileArrayOfDocumentSelectors( + subSelector, + matcher, + inElemMatch + ); return doc => { const result = matchers.every(fn => !fn(doc).result); // Never set arrayIndices, because we only match if nothing in particular @@ -257,19 +271,29 @@ const LOGICAL_OPERATORS = { // convertElementMatcherToBranchedMatcher". const VALUE_OPERATORS = { $eq(operand) { - return convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand)); + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(operand) + ); }, $not(operand, valueSelector, matcher) { return invertBranchedMatcher(compileValueSelector(operand, matcher)); }, $ne(operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand))); + return invertBranchedMatcher( + convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand)) + ); }, $nin(operand) { - return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(ELEMENT_OPERATORS.$in.compileElementSelector(operand))); + return invertBranchedMatcher( + convertElementMatcherToBranchedMatcher( + ELEMENT_OPERATORS.$in.compileElementSelector(operand) + ) + ); }, $exists(operand) { - const exists = convertElementMatcherToBranchedMatcher(value => value !== undefined); + const exists = convertElementMatcherToBranchedMatcher( + value => value !== undefined + ); return operand ? exists : invertBranchedMatcher(exists); }, // $options just provides options for $regex; its logic is inside $regex @@ -316,7 +340,8 @@ const VALUE_OPERATORS = { // marked with a $geometry property, though legacy coordinates can be // matched using $geometry. let maxDistance, point, distance; - if (LocalCollection._isPlainObject(operand) && operand.hasOwnProperty('$geometry')) { + if (LocalCollection._isPlainObject(operand) && + operand.hasOwnProperty('$geometry')) { // GeoJSON "2dsphere" mode. maxDistance = operand.$maxDistance; point = operand.$geometry; @@ -326,11 +351,19 @@ const VALUE_OPERATORS = { // a priority. if (!value) return null; + if (!value.type) - return GeoJSON.pointDistance(point, {type: 'Point', coordinates: pointToArray(value)}); + return GeoJSON.pointDistance( + point, + {type: 'Point', coordinates: pointToArray(value)} + ); + if (value.type === 'Point') return GeoJSON.pointDistance(point, value); - return GeoJSON.geometryWithinRadius(value, point, maxDistance) ? 0 : maxDistance + 1; + + return GeoJSON.geometryWithinRadius(value, point, maxDistance) + ? 0 + : maxDistance + 1; }; } else { maxDistance = valueSelector.$maxDistance; @@ -358,7 +391,8 @@ const VALUE_OPERATORS = { // each within-$maxDistance branching point. const result = {result: false}; expandArraysInBranches(branchedValues).every(branch => { - // if operation is an update, don't skip branches, just return the first one (#3599) + // if operation is an update, don't skip branches, just return the first + // one (#3599) let curDistance; if (!matcher._isUpdate) { if (!(typeof branch.value === 'object')) @@ -411,7 +445,9 @@ function andSomeMatchers(subMatchers) { // one. Yes, this means that if there are multiple $near fields in a // query, something arbitrary happens; this appears to be consistent with // Mongo. - if (subResult.result && subResult.distance !== undefined && match.distance === undefined) + if (subResult.result && + subResult.distance !== undefined && + match.distance === undefined) match.distance = subResult.distance; // Similarly, propagate arrayIndices from sub-matchers... but to match @@ -476,7 +512,11 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) { matcher._recordPathUsed(key); const lookUpByIndex = makeLookupFunction(key); - const valueMatcher = compileValueSelector(subSelector, matcher, options.isRoot); + const valueMatcher = compileValueSelector( + subSelector, + matcher, + options.isRoot + ); return doc => valueMatcher(lookUpByIndex(doc)); }); @@ -491,12 +531,17 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) { function compileValueSelector(valueSelector, matcher, isRoot) { if (valueSelector instanceof RegExp) { matcher._isSimple = false; - return convertElementMatcherToBranchedMatcher(regexpElementMatcher(valueSelector)); + return convertElementMatcherToBranchedMatcher( + regexpElementMatcher(valueSelector) + ); } + if (isOperatorObject(valueSelector)) return operatorBranchedMatcher(valueSelector, matcher, isRoot); - return convertElementMatcherToBranchedMatcher(equalityElementMatcher(valueSelector)); + return convertElementMatcherToBranchedMatcher( + equalityElementMatcher(valueSelector) + ); } // Given an element matcher (which evaluates a single value), returns a branched @@ -580,7 +625,10 @@ export function expandArraysInBranches(branches, skipTheArrays) { if (thisIsArray && !branch.dontIterate) { branch.value.forEach((value, i) => { - branchesOut.push({arrayIndices: (branch.arrayIndices || []).concat(i), value}); + branchesOut.push({ + arrayIndices: (branch.arrayIndices || []).concat(i), + value + }); }); } }); @@ -591,7 +639,8 @@ export function expandArraysInBranches(branches, skipTheArrays) { // Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear. function getOperandBitmask(operand, selector) { // numeric bitmask - // You can provide a numeric bitmask to be matched against the operand field. It must be representable as a non-negative 32-bit signed integer. + // You can provide a numeric bitmask to be matched against the operand field. + // It must be representable as a non-negative 32-bit signed integer. // Otherwise, $bitsAllSet will return an error. if (Number.isInteger(operand) && operand >= 0) return new Uint8Array(new Int32Array([operand]).buffer); @@ -602,8 +651,10 @@ function getOperandBitmask(operand, selector) { return new Uint8Array(operand.buffer); // position list - // If querying a list of bit positions, each must be a non-negative integer. Bit positions start at 0 from the least significant bit. - if (Array.isArray(operand) && operand.every(x => Number.isInteger(x) && x >= 0)) { + // If querying a list of bit positions, each must be a non-negative + // integer. Bit positions start at 0 from the least significant bit. + if (Array.isArray(operand) && + operand.every(x => Number.isInteger(x) && x >= 0)) { const buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1); const view = new Uint8Array(buffer); @@ -615,16 +666,26 @@ function getOperandBitmask(operand, selector) { } // bad operand - throw Error(`operand to ${selector} must be a numeric bitmask (representable as a non-negative 32-bit signed integer), a bindata bitmask or an array with bit positions (non-negative integers)`); + throw Error( + `operand to ${selector} must be a numeric bitmask (representable as a ` + + 'non-negative 32-bit signed integer), a bindata bitmask or an array with ' + + 'bit positions (non-negative integers)' + ); } function getValueBitmask(value, length) { - // The field value must be either numerical or a BinData instance. Otherwise, $bits... will not match the current document. + // The field value must be either numerical or a BinData instance. Otherwise, + // $bits... will not match the current document. + // numerical if (Number.isSafeInteger(value)) { - // $bits... will not match numerical values that cannot be represented as a signed 64-bit integer - // This can be the case if a value is either too large or small to fit in a signed 64-bit integer, or if it has a fractional component. - const buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT)); + // $bits... will not match numerical values that cannot be represented as a + // signed 64-bit integer. This can be the case if a value is either too + // large or small to fit in a signed 64-bit integer, or if it has a + // fractional component. + const buffer = new ArrayBuffer( + Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT) + ); let view = new Uint32Array(buffer, 0, 2); view[0] = value % ((1 << 16) * (1 << 16)) | 0; @@ -658,9 +719,14 @@ function insertIntoDocument(document, key, value) { (existingKey.length > key.length && existingKey.indexOf(key) === 0) || (key.length > existingKey.length && key.indexOf(existingKey) === 0) ) { - throw new Error(`cannot infer query fields to set, both paths '${existingKey}' and '${key}' are matched`); + throw new Error( + `cannot infer query fields to set, both paths '${existingKey}' and ` + + `'${key}' are matched` + ); } else if (existingKey === key) { - throw new Error(`cannot infer query fields to set, path '${key}' is matched twice`); + throw new Error( + `cannot infer query fields to set, path '${key}' is matched twice` + ); } }); @@ -701,8 +767,11 @@ export function isOperatorObject(valueSelector, inconsistentOK) { if (theseAreOperators === undefined) { theseAreOperators = thisIsOperator; } else if (theseAreOperators !== thisIsOperator) { - if (!inconsistentOK) - throw new Error(`Inconsistent operator: ${JSON.stringify(valueSelector)}`); + if (!inconsistentOK) { + throw new Error( + `Inconsistent operator: ${JSON.stringify(valueSelector)}` + ); + } theseAreOperators = false; } @@ -799,7 +868,10 @@ function makeInequality(cmpValueComparator) { export function makeLookupFunction(key, options = {}) { const parts = key.split('.'); const firstPart = parts.length ? parts[0] : ''; - const lookupRest = parts.length > 1 && makeLookupFunction(parts.slice(1).join('.')); + const lookupRest = ( + parts.length > 1 && + makeLookupFunction(parts.slice(1).join('.')) + ); const omitUnnecessaryFields = result => { if (!result.dontIterate) @@ -888,7 +960,8 @@ export function makeLookupFunction(key, options = {}) { // selector), we skip the branching: we ONLY allow the numeric part to mean // 'look up this index' in that case, not 'also look up this index in all // the elements of the array'. - if (Array.isArray(firstLevel) && !(isNumericKey(parts[1]) && options.forSort)) { + if (Array.isArray(firstLevel) && + !(isNumericKey(parts[1]) && options.forSort)) { firstLevel.forEach((branch, arrayIndex) => { if (LocalCollection._isPlainObject(branch)) appendToResult(lookupRest(branch, arrayIndices.concat(arrayIndex))); @@ -925,9 +998,21 @@ function operatorBranchedMatcher(valueSelector, matcher, isRoot) { const operatorMatchers = Object.keys(valueSelector).map(operator => { const operand = valueSelector[operator]; - const simpleRange = ['$lt', '$lte', '$gt', '$gte'].includes(operator) && typeof operand === 'number'; - const simpleEquality = ['$ne', '$eq'].includes(operator) && operand !== Object(operand); - const simpleInclusion = ['$in', '$nin'].includes(operator) && Array.isArray(operand) && !operand.some(x => x === Object(x)); + const simpleRange = ( + ['$lt', '$lte', '$gt', '$gte'].includes(operator) && + typeof operand === 'number' + ); + + const simpleEquality = ( + ['$ne', '$eq'].includes(operator) && + operand !== Object(operand) + ); + + const simpleInclusion = ( + ['$in', '$nin'].includes(operator) + && Array.isArray(operand) + && !operand.some(x => x === Object(x)) + ); if (!(simpleRange || simpleInclusion || simpleEquality)) matcher._isSimple = false; @@ -937,7 +1022,10 @@ function operatorBranchedMatcher(valueSelector, matcher, isRoot) { if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { const options = ELEMENT_OPERATORS[operator]; - return convertElementMatcherToBranchedMatcher(options.compileElementSelector(operand, valueSelector, matcher), options); + return convertElementMatcherToBranchedMatcher( + options.compileElementSelector(operand, valueSelector, matcher), + options + ); } throw new Error(`Unrecognized operator: ${operator}`); @@ -965,7 +1053,11 @@ export function pathsToTree(paths, newLeafFn, conflictFn, root = {}) { if (!tree.hasOwnProperty(key)) { tree[key] = {}; } else if (tree[key] !== Object(tree[key])) { - tree[key] = conflictFn(tree[key], pathArray.slice(0, i + 1).join('.'), path); + tree[key] = conflictFn( + tree[key], + pathArray.slice(0, i + 1).join('.'), + path + ); // break out of loop if we are failing for this path if (tree[key] !== Object(tree[key])) @@ -997,8 +1089,10 @@ function pointToArray(point) { } // Creating a document from an upsert is quite tricky. -// E.g. this selector: {"$or": [{"b.foo": {"$all": ["bar"]}}]}, should result in: {"b.foo": "bar"} -// But this selector: {"$or": [{"b": {"foo": {"$all": ["bar"]}}}]} should throw an error +// E.g. this selector: {"$or": [{"b.foo": {"$all": ["bar"]}}]}, should result +// in: {"b.foo": "bar"} +// But this selector: {"$or": [{"b": {"foo": {"$all": ["bar"]}}}]} should throw +// an error // Some rules (found mainly with trial & error, so there might be more): // - handle all childs of $and (or implicit $and) @@ -1007,7 +1101,8 @@ function pointToArray(point) { // - ignore $nor and $not nodes // - throw when a value can not be set unambiguously // - every value for $all should be dealt with as separate $eq-s -// - threat all children of $all as $eq setters (=> set if $all.length === 1, otherwise throw error) +// - threat all children of $all as $eq setters (=> set if $all.length === 1, +// otherwise throw error) // - you can not mix '$'-prefixed keys and non-'$'-prefixed keys // - you can only have dotted keys on a root-level // - you can not have '$'-prefixed keys more than one-level deep in an object @@ -1043,7 +1138,9 @@ function populateDocumentWithObject(document, key, value) { populateDocumentWithKeyValue(document, key, object); } else if (op === '$all') { // every value for $all should be dealt with as separate $eq-s - object.forEach(element => populateDocumentWithKeyValue(document, key, element)); + object.forEach(element => + populateDocumentWithKeyValue(document, key, element) + ); } }); } @@ -1058,7 +1155,9 @@ export function populateDocumentWithQueryFields(query, document = {}) { if (key === '$and') { // handle explicit $and - value.forEach(element => populateDocumentWithQueryFields(element, document)); + value.forEach(element => + populateDocumentWithQueryFields(element, document) + ); } else if (key === '$or') { // handle $or nodes with exactly 1 child if (value.length === 1) @@ -1084,9 +1183,9 @@ export function populateDocumentWithQueryFields(query, document = {}) { // (exception for '_id' as it is a special case handled separately) // - including - Boolean - "take only certain fields" type of projection export function projectionDetails(fields) { - // Find the non-_id keys (_id is handled specially because it is included unless - // explicitly excluded). Sort the keys, so that our code to detect overlaps - // like 'foo' and 'foo.bar' can assume that 'foo' comes first. + // Find the non-_id keys (_id is handled specially because it is included + // unless explicitly excluded). Sort the keys, so that our code to detect + // overlaps like 'foo' and 'foo.bar' can assume that 'foo' comes first. let fieldsKeys = Object.keys(fields).sort(); // If _id is the only field in the projection, do not remove it, since it is @@ -1108,8 +1207,11 @@ export function projectionDetails(fields) { including = rule; // This error message is copied from MongoDB shell - if (including !== rule) - throw MinimongoError('You cannot currently mix including and excluding fields.'); + if (including !== rule) { + throw MinimongoError( + 'You cannot currently mix including and excluding fields.' + ); + } }); const projectionRulesTree = pathsToTree( @@ -1127,14 +1229,18 @@ export function projectionDetails(fields) { // Example, assume following in mongo shell: // > db.coll.insert({ a: { b: 23, c: 44 } }) // > db.coll.find({}, { 'a': 1, 'a.b': 1 }) - // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23 } } + // {"_id": ObjectId("520bfe456024608e8ef24af3"), "a": {"b": 23}} // > db.coll.find({}, { 'a.b': 1, 'a': 1 }) - // { "_id" : ObjectId("520bfe456024608e8ef24af3"), "a" : { "b" : 23, "c" : 44 } } + // {"_id": ObjectId("520bfe456024608e8ef24af3"), "a": {"b": 23, "c": 44}} // // Note, how second time the return set of keys is different. const currentPath = fullPath; const anotherPath = path; - throw MinimongoError(`both ${currentPath} and ${anotherPath} found in fields option, using both of them may trigger unexpected behavior. Did you mean to use only one of them?`); + throw MinimongoError( + `both ${currentPath} and ${anotherPath} found in fields option, ` + + 'using both of them may trigger unexpected behavior. Did you mean to ' + + 'use only one of them?' + ); }); return {including, tree: projectionRulesTree}; @@ -1165,10 +1271,17 @@ export function regexpElementMatcher(regexp) { // Objects that are nested more then 1 level cannot have dotted fields // or fields starting with '$' function validateKeyInPath(key, path) { - if (key.includes('.')) - throw new Error(`The dotted field '${key}' in '${path}.${key} is not valid for storage.`); - if (key[0] === '$') - throw new Error(`The dollar ($) prefixed field '${path}.${key} is not valid for storage.`); + if (key.includes('.')) { + throw new Error( + `The dotted field '${key}' in '${path}.${key} is not valid for storage.` + ); + } + + if (key[0] === '$') { + throw new Error( + `The dollar ($) prefixed field '${path}.${key} is not valid for storage.` + ); + } } // Recursively validates an object that is nested more than one level deep diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index 786cfc9632..b44d404ebe 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -1,7 +1,7 @@ import LocalCollection from './local_collection.js'; -// Cursor: a specification for a particular subset of documents, w/ -// a defined order, limit, and offset. creating a Cursor with LocalCollection.find(), +// Cursor: a specification for a particular subset of documents, w/ a defined +// order, limit, and offset. creating a Cursor with LocalCollection.find(), export default class Cursor { // don't call this ctor directly. use LocalCollection.find(). constructor(collection, selector, options = {}) { @@ -16,7 +16,10 @@ export default class Cursor { this._selectorId = undefined; if (this.matcher.hasGeoQuery() || options.sort) - this.sorter = new Minimongo.Sorter(options.sort || [], {matcher: this.matcher}); + this.sorter = new Minimongo.Sorter( + options.sort || [], + {matcher: this.matcher} + ); } this.skip = options.skip || 0; @@ -42,7 +45,8 @@ export default class Cursor { */ count() { if (this.reactive) - this._depend({added: true, removed: true}, true /* allow the observe to be unordered */); + // allow the observe to be unordered + this._depend({added: true, removed: true}, true); return this._getRawObjects({ordered: true}).length; } @@ -71,13 +75,18 @@ export default class Cursor { * @param {Number} index */ /** - * @summary Call `callback` once for each matching document, sequentially and synchronously. + * @summary Call `callback` once for each matching document, sequentially and + * synchronously. * @locus Anywhere * @method forEach * @instance * @memberOf Mongo.Cursor - * @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`. + * @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`. */ forEach(callback, thisArg) { if (this.reactive) { @@ -109,8 +118,12 @@ export default class Cursor { * @method map * @instance * @memberOf Mongo.Cursor - * @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`. + * @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`. */ map(callback, thisArg) { const result = []; @@ -148,18 +161,22 @@ export default class Cursor { * @locus Anywhere * @memberOf Mongo.Cursor * @instance - * @param {Object} callbacks Functions to call to deliver the result set as it changes + * @param {Object} callbacks Functions to call to deliver the result set as it + * changes */ observe(options) { return LocalCollection._observeFromObserveChanges(this, options); } /** - * @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. + * @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 * @memberOf Mongo.Cursor * @instance - * @param {Object} callbacks Functions to call to deliver the result set as it changes + * @param {Object} callbacks Functions to call to deliver the result set as it + * changes */ observeChanges(options) { const ordered = LocalCollection._observeChangesCallbacksAreOrdered(options); @@ -169,15 +186,24 @@ export default class Cursor { // comment in _modifyAndNotify // XXX allow skip/limit with unordered observe if (!options._allow_unordered && !ordered && (this.skip || this.limit)) - throw new Error('must use ordered observe (ie, \'addedBefore\' instead of \'added\') with skip or limit'); + throw new Error( + 'must use ordered observe (ie, \'addedBefore\' instead of \'added\') ' + + 'with skip or limit' + ); if (this.fields && (this.fields._id === 0 || this.fields._id === false)) throw Error('You may not observe a cursor with {fields: {_id: 0}}'); + const distances = ( + this.matcher.hasGeoQuery() && + ordered && + new LocalCollection._IdMap + ); + const query = { cursor: this, dirty: false, - distances: this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap, + distances, matcher: this.matcher, // not fast pathed ordered, projectionFn: this._projectionFn, @@ -291,10 +317,11 @@ export default class Cursor { const options = {_allow_unordered, _suppress_initial: true}; - ['added', 'addedBefore', 'changed', 'movedBefore', 'removed'].forEach(fn => { - if (changers[fn]) - options[fn] = notify; - }); + ['added', 'addedBefore', 'changed', 'movedBefore', 'removed'] + .forEach(fn => { + if (changers[fn]) + options[fn] = notify; + }); // observeChanges will stop() when this computation is invalidated this.observeChanges(options); @@ -307,19 +334,20 @@ export default class Cursor { // Returns a collection of matching objects, but doesn't deep copy them. // - // If ordered is set, returns a sorted array, respecting sorter, skip, and limit - // properties of the query. if sorter is falsey, no sort -- you get the natural - // order. + // If ordered is set, returns a sorted array, respecting sorter, skip, and + // limit properties of the query. if sorter is falsey, no sort -- you get the + // natural order. // - // If ordered is not set, returns an object mapping from ID to doc (sorter, skip - // and limit should not be set). + // If ordered is not set, returns an object mapping from ID to doc (sorter, + // skip and limit should not be set). // // If ordered is set and this cursor is a $near geoquery, then this function // will use an _IdMap to track each distance from the $near argument point in // order to use it as a sort key. If an _IdMap is passed in the 'distances' - // argument, this function will clear it and use it for this purpose (otherwise - // it will just create its own _IdMap). The observeChanges implementation uses - // this to remember the distances after this function returns. + // argument, this function will clear it and use it for this purpose + // (otherwise it will just create its own _IdMap). The observeChanges + // implementation uses this to remember the distances after this function + // returns. _getRawObjects(options = {}) { // XXX use OrderedDict instead of array, and make IdMap and OrderedDict // compatible @@ -347,9 +375,9 @@ export default class Cursor { // slow path for arbitrary selector, sort, skip, limit - // in the observeChanges case, distances is actually part of the "query" (ie, - // live results set) object. in other cases, distances is only used inside - // this function. + // in the observeChanges case, distances is actually part of the "query" + // (ie, live results set) object. in other cases, distances is only used + // inside this function. let distances; if (this.matcher.hasGeoQuery() && options.ordered) { if (options.distances) { @@ -376,7 +404,12 @@ export default class Cursor { // Fast path for limited unsorted queries. // XXX 'length' check here seems wrong for ordered - return !this.limit || this.skip || this.sorter || results.length !== this.limit; + return ( + !this.limit || + this.skip || + this.sorter || + results.length !== this.limit + ); }); if (!options.ordered) @@ -388,17 +421,30 @@ export default class Cursor { if (!this.limit && !this.skip) return results; - return results.slice(this.skip, this.limit ? this.limit + this.skip : results.length); + return results.slice( + this.skip, + this.limit ? this.limit + this.skip : results.length + ); } _publishCursor(subscription) { // XXX minimongo should not depend on mongo-livedata! - if (!Package.mongo) - throw new Error('Can\'t publish from Minimongo without the `mongo` package.'); + if (!Package.mongo) { + throw new Error( + 'Can\'t publish from Minimongo without the `mongo` package.' + ); + } - if (!this.collection.name) - throw new Error('Can\'t publish a cursor from a collection without a name.'); + if (!this.collection.name) { + throw new Error( + 'Can\'t publish a cursor from a collection without a name.' + ); + } - return Package.mongo.Mongo.Collection._publishCursor(this, subscription, this.collection.name); + return Package.mongo.Mongo.Collection._publishCursor( + this, + subscription, + this.collection.name + ); } } diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 16727489a1..b3f215756f 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -30,8 +30,8 @@ export default class LocalCollection { // selector, sorter, (callbacks): functions this.queries = {}; - // null if not saving originals; an IdMap from id to original document value if - // saving originals. See comments before saveOriginals(). + // null if not saving originals; an IdMap from id to original document value + // if saving originals. See comments before saveOriginals(). this._savedOriginals = null; // True when observers are paused and we should not send callbacks. @@ -162,9 +162,9 @@ export default class LocalCollection { } remove(selector, callback) { - // Easy special case: if we're not calling observeChanges callbacks and we're - // not saving originals and we got asked to remove everything, then just empty - // everything directly. + // Easy special case: if we're not calling observeChanges callbacks and + // we're not saving originals and we got asked to remove everything, then + // just empty everything directly. if (this.paused && !this._savedOriginals && EJSON.equals(selector, {})) { const result = this._docs.size(); @@ -270,12 +270,19 @@ export default class LocalCollection { if (query.dirty) { query.dirty = false; - // re-compute results will perform `LocalCollection._diffQueryChanges` automatically. + // re-compute results will perform `LocalCollection._diffQueryChanges` + // automatically. this._recomputeResults(query, query.resultsSnapshot); } else { // Diff the current results against the snapshot and send to observers. // pass the query object for its observer callbacks. - LocalCollection._diffQueryChanges(query.ordered, query.resultsSnapshot, query.results, query, {projectionFn: query.projectionFn}); + LocalCollection._diffQueryChanges( + query.ordered, + query.resultsSnapshot, + query.results, + query, + {projectionFn: query.projectionFn} + ); } query.resultsSnapshot = null; @@ -295,13 +302,13 @@ export default class LocalCollection { return originals; } - // To track what documents are affected by a piece of code, call saveOriginals() - // before it and retrieveOriginals() after it. retrieveOriginals returns an - // object whose keys are the ids of the documents that were affected since the - // call to saveOriginals(), and the values are equal to the document's contents - // at the time of saveOriginals. (In the case of an inserted document, undefined - // is the value.) You must alternate between calls to saveOriginals() and - // retrieveOriginals(). + // To track what documents are affected by a piece of code, call + // saveOriginals() before it and retrieveOriginals() after it. + // retrieveOriginals returns an object whose keys are the ids of the documents + // that were affected since the call to saveOriginals(), and the values are + // equal to the document's contents at the time of saveOriginals. (In the case + // of an inserted document, undefined is the value.) You must alternate + // between calls to saveOriginals() and retrieveOriginals(). saveOriginals() { if (this._savedOriginals) throw new Error('Called saveOriginals twice without retrieveOriginals'); @@ -329,9 +336,10 @@ export default class LocalCollection { // _recomputeResults.) const qidToOriginalResults = {}; - // We should only clone each document once, even if it appears in multiple queries + // We should only clone each document once, even if it appears in multiple + // queries const docMap = new LocalCollection._IdMap; - const idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector); + const idsMatched = LocalCollection._idsMatchedBySelector(selector); for (let qid in this.queries) { const query = this.queries[qid]; @@ -358,9 +366,10 @@ export default class LocalCollection { if (docMap.has(doc._id)) return docMap.get(doc._id); - const docToMemoize = idsMatchedBySelector && !idsMatchedBySelector.some(id => EJSON.equals(id, doc._id)) - ? doc - : EJSON.clone(doc); + const docToMemoize = ( + idsMatched && + !idsMatched.some(id => EJSON.equals(id, doc._id)) + ) ? doc : EJSON.clone(doc); docMap.set(doc._id, docToMemoize); @@ -381,7 +390,12 @@ export default class LocalCollection { if (queryResult.result) { // XXX Should we save the original even if mod ends up being a no-op? this._saveOriginal(id, doc); - this._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices); + this._modifyAndNotify( + doc, + mod, + recomputeQids, + queryResult.arrayIndices + ); ++updateCount; @@ -437,15 +451,20 @@ export default class LocalCollection { } // A convenience wrapper on update. LocalCollection.upsert(sel, mod) is - // equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject: - // true }). + // equivalent to LocalCollection.update(sel, mod, {upsert: true, + // _returnObject: true}). upsert(selector, mod, options, callback) { if (!callback && typeof options === 'function') { callback = options; options = {}; } - return this.update(selector, mod, Object.assign({}, options, {upsert: true, _returnObject: true}), callback); + return this.update( + selector, + mod, + Object.assign({}, options, {upsert: true, _returnObject: true}), + callback + ); } // Iterates over a subset of documents that could match selector; calls @@ -526,10 +545,11 @@ export default class LocalCollection { // difference between the previous results and the current results (unless // paused). Used for skip/limit queries. // - // When this is used by insert or remove, it can just use query.results for the - // old results (and there's no need to pass in oldResults), because these - // operations don't mutate the documents in the collection. Update needs to pass - // in an oldResults which was deep-copied before the modifier was applied. + // When this is used by insert or remove, it can just use query.results for + // the old results (and there's no need to pass in oldResults), because these + // operations don't mutate the documents in the collection. Update needs to + // pass in an oldResults which was deep-copied before the modifier was + // applied. // // oldResults is guaranteed to be ignored if the query is not paused. _recomputeResults(query, oldResults) { @@ -547,10 +567,20 @@ export default class LocalCollection { if (query.distances) query.distances.clear(); - query.results = query.cursor._getRawObjects({distances: query.distances, ordered: query.ordered}); + query.results = query.cursor._getRawObjects({ + distances: query.distances, + ordered: query.ordered + }); - if (!this.paused) - LocalCollection._diffQueryChanges(query.ordered, oldResults, query.results, query, {projectionFn: query.projectionFn}); + if (!this.paused) { + LocalCollection._diffQueryChanges( + query.ordered, + oldResults, + query.results, + query, + {projectionFn: query.projectionFn} + ); + } } _saveOriginal(id, doc) { @@ -583,7 +613,10 @@ LocalCollection.ObserveHandle = ObserveHandle; // available as `this` to those callbacks. LocalCollection._CachingChangeObserver = class _CachingChangeObserver { constructor(options = {}) { - const orderedFromCallbacks = options.callbacks && LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks); + const orderedFromCallbacks = ( + options.callbacks && + LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks) + ); if (options.hasOwnProperty('ordered')) { this.ordered = options.ordered; @@ -698,7 +731,8 @@ LocalCollection.wrapTransform = transform => { const id = doc._id; - // XXX consider making tracker a weak dependency and checking Package.tracker here + // XXX consider making tracker a weak dependency and checking + // Package.tracker here const transformed = Tracker.nonreactive(() => transform(doc)); if (!LocalCollection._isPlainObject(transformed)) @@ -750,16 +784,27 @@ LocalCollection._checkSupportedProjection = fields => { throw MinimongoError('fields option must be an object'); Object.keys(fields).forEach(keyPath => { - if (keyPath.split('.').includes('$')) - throw MinimongoError('Minimongo doesn\'t support $ operator in projections yet.'); + if (keyPath.split('.').includes('$')) { + throw MinimongoError( + 'Minimongo doesn\'t support $ operator in projections yet.' + ); + } const value = fields[keyPath]; - if (typeof value === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => value.hasOwnProperty(key))) - throw MinimongoError('Minimongo doesn\'t support operators in projections yet.'); + if (typeof value === 'object' && + ['$elemMatch', '$meta', '$slice'].some(key => + value.hasOwnProperty(key) + )) { + throw MinimongoError( + 'Minimongo doesn\'t support operators in projections yet.' + ); + } if (![1, 0, true, false].includes(value)) - throw MinimongoError('Projection values should be one of 1, 0, true, or false'); + throw MinimongoError( + 'Projection values should be one of 1, 0, true, or false' + ); }); }; @@ -794,7 +839,8 @@ LocalCollection._compileProjection = fields => { // For sub-objects/subsets we branch if (doc[key] === Object(doc[key])) result[key] = transform(doc[key], rule); - } else if (details.including) { // Otherwise we don't even touch this subfield + } else if (details.including) { + // Otherwise we don't even touch this subfield result[key] = EJSON.clone(doc[key]); } else { delete result[key]; @@ -817,8 +863,8 @@ LocalCollection._compileProjection = fields => { }; }; -// Calculates the document to insert in case we're doing an upsert and the selector -// does not match any elements +// Calculates the document to insert in case we're doing an upsert and the +// selector does not match any elements LocalCollection._createUpsertDocument = (selector, modifier) => { const selectorDocument = populateDocumentWithQueryFields(selector); const isModify = LocalCollection._isModificationMod(modifier); @@ -830,8 +876,9 @@ LocalCollection._createUpsertDocument = (selector, modifier) => { delete selectorDocument._id; } - // This double _modify call is made to help with nested properties (see issue #8631). - // We do this even if it's a replacement for validation purposes (e.g. ambiguous id's) + // This double _modify call is made to help with nested properties (see issue + // #8631). We do this even if it's a replacement for validation purposes (e.g. + // ambiguous id's) LocalCollection._modify(newDoc, {$set: selectorDocument}); LocalCollection._modify(newDoc, modifier, {isInsert: true}); @@ -856,17 +903,17 @@ LocalCollection._diffObjects = (left, right, callbacks) => { // old_results and new_results: collections of documents. // if ordered, they are arrays. // if unordered, they are IdMaps -LocalCollection._diffQueryChanges = (ordered, oldResults, newResults, observer, options) => { - return DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options); -}; +LocalCollection._diffQueryChanges = (ordered, oldResults, newResults, observer, options) => + DiffSequence.diffQueryChanges(ordered, oldResults, newResults, observer, options) +; -LocalCollection._diffQueryOrderedChanges = (oldResults, newResults, observer, options) => { - return DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options); -}; +LocalCollection._diffQueryOrderedChanges = (oldResults, newResults, observer, options) => + DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options) +; -LocalCollection._diffQueryUnorderedChanges = (oldResults, newResults, observer, options) => { - return DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options); -}; +LocalCollection._diffQueryUnorderedChanges = (oldResults, newResults, observer, options) => + DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options) +; LocalCollection._findInOrderedResults = (query, doc) => { if (!query.ordered) @@ -934,7 +981,11 @@ LocalCollection._insertInResults = (query, doc) => { query.addedBefore(doc._id, query.projectionFn(fields), null); query.results.push(doc); } else { - const i = LocalCollection._insertInSortedList(query.sorter.getComparator({distances: query.distances}), query.results, doc); + const i = LocalCollection._insertInSortedList( + query.sorter.getComparator({distances: query.distances}), + query.results, + doc + ); let next = query.results[i + 1]; if (next) @@ -978,7 +1029,9 @@ LocalCollection._isModificationMod = mod => { } if (isModify && isReplace) { - throw new Error('Update parameter cannot have both modifier and non-modifier fields.'); + throw new Error( + 'Update parameter cannot have both modifier and non-modifier fields.' + ); } return isModify; @@ -1017,7 +1070,8 @@ LocalCollection._modify = (doc, modifier, options = {}) => { // apply modifiers to the doc. Object.keys(modifier).forEach(operator => { // Treat $setOnInsert as $set if this is an insert. - const modFunc = MODIFIERS[options.isInsert && operator === '$setOnInsert' ? '$set' : operator]; + const setOnInsert = options.isInsert && operator === '$setOnInsert'; + const modFunc = MODIFIERS[setOnInsert ? '$set' : operator]; const operand = modifier[operator]; if (!modFunc) @@ -1031,8 +1085,12 @@ LocalCollection._modify = (doc, modifier, options = {}) => { const keyparts = keypath.split('.'); - if (!keyparts.every(Boolean)) - throw MinimongoError(`The update path '${keypath}' contains an empty field name, which is not allowed.`); + if (!keyparts.every(Boolean)) { + throw MinimongoError( + `The update path '${keypath}' contains an empty field name, ` + + 'which is not allowed.' + ); + } const target = findModTarget(newDoc, keyparts, { arrayIndices: options.arrayIndices, @@ -1044,11 +1102,20 @@ LocalCollection._modify = (doc, modifier, options = {}) => { }); }); - if (doc._id && !EJSON.equals(doc._id, newDoc._id)) - throw MinimongoError(`After applying the update to the document {_id: "${doc._id}", ...}, the (immutable) field '_id' was found to have been altered to _id: "${newDoc._id}"`); + if (doc._id && !EJSON.equals(doc._id, newDoc._id)) { + throw MinimongoError( + `After applying the update to the document {_id: "${doc._id}", ...},` + + ' the (immutable) field \'_id\' was found to have been altered to ' + + `_id: "${newDoc._id}"` + ); + } } else { - if (doc._id && modifier._id && !EJSON.equals(doc._id, modifier._id)) - throw MinimongoError(`The _id field cannot be changed from {_id: "${doc._id}"} to {_id: "${modifier._id}"}`); + if (doc._id && modifier._id && !EJSON.equals(doc._id, modifier._id)) { + throw MinimongoError( + `The _id field cannot be changed from {_id: "${doc._id}"} to ` + + `{_id: "${modifier._id}"}` + ); + } // replace the whole document assertHasValidFieldNames(modifier); @@ -1087,10 +1154,19 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { const doc = transform(Object.assign(fields, {_id: id})); - if (observeCallbacks.addedAt) - observeCallbacks.addedAt(doc, indices ? before ? this.docs.indexOf(before) : this.docs.size() : -1, before); - else + if (observeCallbacks.addedAt) { + observeCallbacks.addedAt( + doc, + indices + ? before + ? this.docs.indexOf(before) + : this.docs.size() + : -1, + before + ); + } else { observeCallbacks.added(doc); + } }, changed(id, fields) { if (!(observeCallbacks.changedAt || observeCallbacks.changed)) @@ -1104,24 +1180,38 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { DiffSequence.applyChanges(doc, fields); - if (observeCallbacks.changedAt) - observeCallbacks.changedAt(transform(doc), oldDoc, indices ? this.docs.indexOf(id) : -1); - else + if (observeCallbacks.changedAt) { + observeCallbacks.changedAt( + transform(doc), + oldDoc, + indices ? this.docs.indexOf(id) : -1 + ); + } else { observeCallbacks.changed(transform(doc), oldDoc); + } }, movedBefore(id, before) { if (!observeCallbacks.movedTo) return; const from = indices ? this.docs.indexOf(id) : -1; - let to = indices ? before ? this.docs.indexOf(before) : this.docs.size() : -1; + let to = indices + ? before + ? this.docs.indexOf(before) + : this.docs.size() + : -1; // When not moving backwards, adjust for the fact that removing the // document slides everything back one slot. if (to > from) --to; - observeCallbacks.movedTo(transform(EJSON.clone(this.docs.get(id))), from, to, before || null); + observeCallbacks.movedTo( + transform(EJSON.clone(this.docs.get(id))), + from, + to, + before || null + ); }, removed(id) { if (!(observeCallbacks.removedAt || observeCallbacks.removed)) @@ -1150,7 +1240,10 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { DiffSequence.applyChanges(doc, fields); - observeCallbacks.changed(transform(doc), transform(EJSON.clone(oldDoc))); + observeCallbacks.changed( + transform(doc), + transform(EJSON.clone(oldDoc)) + ); } }, removed(id) { @@ -1160,7 +1253,10 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { }; } - const changeObserver = new LocalCollection._CachingChangeObserver({callbacks: observeChangesCallbacks}); + const changeObserver = new LocalCollection._CachingChangeObserver({ + callbacks: observeChangesCallbacks + }); + const handle = cursor.observeChanges(changeObserver.applyChange); suppressed = false; @@ -1178,7 +1274,12 @@ LocalCollection._observeCallbacksAreOrdered = callbacks => { if (callbacks.removed && callbacks.removedAt) throw new Error('Please specify only one of removed() and removedAt()'); - return !!(callbacks.addedAt || callbacks.changedAt || callbacks.movedTo || callbacks.removedAt); + return !!( + callbacks.addedAt || + callbacks.changedAt || + callbacks.movedTo || + callbacks.removedAt + ); }; LocalCollection._observeChangesCallbacksAreOrdered = callbacks => { @@ -1203,24 +1304,28 @@ LocalCollection._removeFromResults = (query, doc) => { }; // Is this selector just shorthand for lookup by _id? -LocalCollection._selectorIsId = selector => { - return typeof selector === 'number' || typeof selector === 'string' || selector instanceof MongoID.ObjectID; -}; +LocalCollection._selectorIsId = selector => + typeof selector === 'number' || + typeof selector === 'string' || + selector instanceof MongoID.ObjectID +; // Is the selector just lookup by _id (shorthand or not)? -LocalCollection._selectorIsIdPerhapsAsObject = selector => { - if (LocalCollection._selectorIsId(selector)) - return true; - - return LocalCollection._selectorIsId(selector && selector._id) && Object.keys(selector).length === 1; -}; +LocalCollection._selectorIsIdPerhapsAsObject = selector => + LocalCollection._selectorIsId(selector) || + LocalCollection._selectorIsId(selector && selector._id) && + Object.keys(selector).length === 1 +; LocalCollection._updateInResults = (query, doc, old_doc) => { if (!EJSON.equals(doc._id, old_doc._id)) throw new Error('Can\'t change a doc\'s _id while updating'); const projectionFn = query.projectionFn; - const changedFields = DiffSequence.makeChangedFields(projectionFn(doc), projectionFn(old_doc)); + const changedFields = DiffSequence.makeChangedFields( + projectionFn(doc), + projectionFn(old_doc) + ); if (!query.ordered) { if (Object.keys(changedFields).length) { @@ -1242,7 +1347,11 @@ LocalCollection._updateInResults = (query, doc, old_doc) => { // just take it out and put it back in again, and see if the index changes query.results.splice(old_idx, 1); - const new_idx = LocalCollection._insertInSortedList(query.sorter.getComparator({distances: query.distances}), query.results, doc); + const new_idx = LocalCollection._insertInSortedList( + query.sorter.getComparator({distances: query.distances}), + query.results, + doc + ); if (old_idx !== new_idx) { let next = query.results[new_idx + 1]; @@ -1258,8 +1367,13 @@ LocalCollection._updateInResults = (query, doc, old_doc) => { const MODIFIERS = { $currentDate(target, field, arg) { if (typeof arg === 'object' && arg.hasOwnProperty('$type')) { - if (arg.$type !== 'date') - throw MinimongoError('Minimongo does currently only support the date type in $currentDate modifiers', {field}); + if (arg.$type !== 'date') { + throw MinimongoError( + 'Minimongo does currently only support the date type in ' + + '$currentDate modifiers', + {field} + ); + } } else if (arg !== true) { throw MinimongoError('Invalid $currentDate modifier', {field}); } @@ -1271,8 +1385,12 @@ const MODIFIERS = { throw MinimongoError('Modifier $min allowed for numbers only', {field}); if (field in target) { - if (typeof target[field] !== 'number') - throw MinimongoError('Cannot apply $min modifier to non-number', {field}); + if (typeof target[field] !== 'number') { + throw MinimongoError( + 'Cannot apply $min modifier to non-number', + {field} + ); + } if (target[field] > arg) target[field] = arg; @@ -1285,8 +1403,12 @@ const MODIFIERS = { throw MinimongoError('Modifier $max allowed for numbers only', {field}); if (field in target) { - if (typeof target[field] !== 'number') - throw MinimongoError('Cannot apply $max modifier to non-number', {field}); + if (typeof target[field] !== 'number') { + throw MinimongoError( + 'Cannot apply $max modifier to non-number', + {field} + ); + } if (target[field] < arg) target[field] = arg; @@ -1299,8 +1421,12 @@ const MODIFIERS = { throw MinimongoError('Modifier $inc allowed for numbers only', {field}); if (field in target) { - if (typeof target[field] !== 'number') - throw MinimongoError('Cannot apply $inc modifier to non-number', { field }); + if (typeof target[field] !== 'number') { + throw MinimongoError( + 'Cannot apply $inc modifier to non-number', + {field} + ); + } target[field] += arg; } else { @@ -1309,7 +1435,10 @@ const MODIFIERS = { }, $set(target, field, arg) { if (target !== Object(target)) { // not an array or an object - const error = MinimongoError('Cannot set property on non-object field', {field}); + const error = MinimongoError( + 'Cannot set property on non-object field', + {field} + ); error.setPropertyError = true; throw error; } @@ -1367,8 +1496,12 @@ const MODIFIERS = { throw MinimongoError('$position must be a numeric value', {field}); // XXX should check to make sure integer - if (arg.$position < 0) - throw MinimongoError('$position in $push must be zero or positive', {field}); + if (arg.$position < 0) { + throw MinimongoError( + '$position in $push must be zero or positive', + {field} + ); + } position = arg.$position; } @@ -1396,8 +1529,13 @@ const MODIFIERS = { sortFunction = new Minimongo.Sorter(arg.$sort).getComparator(); toPush.forEach(element => { - if (LocalCollection._f._type(element) !== 3) - throw MinimongoError('$push like modifiers using $sort require all elements to be objects', {field}); + if (LocalCollection._f._type(element) !== 3) { + throw MinimongoError( + '$push like modifiers using $sort require all elements to be ' + + 'objects', + {field} + ); + } }); } @@ -1442,7 +1580,10 @@ const MODIFIERS = { if (toPush === undefined) { target[field] = arg; } else if (!(toPush instanceof Array)) { - throw MinimongoError('Cannot apply $pushAll modifier to non-array', {field}); + throw MinimongoError( + 'Cannot apply $pushAll modifier to non-array', + {field} + ); } else { toPush.push(...arg); } @@ -1466,7 +1607,10 @@ const MODIFIERS = { if (toAdd === undefined) { target[field] = values; } else if (!(toAdd instanceof Array)) { - throw MinimongoError('Cannot apply $addToSet modifier to non-array', {field}); + throw MinimongoError( + 'Cannot apply $addToSet modifier to non-array', + {field} + ); } else { values.forEach(value => { if (toAdd.some(element => LocalCollection._f._equal(value, element))) @@ -1501,8 +1645,12 @@ const MODIFIERS = { if (toPull === undefined) return; - if (!(toPull instanceof Array)) - throw MinimongoError('Cannot apply $pull/pullAll modifier to non-array', {field}); + if (!(toPull instanceof Array)) { + throw MinimongoError( + 'Cannot apply $pull/pullAll modifier to non-array', + {field} + ); + } let out; if (arg != null && typeof arg === 'object' && !(arg instanceof Array)) { @@ -1525,8 +1673,12 @@ const MODIFIERS = { target[field] = out; }, $pullAll(target, field, arg) { - if (!(typeof arg === 'object' && arg instanceof Array)) - throw MinimongoError('Modifier $pushAll/pullAll allowed for arrays only', {field}); + if (!(typeof arg === 'object' && arg instanceof Array)) { + throw MinimongoError( + 'Modifier $pushAll/pullAll allowed for arrays only', + {field} + ); + } if (target === undefined) return; @@ -1536,10 +1688,16 @@ const MODIFIERS = { if (toPull === undefined) return; - if (!(toPull instanceof Array)) - throw MinimongoError('Cannot apply $pull/pullAll modifier to non-array', {field}); + if (!(toPull instanceof Array)) { + throw MinimongoError( + 'Cannot apply $pull/pullAll modifier to non-array', + {field} + ); + } - target[field] = toPull.filter(object => !arg.some(element => LocalCollection._f._equal(object, element))); + target[field] = toPull.filter(object => + !arg.some(element => LocalCollection._f._equal(object, element)) + ); }, $rename(target, field, arg, keypath, doc) { // no idea why mongo has this restriction.. @@ -1555,7 +1713,10 @@ const MODIFIERS = { if (arg.includes('\0')) { // Null bytes are not allowed in Mongo field names // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names - throw MinimongoError('The \'to\' field for $rename cannot contain an embedded null byte', {field}); + throw MinimongoError( + 'The \'to\' field for $rename cannot contain an embedded null byte', + {field} + ); } if (target === undefined) @@ -1591,7 +1752,11 @@ const NO_CREATE_MODIFIERS = { // Make sure field names do not contain Mongo restricted // characters ('.', '$', '\0'). // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names -const invalidCharMsg = {$: 'start with \'$\'', '.': 'contain \'.\'', '\0': 'contain null bytes'}; +const invalidCharMsg = { + $: 'start with \'$\'', + '.': 'contain \'.\'', + '\0': 'contain null bytes' +}; // checks if all field names in an object are valid function assertHasValidFieldNames(doc) { @@ -1637,7 +1802,9 @@ function findModTarget(doc, keyparts, options = {}) { if (options.noCreate) return undefined; - const error = MinimongoError(`cannot use the part '${keypart}' to traverse ${doc}`); + const error = MinimongoError( + `cannot use the part '${keypart}' to traverse ${doc}` + ); error.setPropertyError = true; throw error; } @@ -1650,8 +1817,12 @@ function findModTarget(doc, keyparts, options = {}) { if (usedArrayIndex) throw MinimongoError('Too many positional (i.e. \'$\') elements'); - if (!options.arrayIndices || !options.arrayIndices.length) - throw MinimongoError('The positional operator did not find the match needed from the query'); + if (!options.arrayIndices || !options.arrayIndices.length) { + throw MinimongoError( + 'The positional operator did not find the match needed from the ' + + 'query' + ); + } keypart = options.arrayIndices[0]; usedArrayIndex = true; @@ -1661,7 +1832,9 @@ function findModTarget(doc, keyparts, options = {}) { if (options.noCreate) return undefined; - throw MinimongoError(`can't append to array using string field name [${keypart}]`); + throw MinimongoError( + `can't append to array using string field name [${keypart}]` + ); } if (last) @@ -1677,7 +1850,10 @@ function findModTarget(doc, keyparts, options = {}) { if (doc.length === keypart) { doc.push({}); } else if (typeof doc[keypart] !== 'object') { - throw MinimongoError(`can't modify field '${keyparts[i + 1]}' of list value ${JSON.stringify(doc[keypart])}`); + throw MinimongoError( + `can't modify field '${keyparts[i + 1]}' of list value ` + + JSON.stringify(doc[keypart]) + ); } } } else { diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index 926610453a..25a0ce38a4 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -103,7 +103,9 @@ export default class Matcher { } // Top level can't be an array or true or binary. - if (Array.isArray(selector) || EJSON.isBinary(selector) || typeof selector === 'boolean') + if (Array.isArray(selector) || + EJSON.isBinary(selector) || + typeof selector === 'boolean') throw new Error(`Invalid selector: ${selector}`); this._selector = EJSON.clone(selector); @@ -174,7 +176,8 @@ LocalCollection._f = { return EJSON.equals(a, b, {keyOrderSensitive: true}); }, - // maps a type code to a value that can be used to sort values of different types + // maps a type code to a value that can be used to sort values of different + // types _typeorder(t) { // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types // XXX what is the correct sort position for Javascript code? @@ -223,7 +226,8 @@ LocalCollection._f = { if (oa !== ob) return oa < ob ? -1 : 1; - // XXX need to implement this if we implement Symbol or integers, or Timestamp + // XXX need to implement this if we implement Symbol or integers, or + // Timestamp if (ta !== tb) throw Error('Missing type coercion logic in _cmp'); diff --git a/packages/minimongo/minimongo_server.js b/packages/minimongo/minimongo_server.js index f83147cbab..f0a2dd9896 100644 --- a/packages/minimongo/minimongo_server.js +++ b/packages/minimongo/minimongo_server.js @@ -6,7 +6,9 @@ import { projectionDetails, } from './common.js'; -Minimongo._pathsElidingNumericKeys = paths => paths.map(path => path.split('.').filter(part => !isNumericKey(part)).join('.')); +Minimongo._pathsElidingNumericKeys = paths => paths.map(path => + path.split('.').filter(part => !isNumericKey(part)).join('.') +); // Returns true if the modifier applied to some document may change the result // of matching the document by selector @@ -20,8 +22,11 @@ Minimongo.Matcher.prototype.affectedByModifier = function(modifier) { // safe check for $set/$unset being objects modifier = Object.assign({$set: {}, $unset: {}}, modifier); - const modifiedPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); const meaningfulPaths = this._getPaths(); + const modifiedPaths = [].concat( + Object.keys(modifier.$set), + Object.keys(modifier.$unset) + ); return modifiedPaths.some(path => { const mod = path.split('.'); @@ -77,9 +82,13 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { modifier = Object.assign({$set: {}, $unset: {}}, modifier); - const modifierPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset)); + const modifierPaths = [].concat( + Object.keys(modifier.$set), + Object.keys(modifier.$unset) + ); - if (this._getPaths().some(pathHasNumericKeys) || modifierPaths.some(pathHasNumericKeys)) + if (this._getPaths().some(pathHasNumericKeys) || + modifierPaths.some(pathHasNumericKeys)) return true; // check if there is a $set or $unset that indicates something is an @@ -91,7 +100,9 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { if (!isOperatorObject(this._selector[path])) return false; - return modifierPaths.some(modifierPath => modifierPath.startsWith(`${path}.`)); + return modifierPaths.some(modifierPath => + modifierPath.startsWith(`${path}.`) + ); }); if (expectedScalarIsObject) @@ -153,7 +164,8 @@ Minimongo.Matcher.prototype.matchingDocument = function() { if (this._matchingDocument !== undefined) return this._matchingDocument; - // If the analysis of this selector is too hard for our implementation fallback to "YES" + // If the analysis of this selector is too hard for our implementation + // fallback to "YES" let fallback = false; this._matchingDocument = pathsToTree( @@ -175,7 +187,9 @@ Minimongo.Matcher.prototype.matchingDocument = function() { // Return anything from $in that matches the whole selector for this // path. If nothing matches, returns `undefined` as nothing can make // this selector into `true`. - return valueSelector.$in.find(placeholder => matcher.documentMatches({placeholder}).result); + return valueSelector.$in.find(placeholder => + matcher.documentMatches({placeholder}).result + ); } if (onlyContainsKeys(valueSelector, ['$gt', '$gte', '$lt', '$lte'])) { @@ -183,19 +197,22 @@ Minimongo.Matcher.prototype.matchingDocument = function() { let upperBound = Infinity; ['$lte', '$lt'].forEach(op => { - if (valueSelector.hasOwnProperty(op) && valueSelector[op] < upperBound) + if (valueSelector.hasOwnProperty(op) && + valueSelector[op] < upperBound) upperBound = valueSelector[op]; }); ['$gte', '$gt'].forEach(op => { - if (valueSelector.hasOwnProperty(op) && valueSelector[op] > lowerBound) + if (valueSelector.hasOwnProperty(op) && + valueSelector[op] > lowerBound) lowerBound = valueSelector[op]; }); const middle = (lowerBound + upperBound) / 2; const matcher = new Minimongo.Matcher({placeholder: valueSelector}); - if (!matcher.documentMatches({placeholder: middle}).result && (middle === lowerBound || middle === upperBound)) + if (!matcher.documentMatches({placeholder: middle}).result && + (middle === lowerBound || middle === upperBound)) fallback = true; return middle; @@ -228,14 +245,22 @@ Minimongo.Sorter.prototype.affectedByModifier = function(modifier) { }; Minimongo.Sorter.prototype.combineIntoProjection = function(projection) { - return combineImportantPathsIntoProjection(Minimongo._pathsElidingNumericKeys(this._getPaths()), projection); + return combineImportantPathsIntoProjection( + Minimongo._pathsElidingNumericKeys(this._getPaths()), + projection + ); }; function combineImportantPathsIntoProjection(paths, projection) { const details = projectionDetails(projection); // merge the paths to include - const tree = pathsToTree(paths, path => true, (node, path, fullPath) => true, details.tree); + const tree = pathsToTree( + paths, + path => true, + (node, path, fullPath) => true, + details.tree + ); const mergedProjection = treeToPaths(tree); if (details.including) { @@ -272,7 +297,9 @@ function getPaths(selector) { // // the value is a literal or some comparison operator // return k; - // }).reduce((a, b) => a.concat(b), []).filter((a, b, c) => c.indexOf(a) === b); + // }) + // .reduce((a, b) => a.concat(b), []) + // .filter((a, b, c) => c.indexOf(a) === b); } // A helper to ensure object has only certain keys diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index a075652a51..46fa410580 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -11,10 +11,12 @@ Package.onUse(api => { api.export('MinimongoError', { testOnly: true }); api.use([ - 'diff-sequence', // This package is used to get diff results on arrays and objects + // This package is used to get diff results on arrays and objects + 'diff-sequence', 'ecmascript', 'ejson', - 'geojson-utils', // This package is used for geo-location queries such as $near + // This package is used for geo-location queries such as $near + 'geojson-utils', 'id-map', 'mongo-id', 'ordered-dict', diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index 7130697dad..6d90ec112a 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -32,7 +32,11 @@ export default class Sorter { if (path.charAt(0) === '$') throw Error(`unsupported sort key: ${path}`); - this._sortSpecParts.push({ascending, lookup: makeLookupFunction(path, {forSort: true}), path}); + this._sortSpecParts.push({ + ascending, + lookup: makeLookupFunction(path, {forSort: true}), + path + }); }; if (spec instanceof Array) { @@ -58,8 +62,9 @@ export default class Sorter { return; // To implement affectedByModifier, we piggy-back on top of Matcher's - // affectedByModifier code; we create a selector that is affected by the same - // modifiers as this sort order. This is only implemented on the server. + // affectedByModifier code; we create a selector that is affected by the + // same modifiers as this sort order. This is only implemented on the + // server. if (this.affectedByModifier) { const selector = {}; @@ -70,7 +75,9 @@ export default class Sorter { this._selectorForAffectedByModifier = new Minimongo.Matcher(selector); } - this._keyComparator = composeComparators(this._sortSpecParts.map((spec, i) => this._keyFieldComparator(i))); + this._keyComparator = composeComparators( + this._sortSpecParts.map((spec, i) => this._keyFieldComparator(i)) + ); // If you specify a matcher for this Sorter, _keyFilter may be set to a // function which selects whether or not a given "sort key" (tuple of values @@ -108,7 +115,8 @@ export default class Sorter { // parts. Returns negative, 0, or positive based on using the sort spec to // compare fields. _compareKeys(key1, key2) { - if (key1.length !== this._sortSpecParts.length || key2.length !== this._sortSpecParts.length) + if (key1.length !== this._sortSpecParts.length || + key2.length !== this._sortSpecParts.length) throw Error('Key has wrong length'); return this._keyComparator(key1, key2); @@ -176,7 +184,8 @@ export default class Sorter { if (knownPaths) { // Similarly to above, paths must match everywhere, unless this is a // non-array field. - if (!element.hasOwnProperty('') && Object.keys(knownPaths).length !== Object.keys(element).length) + if (!element.hasOwnProperty('') && + Object.keys(knownPaths).length !== Object.keys(element).length) throw Error('cannot index parallel arrays!'); } else if (usedPaths) { knownPaths = {}; @@ -364,12 +373,19 @@ export default class Sorter { if (['$lt', '$lte', '$gt', '$gte'].includes(operator)) { // XXX this depends on us knowing that these operators don't use any // of the arguments to compileElementSelector other than operand. - constraints.push(ELEMENT_OPERATORS[operator].compileElementSelector(operand)); + constraints.push( + ELEMENT_OPERATORS[operator].compileElementSelector(operand) + ); } // See comments in the RegExp block above. if (operator === '$regex' && !subSelector.$options) - constraints.push(ELEMENT_OPERATORS.$regex.compileElementSelector(operand, subSelector)); + constraints.push( + ELEMENT_OPERATORS.$regex.compileElementSelector( + operand, + subSelector + ) + ); // XXX support {$exists: true}, $mod, $type, $in, $elemMatch }); @@ -388,7 +404,11 @@ export default class Sorter { if (!constraintsByPath[this._sortSpecParts[0].path].length) return; - this._keyFilter = key => this._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(fn => fn(key[index]))); + this._keyFilter = key => + this._sortSpecParts.every((specPart, index) => + constraintsByPath[specPart.path].every(fn => fn(key[index])) + ) + ; } } From 5baf579c20ba6f5dc4a5af366cec02335ebaada4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 9 Aug 2017 16:51:27 +0200 Subject: [PATCH 26/28] Refactored .hasOwnProperty. --- packages/minimongo/common.js | 19 +++++++++--------- packages/minimongo/local_collection.js | 21 ++++++++++---------- packages/minimongo/matcher.js | 3 ++- packages/minimongo/minimongo_server.js | 4 ++-- packages/minimongo/minimongo_tests_client.js | 10 ++++++---- packages/minimongo/sorter.js | 13 ++++++------ 6 files changed, 38 insertions(+), 32 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index 72707dd377..88b22f1264 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -1,5 +1,7 @@ import LocalCollection from './local_collection.js'; +export const hasOwn = Object.prototype.hasOwnProperty; + // Each element selector contains: // - compileElementSelector, a function with args: // - operand - the "right hand side" of the operator @@ -155,7 +157,7 @@ export const ELEMENT_OPERATORS = { const isDocMatcher = !isOperatorObject( Object.keys(operand) - .filter(key => !LOGICAL_OPERATORS.hasOwnProperty(key)) + .filter(key => !hasOwn.call(LOGICAL_OPERATORS, key)) .reduce((a, b) => Object.assign(a, {[b]: operand[b]}), {}), true); @@ -298,7 +300,7 @@ const VALUE_OPERATORS = { }, // $options just provides options for $regex; its logic is inside $regex $options(operand, valueSelector) { - if (!valueSelector.hasOwnProperty('$regex')) + if (!hasOwn.call(valueSelector, '$regex')) throw Error('$options needs a $regex'); return everythingMatcher; }, @@ -340,8 +342,7 @@ const VALUE_OPERATORS = { // marked with a $geometry property, though legacy coordinates can be // matched using $geometry. let maxDistance, point, distance; - if (LocalCollection._isPlainObject(operand) && - operand.hasOwnProperty('$geometry')) { + if (LocalCollection._isPlainObject(operand) && hasOwn.call(operand, '$geometry')) { // GeoJSON "2dsphere" mode. maxDistance = operand.$maxDistance; point = operand.$geometry; @@ -504,7 +505,7 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) { if (key.substr(0, 1) === '$') { // Outer operators are either logical operators (they recurse back into // this function), or $where. - if (!LOGICAL_OPERATORS.hasOwnProperty(key)) + if (!hasOwn.call(LOGICAL_OPERATORS, key)) throw new Error(`Unrecognized logical operator: ${key}`); matcher._isSimple = false; @@ -1023,10 +1024,10 @@ function operatorBranchedMatcher(valueSelector, matcher, isRoot) { if (!(simpleRange || simpleInclusion || simpleEquality)) matcher._isSimple = false; - if (VALUE_OPERATORS.hasOwnProperty(operator)) + if (hasOwn.call(VALUE_OPERATORS, operator)) return VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot); - if (ELEMENT_OPERATORS.hasOwnProperty(operator)) { + if (hasOwn.call(ELEMENT_OPERATORS, operator)) { const options = ELEMENT_OPERATORS[operator]; return convertElementMatcherToBranchedMatcher( options.compileElementSelector(operand, valueSelector, matcher), @@ -1056,7 +1057,7 @@ export function pathsToTree(paths, newLeafFn, conflictFn, root = {}) { // use .every just for iteration with break const success = pathArray.slice(0, -1).every((key, i) => { - if (!tree.hasOwnProperty(key)) { + if (!hasOwn.call(tree, key)) { tree[key] = {}; } else if (tree[key] !== Object(tree[key])) { tree[key] = conflictFn( @@ -1077,7 +1078,7 @@ export function pathsToTree(paths, newLeafFn, conflictFn, root = {}) { if (success) { const lastKey = pathArray[pathArray.length - 1]; - if (tree.hasOwnProperty(lastKey)) + if (hasOwn.call(tree, lastKey)) tree[lastKey] = conflictFn(tree[lastKey], path, path); else tree[lastKey] = newLeafFn(path); diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index b3f215756f..7a0968099c 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -1,6 +1,7 @@ import Cursor from './cursor.js'; import ObserveHandle from './observe_handle.js'; import { + hasOwn, isIndexable, isNumericKey, isOperatorObject, @@ -92,7 +93,7 @@ export default class LocalCollection { // if you really want to use ObjectIDs, set this global. // Mongo.Collection specifies its own ids and does not use this code. - if (!doc.hasOwnProperty('_id')) + if (!hasOwn.call(doc, '_id')) doc._id = LocalCollection._useOID ? new MongoID.ObjectID() : Random.id(); const id = doc._id; @@ -618,7 +619,7 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks) ); - if (options.hasOwnProperty('ordered')) { + if (hasOwn.call(options, 'ordered')) { this.ordered = options.ordered; if (options.callbacks && options.ordered !== orderedFromCallbacks) @@ -723,7 +724,7 @@ LocalCollection.wrapTransform = transform => { return transform; const wrapped = doc => { - if (!doc.hasOwnProperty('_id')) { + if (!hasOwn.call(doc, '_id')) { // XXX do we ever have a transform on the oplog's collection? because that // collection has no _id. throw new Error('can only transform documents with _id'); @@ -738,7 +739,7 @@ LocalCollection.wrapTransform = transform => { if (!LocalCollection._isPlainObject(transformed)) throw new Error('transform must return object'); - if (transformed.hasOwnProperty('_id')) { + if (hasOwn.call(transformed, '_id')) { if (!EJSON.equals(transformed._id, id)) throw new Error('transformed document can\'t have different _id'); } else { @@ -794,7 +795,7 @@ LocalCollection._checkSupportedProjection = fields => { if (typeof value === 'object' && ['$elemMatch', '$meta', '$slice'].some(key => - value.hasOwnProperty(key) + hasOwn.call(value, key) )) { throw MinimongoError( 'Minimongo doesn\'t support operators in projections yet.' @@ -830,7 +831,7 @@ LocalCollection._compileProjection = fields => { const result = details.including ? {} : EJSON.clone(doc); Object.keys(ruleTree).forEach(key => { - if (!doc.hasOwnProperty(key)) + if (!hasOwn.call(doc, key)) return; const rule = ruleTree[key]; @@ -853,10 +854,10 @@ LocalCollection._compileProjection = fields => { return doc => { const result = transform(doc, details.tree); - if (_idProjection && doc.hasOwnProperty('_id')) + if (_idProjection && hasOwn.call(doc, '_id')) result._id = doc._id; - if (!_idProjection && result.hasOwnProperty('_id')) + if (!_idProjection && hasOwn.call(result, '_id')) delete result._id; return result; @@ -941,7 +942,7 @@ LocalCollection._idsMatchedBySelector = selector => { return null; // Do we have an _id clause? - if (selector.hasOwnProperty('_id')) { + if (hasOwn.call(selector, '_id')) { // Is the _id clause just an ID? if (LocalCollection._selectorIsId(selector._id)) return [selector._id]; @@ -1366,7 +1367,7 @@ LocalCollection._updateInResults = (query, doc, old_doc) => { const MODIFIERS = { $currentDate(target, field, arg) { - if (typeof arg === 'object' && arg.hasOwnProperty('$type')) { + if (typeof arg === 'object' && hasOwn.call(arg, '$type')) { if (arg.$type !== 'date') { throw MinimongoError( 'Minimongo does currently only support the date type in ' + diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index 25a0ce38a4..b450c0cf58 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -1,6 +1,7 @@ import LocalCollection from './local_collection.js'; import { compileDocumentSelector, + hasOwn, nothingMatcher, } from './common.js'; @@ -97,7 +98,7 @@ export default class Matcher { // protect against dangerous selectors. falsey and {_id: falsey} are both // likely programmer error, and not what you want, particularly for // destructive operations. - if (!selector || selector.hasOwnProperty('_id') && !selector._id) { + if (!selector || hasOwn.call(selector, '_id') && !selector._id) { this._isSimple = false; return nothingMatcher; } diff --git a/packages/minimongo/minimongo_server.js b/packages/minimongo/minimongo_server.js index f0a2dd9896..0758088c68 100644 --- a/packages/minimongo/minimongo_server.js +++ b/packages/minimongo/minimongo_server.js @@ -197,13 +197,13 @@ Minimongo.Matcher.prototype.matchingDocument = function() { let upperBound = Infinity; ['$lte', '$lt'].forEach(op => { - if (valueSelector.hasOwnProperty(op) && + if (hasOwn.call(valueSelector, op) && valueSelector[op] < upperBound) upperBound = valueSelector[op]; }); ['$gte', '$gt'].forEach(op => { - if (valueSelector.hasOwnProperty(op) && + if (hasOwn.call(valueSelector, op) && valueSelector[op] > lowerBound) lowerBound = valueSelector[op]; }); diff --git a/packages/minimongo/minimongo_tests_client.js b/packages/minimongo/minimongo_tests_client.js index 8f1517e58f..430b4c084e 100644 --- a/packages/minimongo/minimongo_tests_client.js +++ b/packages/minimongo/minimongo_tests_client.js @@ -1,3 +1,5 @@ +import {hasOwn} from './common'; + // Hack to make LocalCollection generate ObjectIDs by default. LocalCollection._useOID = true; @@ -1602,8 +1604,8 @@ Tinytest.add('minimongo - fetch with fields', test => { x.anything && x.anything.foo && x.anything.foo === 'bar' && - !x.hasOwnProperty('nothing') && - !x.anything.hasOwnProperty('cool'))); + !hasOwn.call(x, 'nothing') && + !hasOwn.call(x.anything, 'cool'))); // Test with a selector, even field used in the selector is excluded in the // projection @@ -1618,7 +1620,7 @@ Tinytest.add('minimongo - fetch with fields', test => { x.anything && x.anything.foo === 'bar' && x.anything.cool === 'hot' && - !x.hasOwnProperty('nothing') && + !hasOwn.call(x, 'nothing') && x.i && x.i >= 5)); @@ -2101,7 +2103,7 @@ Tinytest.add('minimongo - array sort', test => { const testCursorMatchesField = (cursor, field) => { const fieldValues = []; c.find().forEach(doc => { - if (doc.hasOwnProperty(field)) {fieldValues.push(doc[field]);} + if (hasOwn.call(doc, field)) {fieldValues.push(doc[field]);} }); test.equal(cursor.fetch().map(doc => doc[field]), Array.from({length: Math.max(...fieldValues) + 1}, (x, i) => i)); diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index 6d90ec112a..f1f95f3889 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -2,6 +2,7 @@ import { ELEMENT_OPERATORS, equalityElementMatcher, expandArraysInBranches, + hasOwn, isOperatorObject, makeLookupFunction, regexpElementMatcher, @@ -162,7 +163,7 @@ export default class Sorter { const path = pathFromIndices(branch.arrayIndices); - if (element.hasOwnProperty(path)) + if (hasOwn.call(element, path)) throw Error(`duplicate path: ${path}`); element[path] = branch.value; @@ -177,14 +178,14 @@ export default class Sorter { // and 'a.x.y' are both arrays, but we don't allow this for now. // #NestedArraySort // XXX achieve full compatibility here - if (knownPaths && !knownPaths.hasOwnProperty(path)) + if (knownPaths && !hasOwn.call(knownPaths, path)) throw Error('cannot index parallel arrays'); }); if (knownPaths) { // Similarly to above, paths must match everywhere, unless this is a // non-array field. - if (!element.hasOwnProperty('') && + if (!hasOwn.call(element, '') && Object.keys(knownPaths).length !== Object.keys(element).length) throw Error('cannot index parallel arrays!'); } else if (usedPaths) { @@ -201,7 +202,7 @@ export default class Sorter { if (!knownPaths) { // Easy case: no use of arrays. const soleKey = valuesByIndexAndPath.map(values => { - if (!values.hasOwnProperty('')) + if (!hasOwn.call(values, '')) throw Error('no value in sole key case?'); return values['']; @@ -214,10 +215,10 @@ export default class Sorter { Object.keys(knownPaths).forEach(path => { const key = valuesByIndexAndPath.map(values => { - if (values.hasOwnProperty('')) + if (hasOwn.call(values, '')) return values['']; - if (!values.hasOwnProperty(path)) + if (!hasOwn.call(values, path)) throw Error('missing path?'); return values[path]; From 8b43925d1f8a4e06d06646e0d84553687c7014a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 9 Aug 2017 17:12:24 +0200 Subject: [PATCH 27/28] Refactored braces. --- packages/minimongo/common.js | 214 +++++++++----- packages/minimongo/cursor.js | 56 ++-- packages/minimongo/local_collection.js | 374 ++++++++++++++++--------- packages/minimongo/matcher.js | 69 +++-- packages/minimongo/minimongo_server.js | 53 ++-- packages/minimongo/package.js | 4 +- packages/minimongo/sorter.js | 92 ++++-- 7 files changed, 572 insertions(+), 290 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index 88b22f1264..e4e6f82ccf 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -23,8 +23,9 @@ export const ELEMENT_OPERATORS = { compileElementSelector(operand) { if (!(Array.isArray(operand) && operand.length === 2 && typeof operand[0] === 'number' - && typeof operand[1] === 'number')) + && typeof operand[1] === 'number')) { throw Error('argument to $mod must be an array of two numbers'); + } // XXX could require to be ints or round or something const divisor = operand[0]; @@ -36,21 +37,27 @@ export const ELEMENT_OPERATORS = { }, $in: { compileElementSelector(operand) { - if (!Array.isArray(operand)) + if (!Array.isArray(operand)) { throw Error('$in needs an array'); + } const elementMatchers = operand.map(option => { - if (option instanceof RegExp) + if (option instanceof RegExp) { return regexpElementMatcher(option); - if (isOperatorObject(option)) + } + + if (isOperatorObject(option)) { throw Error('cannot nest $ under $in'); + } + return equalityElementMatcher(option); }); return value => { // Allow {a: {$in: [null]}} to match when 'a' does not exist. - if (value === undefined) + if (value === undefined) { value = null; + } return elementMatchers.some(matcher => matcher(value)); }; @@ -66,8 +73,9 @@ export const ELEMENT_OPERATORS = { // Don't ask me why, but by experimentation, this seems to be what Mongo // does. operand = 0; - } else if (typeof operand !== 'number') + } else if (typeof operand !== 'number') { throw Error('$size needs a number'); + } return value => Array.isArray(value) && value.length === operand; }, @@ -79,8 +87,10 @@ export const ELEMENT_OPERATORS = { // should *not* include it itself. dontIncludeLeafArrays: true, compileElementSelector(operand) { - if (typeof operand !== 'number') + if (typeof operand !== 'number') { throw Error('$type needs a number'); + } + return value => ( value !== undefined && LocalCollection._f._type(value) === operand ); @@ -124,8 +134,9 @@ export const ELEMENT_OPERATORS = { }, $regex: { compileElementSelector(operand, valueSelector) { - if (!(typeof operand === 'string' || operand instanceof RegExp)) + if (!(typeof operand === 'string' || operand instanceof RegExp)) { throw Error('$regex has to be a string or RegExp'); + } let regexp; if (valueSelector.$options !== undefined) { @@ -135,8 +146,9 @@ export const ELEMENT_OPERATORS = { // Be clear that we only support the JS-supported options, not extended // ones (eg, Mongo supports x and s). Ideally we would implement x and s // by transforming the regexp, but not today... - if (/[^gim]/.test(valueSelector.$options)) + if (/[^gim]/.test(valueSelector.$options)) { throw new Error('Only the i, m, and g regexp options are supported'); + } const source = operand instanceof RegExp ? operand.source : operand; regexp = new RegExp(source, valueSelector.$options); @@ -152,8 +164,9 @@ export const ELEMENT_OPERATORS = { $elemMatch: { dontExpandLeafArrays: true, compileElementSelector(operand, valueSelector, matcher) { - if (!LocalCollection._isPlainObject(operand)) + if (!LocalCollection._isPlainObject(operand)) { throw Error('$elemMatch need an object'); + } const isDocMatcher = !isOperatorObject( Object.keys(operand) @@ -173,8 +186,9 @@ export const ELEMENT_OPERATORS = { } return value => { - if (!Array.isArray(value)) + if (!Array.isArray(value)) { return false; + } for (let i = 0; i < value.length; ++i) { const arrayElement = value[i]; @@ -183,8 +197,10 @@ export const ELEMENT_OPERATORS = { // We can only match {$elemMatch: {b: 3}} against objects. // (We can also match against arrays, if there's numeric indices, // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.) - if (!isIndexable(arrayElement)) + if (!isIndexable(arrayElement)) { return false; + } + arg = arrayElement; } else { // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches @@ -192,8 +208,9 @@ export const ELEMENT_OPERATORS = { arg = [{value: arrayElement, dontIterate: true}]; } // XXX support $near in $elemMatch by propagating $distance? - if (subMatcher(arg).result) + if (subMatcher(arg).result) { return i; // specially understood to mean "use as arrayIndices" + } } return false; @@ -219,8 +236,9 @@ const LOGICAL_OPERATORS = { // Special case: if there is only one matcher, use it directly, *preserving* // any arrayIndices it returns. - if (matchers.length === 1) + if (matchers.length === 1) { return matchers[0]; + } return doc => { const result = matchers.some(fn => fn(doc).result); @@ -300,28 +318,35 @@ const VALUE_OPERATORS = { }, // $options just provides options for $regex; its logic is inside $regex $options(operand, valueSelector) { - if (!hasOwn.call(valueSelector, '$regex')) + if (!hasOwn.call(valueSelector, '$regex')) { throw Error('$options needs a $regex'); + } + return everythingMatcher; }, // $maxDistance is basically an argument to $near $maxDistance(operand, valueSelector) { - if (!valueSelector.$near) + if (!valueSelector.$near) { throw Error('$maxDistance needs a $near'); + } + return everythingMatcher; }, $all(operand, valueSelector, matcher) { - if (!Array.isArray(operand)) + if (!Array.isArray(operand)) { throw Error('$all requires array'); + } // Not sure why, but this seems to be what MongoDB does. - if (operand.length === 0) + if (operand.length === 0) { return nothingMatcher; + } const branchedMatchers = operand.map(criterion => { // XXX handle $all/$elemMatch combination - if (isOperatorObject(criterion)) + if (isOperatorObject(criterion)) { throw Error('no $ expressions in $all'); + } // This is always a regexp or equality selector. return compileValueSelector(criterion, matcher); @@ -332,8 +357,9 @@ const VALUE_OPERATORS = { return andBranchedMatchers(branchedMatchers); }, $near(operand, valueSelector, matcher, isRoot) { - if (!isRoot) + if (!isRoot) { throw Error('$near can\'t be inside another $ operator'); + } matcher._hasGeoQuery = true; @@ -350,17 +376,20 @@ const VALUE_OPERATORS = { // XXX: for now, we don't calculate the actual distance between, say, // polygon and circle. If people care about this use-case it will get // a priority. - if (!value) + if (!value) { return null; + } - if (!value.type) + if (!value.type) { return GeoJSON.pointDistance( point, {type: 'Point', coordinates: pointToArray(value)} ); + } - if (value.type === 'Point') + if (value.type === 'Point') { return GeoJSON.pointDistance(point, value); + } return GeoJSON.geometryWithinRadius(value, point, maxDistance) ? 0 @@ -369,14 +398,17 @@ const VALUE_OPERATORS = { } else { maxDistance = valueSelector.$maxDistance; - if (!isIndexable(operand)) + if (!isIndexable(operand)) { throw Error('$near argument must be coordinate pair or GeoJSON'); + } point = pointToArray(operand); distance = value => { - if (!isIndexable(value)) + if (!isIndexable(value)) { return null; + } + return distanceCoordinatePairs(point, value); }; } @@ -396,27 +428,31 @@ const VALUE_OPERATORS = { // one (#3599) let curDistance; if (!matcher._isUpdate) { - if (!(typeof branch.value === 'object')) + if (!(typeof branch.value === 'object')) { return true; + } curDistance = distance(branch.value); // Skip branches that aren't real points or are too far away. - if (curDistance === null || curDistance > maxDistance) + if (curDistance === null || curDistance > maxDistance) { return true; + } // Skip anything that's a tie. - if (result.distance !== undefined && result.distance <= curDistance) + if (result.distance !== undefined && result.distance <= curDistance) { return true; + } } result.result = true; result.distance = curDistance; - if (branch.arrayIndices) + if (branch.arrayIndices) { result.arrayIndices = branch.arrayIndices; - else + } else { delete result.arrayIndices; + } return !matcher._isUpdate; }); @@ -431,11 +467,13 @@ const VALUE_OPERATORS = { // but the argument is different: for the former it's a whole doc, whereas for // the latter it's an array of 'branched values'. function andSomeMatchers(subMatchers) { - if (subMatchers.length === 0) + if (subMatchers.length === 0) { return everythingMatcher; + } - if (subMatchers.length === 1) + if (subMatchers.length === 1) { return subMatchers[0]; + } return docOrBranches => { const match = {}; @@ -448,14 +486,16 @@ function andSomeMatchers(subMatchers) { // Mongo. if (subResult.result && subResult.distance !== undefined && - match.distance === undefined) + match.distance === undefined) { match.distance = subResult.distance; + } // Similarly, propagate arrayIndices from sub-matchers... but to match // MongoDB behavior, this time the *last* sub-matcher with arrayIndices // wins. - if (subResult.result && subResult.arrayIndices) + if (subResult.result && subResult.arrayIndices) { match.arrayIndices = subResult.arrayIndices; + } return subResult.result; }); @@ -474,12 +514,14 @@ const andDocumentMatchers = andSomeMatchers; const andBranchedMatchers = andSomeMatchers; function compileArrayOfDocumentSelectors(selectors, matcher, inElemMatch) { - if (!Array.isArray(selectors) || selectors.length === 0) + if (!Array.isArray(selectors) || selectors.length === 0) { throw Error('$and/$or/$nor must be nonempty array'); + } return selectors.map(subSelector => { - if (!LocalCollection._isPlainObject(subSelector)) + if (!LocalCollection._isPlainObject(subSelector)) { throw Error('$or/$and/$nor entries need to be full objects'); + } return compileDocumentSelector(subSelector, matcher, {inElemMatch}); }); @@ -499,14 +541,16 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) { // Don't add a matcher if subSelector is a function -- this is to match // the behavior of Meteor on the server (inherited from the node mongodb // driver), which is to ignore any part of a selector which is a function. - if (typeof subSelector === 'function') + if (typeof subSelector === 'function') { return undefined; + } if (key.substr(0, 1) === '$') { // Outer operators are either logical operators (they recurse back into // this function), or $where. - if (!hasOwn.call(LOGICAL_OPERATORS, key)) + if (!hasOwn.call(LOGICAL_OPERATORS, key)) { throw new Error(`Unrecognized logical operator: ${key}`); + } matcher._isSimple = false; return LOGICAL_OPERATORS[key](subSelector, matcher, options.inElemMatch); @@ -515,8 +559,9 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) { // Record this path, but only if we aren't in an elemMatcher, since in an // elemMatch this is a path inside an object in an array, not in the doc // root. - if (!options.inElemMatch) + if (!options.inElemMatch) { matcher._recordPathUsed(key); + } const lookUpByIndex = makeLookupFunction(key); const valueMatcher = compileValueSelector( @@ -543,8 +588,9 @@ function compileValueSelector(valueSelector, matcher, isRoot) { ); } - if (isOperatorObject(valueSelector)) + if (isOperatorObject(valueSelector)) { return operatorBranchedMatcher(valueSelector, matcher, isRoot); + } return convertElementMatcherToBranchedMatcher( equalityElementMatcher(valueSelector) @@ -570,16 +616,18 @@ function convertElementMatcherToBranchedMatcher(elementMatcher, options = {}) { // XXX This code dates from when we only stored a single array index // (for the outermost array). Should we be also including deeper array // indices from the $elemMatch match? - if (!element.arrayIndices) + if (!element.arrayIndices) { element.arrayIndices = [matched]; + } matched = true; } // If some element matched, and it's tagged with array indices, include // those indices in our result object. - if (matched && element.arrayIndices) + if (matched && element.arrayIndices) { match.arrayIndices = element.arrayIndices; + } return matched; }); @@ -599,8 +647,9 @@ function distanceCoordinatePairs(a, b) { // Takes something that is not an operator object and returns an element matcher // for equality with that thing. export function equalityElementMatcher(elementSelector) { - if (isOperatorObject(elementSelector)) + if (isOperatorObject(elementSelector)) { throw Error('Can\'t create equalityValueSelector for operator object'); + } // Special-case: null and undefined are equal (if you got undefined in there // somewhere, or if you got it due to some branch being non-existent in the @@ -627,8 +676,9 @@ export function expandArraysInBranches(branches, skipTheArrays) { // to iterate and we're told to skip arrays. (That's right, we include some // arrays even skipTheArrays is true: these are arrays that were found via // explicit numerical indices.) - if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) + if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) { branchesOut.push({arrayIndices: branch.arrayIndices, value: branch.value}); + } if (thisIsArray && !branch.dontIterate) { branch.value.forEach((value, i) => { @@ -649,13 +699,15 @@ function getOperandBitmask(operand, selector) { // You can provide a numeric bitmask to be matched against the operand field. // It must be representable as a non-negative 32-bit signed integer. // Otherwise, $bitsAllSet will return an error. - if (Number.isInteger(operand) && operand >= 0) + if (Number.isInteger(operand) && operand >= 0) { return new Uint8Array(new Int32Array([operand]).buffer); + } // bindata bitmask // You can also use an arbitrarily large BinData instance as a bitmask. - if (EJSON.isBinary(operand)) + if (EJSON.isBinary(operand)) { return new Uint8Array(operand.buffer); + } // position list // If querying a list of bit positions, each must be a non-negative @@ -710,8 +762,9 @@ function getValueBitmask(value, length) { } // bindata - if (EJSON.isBinary(value)) + if (EJSON.isBinary(value)) { return new Uint8Array(value.buffer); + } // no match return false; @@ -764,8 +817,9 @@ export function isNumericKey(s) { // with $. Unless inconsistentOK is set, throws if some keys begin with $ and // others don't. export function isOperatorObject(valueSelector, inconsistentOK) { - if (!LocalCollection._isPlainObject(valueSelector)) + if (!LocalCollection._isPlainObject(valueSelector)) { return false; + } let theseAreOperators = undefined; Object.keys(valueSelector).forEach(selKey => { @@ -795,24 +849,28 @@ function makeInequality(cmpValueComparator) { // XXX This was behavior we observed in pre-release MongoDB 2.5, but // it seems to have been reverted. // See https://jira.mongodb.org/browse/SERVER-11444 - if (Array.isArray(operand)) + if (Array.isArray(operand)) { return () => false; + } // Special case: consider undefined and null the same (so true with // $gte/$lte). - if (operand === undefined) + if (operand === undefined) { operand = null; + } const operandType = LocalCollection._f._type(operand); return value => { - if (value === undefined) + if (value === undefined) { value = null; + } // Comparisons are never true among things of different type (except // null vs undefined). - if (LocalCollection._f._type(value) !== operandType) + if (LocalCollection._f._type(value) !== operandType) { return false; + } return cmpValueComparator(LocalCollection._f._cmp(value, operand)); }; @@ -881,11 +939,13 @@ export function makeLookupFunction(key, options = {}) { ); const omitUnnecessaryFields = result => { - if (!result.dontIterate) + if (!result.dontIterate) { delete result.dontIterate; + } - if (result.arrayIndices && !result.arrayIndices.length) + if (result.arrayIndices && !result.arrayIndices.length) { delete result.arrayIndices; + } return result; }; @@ -897,8 +957,9 @@ export function makeLookupFunction(key, options = {}) { // If we're being asked to do an invalid lookup into an array (non-integer // or out-of-bounds), return no results (which is different from returning // a single undefined result, in that `null` equality checks won't match). - if (!(isNumericKey(firstPart) && firstPart < doc.length)) + if (!(isNumericKey(firstPart) && firstPart < doc.length)) { return []; + } // Remember that we used this array index. Include an 'x' to indicate that // the previous index came from being considered as an explicit array @@ -936,8 +997,9 @@ export function makeLookupFunction(key, options = {}) { // return a single `undefined` (which can, for example, match via equality // with `null`). if (!isIndexable(firstLevel)) { - if (Array.isArray(doc)) + if (Array.isArray(doc)) { return []; + } return [omitUnnecessaryFields({arrayIndices, value: undefined})]; } @@ -970,8 +1032,9 @@ export function makeLookupFunction(key, options = {}) { if (Array.isArray(firstLevel) && !(isNumericKey(parts[1]) && options.forSort)) { firstLevel.forEach((branch, arrayIndex) => { - if (LocalCollection._isPlainObject(branch)) + if (LocalCollection._isPlainObject(branch)) { appendToResult(lookupRest(branch, arrayIndices.concat(arrayIndex))); + } }); } @@ -1021,11 +1084,13 @@ function operatorBranchedMatcher(valueSelector, matcher, isRoot) { && !operand.some(x => x === Object(x)) ); - if (!(simpleRange || simpleInclusion || simpleEquality)) + if (!(simpleRange || simpleInclusion || simpleEquality)) { matcher._isSimple = false; + } - if (hasOwn.call(VALUE_OPERATORS, operator)) + if (hasOwn.call(VALUE_OPERATORS, operator)) { return VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot); + } if (hasOwn.call(ELEMENT_OPERATORS, operator)) { const options = ELEMENT_OPERATORS[operator]; @@ -1067,8 +1132,9 @@ export function pathsToTree(paths, newLeafFn, conflictFn, root = {}) { ); // break out of loop if we are failing for this path - if (tree[key] !== Object(tree[key])) + if (tree[key] !== Object(tree[key])) { return false; + } } tree = tree[key]; @@ -1078,10 +1144,11 @@ export function pathsToTree(paths, newLeafFn, conflictFn, root = {}) { if (success) { const lastKey = pathArray[pathArray.length - 1]; - if (hasOwn.call(tree, lastKey)) + if (hasOwn.call(tree, lastKey)) { tree[lastKey] = conflictFn(tree[lastKey], path, path); - else + } else { tree[lastKey] = newLeafFn(path); + } } }); @@ -1132,8 +1199,9 @@ function populateDocumentWithObject(document, key, value) { if (unprefixedKeys.length > 0 || !keys.length) { // Literal (possibly empty) object ( or empty object ) // Don't allow mixing '$'-prefixed with non-'$'-prefixed fields - if (keys.length !== unprefixedKeys.length) + if (keys.length !== unprefixedKeys.length) { throw new Error(`unknown operator: ${unprefixedKeys[0]}`); + } validateObject(value, key); insertIntoDocument(document, key, value); @@ -1167,17 +1235,19 @@ export function populateDocumentWithQueryFields(query, document = {}) { ); } else if (key === '$or') { // handle $or nodes with exactly 1 child - if (value.length === 1) + if (value.length === 1) { populateDocumentWithQueryFields(value[0], document); + } } else if (key[0] !== '$') { // Ignore other '$'-prefixed logical selectors populateDocumentWithKeyValue(document, key, value); } - }) + }); } else { // Handle meteor-specific shortcut for selecting _id - if (LocalCollection._selectorIsId(query)) + if (LocalCollection._selectorIsId(query)) { insertIntoDocument(document, '_id', query); + } } return document; @@ -1202,16 +1272,18 @@ export function projectionDetails(fields) { // projection and is exclusive, remove it so it can be handled later by a // special case, since exclusive _id is always allowed. if (!(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') && - !(fieldsKeys.includes('_id') && fields._id)) + !(fieldsKeys.includes('_id') && fields._id)) { fieldsKeys = fieldsKeys.filter(key => key !== '_id'); + } let including = null; // Unknown fieldsKeys.forEach(keyPath => { const rule = !!fields[keyPath]; - if (including === null) + if (including === null) { including = rule; + } // This error message is copied from MongoDB shell if (including !== rule) { @@ -1256,12 +1328,14 @@ export function projectionDetails(fields) { // Takes a RegExp object and returns an element matcher. export function regexpElementMatcher(regexp) { return value => { - if (value instanceof RegExp) + if (value instanceof RegExp) { return value.toString() === regexp.toString(); + } // Regexps only work against strings. - if (typeof value !== 'string') + if (typeof value !== 'string') { return false; + } // Reset regexp's state to avoid inconsistent matching for objects with the // same value on consecutive calls of regexp.test. This happens only if the diff --git a/packages/minimongo/cursor.js b/packages/minimongo/cursor.js index b44d404ebe..6655f1f3ea 100644 --- a/packages/minimongo/cursor.js +++ b/packages/minimongo/cursor.js @@ -15,11 +15,12 @@ export default class Cursor { } else { this._selectorId = undefined; - if (this.matcher.hasGeoQuery() || options.sort) + if (this.matcher.hasGeoQuery() || options.sort) { this.sorter = new Minimongo.Sorter( options.sort || [], {matcher: this.matcher} ); + } } this.skip = options.skip || 0; @@ -31,8 +32,9 @@ export default class Cursor { this._transform = LocalCollection.wrapTransform(options.transform); // by default, queries register w/ Tracker when it is available. - if (typeof Tracker !== 'undefined') + if (typeof Tracker !== 'undefined') { this.reactive = options.reactive === undefined ? true : options.reactive; + } } /** @@ -44,9 +46,10 @@ export default class Cursor { * @returns {Number} */ count() { - if (this.reactive) + if (this.reactive) { // allow the observe to be unordered this._depend({added: true, removed: true}, true); + } return this._getRawObjects({ordered: true}).length; } @@ -101,8 +104,9 @@ export default class Cursor { // This doubles as a clone operation. element = this._projectionFn(element); - if (this._transform) + if (this._transform) { element = this._transform(element); + } callback.call(thisArg, element, i, this); }); @@ -185,14 +189,16 @@ export default class Cursor { // unordered observe. eg, update's EJSON.clone, and the "there are several" // comment in _modifyAndNotify // XXX allow skip/limit with unordered observe - if (!options._allow_unordered && !ordered && (this.skip || this.limit)) + if (!options._allow_unordered && !ordered && (this.skip || this.limit)) { throw new Error( 'must use ordered observe (ie, \'addedBefore\' instead of \'added\') ' + 'with skip or limit' ); + } - if (this.fields && (this.fields._id === 0 || this.fields._id === false)) + if (this.fields && (this.fields._id === 0 || this.fields._id === false)) { throw Error('You may not observe a cursor with {fields: {_id: 0}}'); + } const distances = ( this.matcher.hasGeoQuery() && @@ -222,8 +228,9 @@ export default class Cursor { query.results = this._getRawObjects({ordered, distances: query.distances}); - if (this.collection.paused) + if (this.collection.paused) { query.resultsSnapshot = ordered ? [] : new LocalCollection._IdMap; + } // wrap callbacks we were passed. callbacks only fire when not paused and // are never undefined @@ -233,13 +240,15 @@ export default class Cursor { // furthermore, callbacks enqueue until the operation we're working on is // done. const wrapCallback = fn => { - if (!fn) + if (!fn) { return () => {}; + } const self = this; return function(/* args*/) { - if (self.collection.paused) + if (self.collection.paused) { return; + } const args = arguments; @@ -267,8 +276,9 @@ export default class Cursor { delete fields._id; - if (ordered) + if (ordered) { query.addedBefore(doc._id, this._projectionFn(fields), null); + } query.added(doc._id, this._projectionFn(fields)); }); @@ -277,8 +287,9 @@ export default class Cursor { const handle = Object.assign(new LocalCollection.ObserveHandle, { collection: this.collection, stop: () => { - if (this.reactive) + if (this.reactive) { delete this.collection.queries[qid]; + } } }); @@ -319,8 +330,9 @@ export default class Cursor { ['added', 'addedBefore', 'changed', 'movedBefore', 'removed'] .forEach(fn => { - if (changers[fn]) + if (changers[fn]) { options[fn] = notify; + } }); // observeChanges will stop() when this computation is invalidated @@ -358,16 +370,18 @@ export default class Cursor { // If you have non-zero skip and ask for a single id, you get // nothing. This is so it matches the behavior of the '{_id: foo}' // path. - if (this.skip) + if (this.skip) { return results; + } const selectedDoc = this.collection._docs.get(this._selectorId); if (selectedDoc) { - if (options.ordered) + if (options.ordered) { results.push(selectedDoc); - else + } else { results.set(this._selectorId, selectedDoc); + } } return results; @@ -395,8 +409,9 @@ export default class Cursor { if (options.ordered) { results.push(doc); - if (distances && matchResult.distance !== undefined) + if (distances && matchResult.distance !== undefined) { distances.set(id, matchResult.distance); + } } else { results.set(id, doc); } @@ -412,14 +427,17 @@ export default class Cursor { ); }); - if (!options.ordered) + if (!options.ordered) { return results; + } - if (this.sorter) + if (this.sorter) { results.sort(this.sorter.getComparator({distances})); + } - if (!this.limit && !this.skip) + if (!this.limit && !this.skip) { return results; + } return results.slice( this.skip, diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 7a0968099c..17a2ba5216 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -61,15 +61,17 @@ export default class LocalCollection { // default syntax for everything is to omit the selector argument. // but if selector is explicitly passed in as false or undefined, we // want a selector that matches nothing. - if (arguments.length === 0) + if (arguments.length === 0) { selector = {}; + } return new LocalCollection.Cursor(this, selector, options); } findOne(selector, options = {}) { - if (arguments.length === 0) + if (arguments.length === 0) { selector = {}; + } // NOTE: by setting limit 1 here, we end up using very inefficient // code that recomputes the whole query on each update. The upside is @@ -93,13 +95,15 @@ export default class LocalCollection { // if you really want to use ObjectIDs, set this global. // Mongo.Collection specifies its own ids and does not use this code. - if (!hasOwn.call(doc, '_id')) + if (!hasOwn.call(doc, '_id')) { doc._id = LocalCollection._useOID ? new MongoID.ObjectID() : Random.id(); + } const id = doc._id; - if (this._docs.has(id)) + if (this._docs.has(id)) { throw MinimongoError(`Duplicate _id '${id}'`); + } this._saveOriginal(id, undefined); this._docs.set(id, doc); @@ -110,25 +114,29 @@ export default class LocalCollection { for (let qid in this.queries) { const query = this.queries[qid]; - if (query.dirty) + if (query.dirty) { continue; + } const matchResult = query.matcher.documentMatches(doc); if (matchResult.result) { - if (query.distances && matchResult.distance !== undefined) + if (query.distances && matchResult.distance !== undefined) { query.distances.set(id, matchResult.distance); + } - if (query.cursor.skip || query.cursor.limit) + if (query.cursor.skip || query.cursor.limit) { queriesToRecompute.push(qid); - else + } else { LocalCollection._insertInResults(query, doc); + } } } queriesToRecompute.forEach(qid => { - if (this.queries[qid]) + if (this.queries[qid]) { this._recomputeResults(this.queries[qid]); + } }); this._observeQueue.drain(); @@ -148,8 +156,9 @@ export default class LocalCollection { // 'resumeObservers' is called. pauseObservers() { // No-op if already paused. - if (this.paused) + if (this.paused) { return; + } // Set the 'paused' flag such that new observer messages don't fire. this.paused = true; @@ -174,10 +183,11 @@ export default class LocalCollection { for (let qid in this.queries) { const query = this.queries[qid]; - if (query.ordered) + if (query.ordered) { query.results = []; - else + } else { query.results.clear(); + } } if (callback) { @@ -193,8 +203,9 @@ export default class LocalCollection { const remove = []; this._eachPossiblyMatchingDoc(selector, (doc, id) => { - if (matcher.documentMatches(doc).result) + if (matcher.documentMatches(doc).result) { remove.push(id); + } }); const queriesToRecompute = []; @@ -207,14 +218,16 @@ export default class LocalCollection { for (let qid in this.queries) { const query = this.queries[qid]; - if (query.dirty) + if (query.dirty) { return; + } if (query.matcher.documentMatches(removeDoc).result) { - if (query.cursor.skip || query.cursor.limit) + if (query.cursor.skip || query.cursor.limit) { queriesToRecompute.push(qid); - else + } else { queryRemove.push({qid, doc: removeDoc}); + } } } @@ -235,8 +248,9 @@ export default class LocalCollection { queriesToRecompute.forEach(qid => { const query = this.queries[qid]; - if (query) + if (query) { this._recomputeResults(query); + } }); this._observeQueue.drain(); @@ -258,8 +272,9 @@ export default class LocalCollection { // happened during the pause, it is a smarter 'coalesced' diff. resumeObservers() { // No-op if not paused. - if (!this.paused) + if (!this.paused) { return; + } // Unset the 'paused' flag. Make sure to do this first, otherwise // observer methods won't actually fire when we trigger them. @@ -293,8 +308,9 @@ export default class LocalCollection { } retrieveOriginals() { - if (!this._savedOriginals) + if (!this._savedOriginals) { throw new Error('Called retrieveOriginals without saveOriginals'); + } const originals = this._savedOriginals; @@ -311,8 +327,9 @@ export default class LocalCollection { // of an inserted document, undefined is the value.) You must alternate // between calls to saveOriginals() and retrieveOriginals(). saveOriginals() { - if (this._savedOriginals) + if (this._savedOriginals) { throw new Error('Called saveOriginals twice without retrieveOriginals'); + } this._savedOriginals = new LocalCollection._IdMap; } @@ -325,8 +342,9 @@ export default class LocalCollection { options = null; } - if (!options) + if (!options) { options = {}; + } const matcher = new Minimongo.Matcher(selector, true); @@ -356,16 +374,18 @@ export default class LocalCollection { return; } - if (!(query.results instanceof Array)) + if (!(query.results instanceof Array)) { throw new Error('Assertion failed: query.results not an array'); + } // Clones a document to be stored in `qidToOriginalResults` // because it may be modified before the new and old result sets // are diffed. But if we know exactly which document IDs we're // going to modify, then we only need to clone those. const memoizedCloneIfNeeded = doc => { - if (docMap.has(doc._id)) + if (docMap.has(doc._id)) { return docMap.get(doc._id); + } const docToMemoize = ( idsMatched && @@ -400,8 +420,9 @@ export default class LocalCollection { ++updateCount; - if (!options.multi) + if (!options.multi) { return false; // break + } } return true; @@ -410,8 +431,9 @@ export default class LocalCollection { Object.keys(recomputeQids).forEach(qid => { const query = this.queries[qid]; - if (query) + if (query) { this._recomputeResults(query, qidToOriginalResults[qid]); + } }); this._observeQueue.drain(); @@ -422,8 +444,9 @@ export default class LocalCollection { let insertedId; if (updateCount === 0 && options.upsert) { const doc = LocalCollection._createUpsertDocument(selector, mod); - if (! doc._id && options.insertedId) + if (! doc._id && options.insertedId) { doc._id = options.insertedId; + } insertedId = this.insert(doc); updateCount = 1; @@ -436,8 +459,9 @@ export default class LocalCollection { if (options._returnObject) { result = {numberAffected: updateCount}; - if (insertedId !== undefined) + if (insertedId !== undefined) { result.insertedId = insertedId; + } } else { result = updateCount; } @@ -479,8 +503,9 @@ export default class LocalCollection { specificIds.some(id => { const doc = this._docs.get(id); - if (doc) + if (doc) { return fn(doc, id) === false; + } }); } else { this._docs.forEach(fn); @@ -493,8 +518,9 @@ export default class LocalCollection { for (let qid in this.queries) { const query = this.queries[qid]; - if (query.dirty) + if (query.dirty) { continue; + } if (query.ordered) { matched_before[qid] = query.matcher.documentMatches(doc).result; @@ -512,15 +538,17 @@ export default class LocalCollection { for (let qid in this.queries) { const query = this.queries[qid]; - if (query.dirty) + if (query.dirty) { continue; + } const afterMatch = query.matcher.documentMatches(doc); const after = afterMatch.result; const before = matched_before[qid]; - if (after && query.distances && afterMatch.distance !== undefined) + if (after && query.distances && afterMatch.distance !== undefined) { query.distances.set(doc._id, afterMatch.distance); + } if (query.cursor.skip || query.cursor.limit) { // We need to recompute any query where the doc may have been in the @@ -530,8 +558,9 @@ export default class LocalCollection { // applied... but if they are false, then the document definitely is NOT // in the output. So it's safe to skip recompute if neither before or // after are true.) - if (before || after) + if (before || after) { recomputeQids[qid] = true; + } } else if (before && !after) { LocalCollection._removeFromResults(query, doc); } else if (!before && after) { @@ -562,11 +591,13 @@ export default class LocalCollection { return; } - if (!this.paused && !oldResults) + if (!this.paused && !oldResults) { oldResults = query.results; + } - if (query.distances) + if (query.distances) { query.distances.clear(); + } query.results = query.cursor._getRawObjects({ distances: query.distances, @@ -586,14 +617,16 @@ export default class LocalCollection { _saveOriginal(id, doc) { // Are we even trying to save originals? - if (!this._savedOriginals) + if (!this._savedOriginals) { return; + } // Have we previously mutated the original (and so 'doc' is not actually // original)? (Note the 'has' check rather than truth: we store undefined // here for inserted docs!) - if (this._savedOriginals.has(id)) + if (this._savedOriginals.has(id)) { return; + } this._savedOriginals.set(id, EJSON.clone(doc)); } @@ -622,8 +655,9 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { if (hasOwn.call(options, 'ordered')) { this.ordered = options.ordered; - if (options.callbacks && options.ordered !== orderedFromCallbacks) + if (options.callbacks && options.ordered !== orderedFromCallbacks) { throw Error('ordered option doesn\'t match callbacks'); + } } else if (options.callbacks) { this.ordered = orderedFromCallbacks; } else { @@ -640,12 +674,14 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { doc._id = id; - if (callbacks.addedBefore) + if (callbacks.addedBefore) { callbacks.addedBefore.call(this, id, fields, before); + } // This line triggers if we provide added with movedBefore. - if (callbacks.added) + if (callbacks.added) { callbacks.added.call(this, id, fields); + } // XXX could `before` be a falsy ID? Technically // idStringify seems to allow for them -- though @@ -655,8 +691,9 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { movedBefore: (id, before) => { const doc = this.docs.get(id); - if (callbacks.movedBefore) + if (callbacks.movedBefore) { callbacks.movedBefore.call(this, id, before); + } this.docs.moveBefore(id, before || null); }, @@ -667,8 +704,9 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { added: (id, fields) => { const doc = EJSON.clone(fields); - if (callbacks.added) + if (callbacks.added) { callbacks.added.call(this, id, fields); + } doc._id = id; @@ -682,18 +720,21 @@ LocalCollection._CachingChangeObserver = class _CachingChangeObserver { this.applyChange.changed = (id, fields) => { const doc = this.docs.get(id); - if (!doc) + if (!doc) { throw new Error(`Unknown id for changed: ${id}`); + } - if (callbacks.changed) + if (callbacks.changed) { callbacks.changed.call(this, id, EJSON.clone(fields)); + } DiffSequence.applyChanges(doc, fields); }; this.applyChange.removed = id => { - if (callbacks.removed) + if (callbacks.removed) { callbacks.removed.call(this, id); + } this.docs.remove(id); }; @@ -716,12 +757,14 @@ LocalCollection._IdMap = class _IdMap extends IdMap { // original _id field // - If the return value doesn't have an _id field, add it back. LocalCollection.wrapTransform = transform => { - if (!transform) + if (!transform) { return null; + } // No need to doubly-wrap transforms. - if (transform.__wrappedTransform__) + if (transform.__wrappedTransform__) { return transform; + } const wrapped = doc => { if (!hasOwn.call(doc, '_id')) { @@ -736,12 +779,14 @@ LocalCollection.wrapTransform = transform => { // Package.tracker here const transformed = Tracker.nonreactive(() => transform(doc)); - if (!LocalCollection._isPlainObject(transformed)) + if (!LocalCollection._isPlainObject(transformed)) { throw new Error('transform must return object'); + } if (hasOwn.call(transformed, '_id')) { - if (!EJSON.equals(transformed._id, id)) + if (!EJSON.equals(transformed._id, id)) { throw new Error('transformed document can\'t have different _id'); + } } else { transformed._id = id; } @@ -781,8 +826,9 @@ LocalCollection._binarySearch = (cmp, array, value) => { }; LocalCollection._checkSupportedProjection = fields => { - if (fields !== Object(fields) || Array.isArray(fields)) + if (fields !== Object(fields) || Array.isArray(fields)) { throw MinimongoError('fields option must be an object'); + } Object.keys(fields).forEach(keyPath => { if (keyPath.split('.').includes('$')) { @@ -802,10 +848,11 @@ LocalCollection._checkSupportedProjection = fields => { ); } - if (![1, 0, true, false].includes(value)) + if (![1, 0, true, false].includes(value)) { throw MinimongoError( 'Projection values should be one of 1, 0, true, or false' ); + } }); }; @@ -825,21 +872,24 @@ LocalCollection._compileProjection = fields => { // returns transformed doc according to ruleTree const transform = (doc, ruleTree) => { // Special case for "sets" - if (Array.isArray(doc)) + if (Array.isArray(doc)) { return doc.map(subdoc => transform(subdoc, ruleTree)); + } const result = details.including ? {} : EJSON.clone(doc); Object.keys(ruleTree).forEach(key => { - if (!hasOwn.call(doc, key)) + if (!hasOwn.call(doc, key)) { return; + } const rule = ruleTree[key]; if (rule === Object(rule)) { // For sub-objects/subsets we branch - if (doc[key] === Object(doc[key])) + if (doc[key] === Object(doc[key])) { result[key] = transform(doc[key], rule); + } } else if (details.including) { // Otherwise we don't even touch this subfield result[key] = EJSON.clone(doc[key]); @@ -854,11 +904,13 @@ LocalCollection._compileProjection = fields => { return doc => { const result = transform(doc, details.tree); - if (_idProjection && hasOwn.call(doc, '_id')) + if (_idProjection && hasOwn.call(doc, '_id')) { result._id = doc._id; + } - if (!_idProjection && hasOwn.call(result, '_id')) + if (!_idProjection && hasOwn.call(result, '_id')) { delete result._id; + } return result; }; @@ -917,12 +969,14 @@ LocalCollection._diffQueryUnorderedChanges = (oldResults, newResults, observer, ; LocalCollection._findInOrderedResults = (query, doc) => { - if (!query.ordered) + if (!query.ordered) { throw new Error('Can\'t call _findInOrderedResults on unordered query'); + } for (let i = 0; i < query.results.length; i++) { - if (query.results[i] === doc) + if (query.results[i] === doc) { return i; + } } throw Error('object missing from query'); @@ -935,24 +989,28 @@ LocalCollection._findInOrderedResults = (query, doc) => { // access-controlled update and remove. LocalCollection._idsMatchedBySelector = selector => { // Is the selector just an ID? - if (LocalCollection._selectorIsId(selector)) + if (LocalCollection._selectorIsId(selector)) { return [selector]; + } - if (!selector) + if (!selector) { return null; + } // Do we have an _id clause? if (hasOwn.call(selector, '_id')) { // Is the _id clause just an ID? - if (LocalCollection._selectorIsId(selector._id)) + if (LocalCollection._selectorIsId(selector._id)) { return [selector._id]; + } // Is the _id clause {_id: {$in: ["x", "y", "z"]}}? if (selector._id && Array.isArray(selector._id.$in) && selector._id.$in.length - && selector._id.$in.every(LocalCollection._selectorIsId)) + && selector._id.$in.every(LocalCollection._selectorIsId)) { return selector._id.$in; + } return null; } @@ -964,8 +1022,9 @@ LocalCollection._idsMatchedBySelector = selector => { for (let i = 0; i < selector.$and.length; ++i) { const subIds = LocalCollection._idsMatchedBySelector(selector.$and[i]); - if (subIds) + if (subIds) { return subIds; + } } } @@ -989,10 +1048,11 @@ LocalCollection._insertInResults = (query, doc) => { ); let next = query.results[i + 1]; - if (next) + if (next) { next = next._id; - else + } else { next = null; + } query.addedBefore(doc._id, query.projectionFn(fields), next); } @@ -1058,8 +1118,9 @@ LocalCollection._isPlainObject = x => { // insert as part of an upsert operation. We use this primarily to figure // out when to set the fields in $setOnInsert, if present. LocalCollection._modify = (doc, modifier, options = {}) => { - if (!LocalCollection._isPlainObject(modifier)) + if (!LocalCollection._isPlainObject(modifier)) { throw MinimongoError('Modifier must be an object'); + } // Make sure the caller can't mutate our data structures. modifier = EJSON.clone(modifier); @@ -1075,14 +1136,16 @@ LocalCollection._modify = (doc, modifier, options = {}) => { const modFunc = MODIFIERS[setOnInsert ? '$set' : operator]; const operand = modifier[operator]; - if (!modFunc) + if (!modFunc) { throw MinimongoError(`Invalid modifier specified ${operator}`); + } Object.keys(operand).forEach(keypath => { const arg = operand[keypath]; - if (keypath === '') + if (keypath === '') { throw MinimongoError('An empty update path is not valid.'); + } const keyparts = keypath.split('.'); @@ -1127,8 +1190,9 @@ LocalCollection._modify = (doc, modifier, options = {}) => { // Note: this used to be for (var key in doc) however, this does not // work right in Opera. Deleting from a doc while iterating over it // would sometimes cause opera to skip some keys. - if (key !== '_id') + if (key !== '_id') { delete doc[key]; + } }); Object.keys(newDoc).forEach(key => { @@ -1150,8 +1214,9 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { observeChangesCallbacks = { addedBefore(id, fields, before) { - if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) + if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added)) { return; + } const doc = transform(Object.assign(fields, {_id: id})); @@ -1170,12 +1235,14 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { } }, changed(id, fields) { - if (!(observeCallbacks.changedAt || observeCallbacks.changed)) + if (!(observeCallbacks.changedAt || observeCallbacks.changed)) { return; + } let doc = EJSON.clone(this.docs.get(id)); - if (!doc) + if (!doc) { throw new Error(`Unknown id for changed: ${id}`); + } const oldDoc = transform(EJSON.clone(doc)); @@ -1192,8 +1259,9 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { } }, movedBefore(id, before) { - if (!observeCallbacks.movedTo) + if (!observeCallbacks.movedTo) { return; + } const from = indices ? this.docs.indexOf(id) : -1; let to = indices @@ -1204,8 +1272,9 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { // When not moving backwards, adjust for the fact that removing the // document slides everything back one slot. - if (to > from) + if (to > from) { --to; + } observeCallbacks.movedTo( transform(EJSON.clone(this.docs.get(id))), @@ -1215,24 +1284,27 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { ); }, removed(id) { - if (!(observeCallbacks.removedAt || observeCallbacks.removed)) + if (!(observeCallbacks.removedAt || observeCallbacks.removed)) { return; + } // technically maybe there should be an EJSON.clone here, but it's about // to be removed from this.docs! const doc = transform(this.docs.get(id)); - if (observeCallbacks.removedAt) + if (observeCallbacks.removedAt) { observeCallbacks.removedAt(doc, indices ? this.docs.indexOf(id) : -1); - else + } else { observeCallbacks.removed(doc); + } }, }; } else { observeChangesCallbacks = { added(id, fields) { - if (!suppressed && observeCallbacks.added) + if (!suppressed && observeCallbacks.added) { observeCallbacks.added(transform(Object.assign(fields, {_id: id}))); + } }, changed(id, fields) { if (observeCallbacks.changed) { @@ -1248,8 +1320,9 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { } }, removed(id) { - if (observeCallbacks.removed) + if (observeCallbacks.removed) { observeCallbacks.removed(transform(this.docs.get(id))); + } }, }; } @@ -1266,14 +1339,17 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => { }; LocalCollection._observeCallbacksAreOrdered = callbacks => { - if (callbacks.added && callbacks.addedAt) + if (callbacks.added && callbacks.addedAt) { throw new Error('Please specify only one of added() and addedAt()'); + } - if (callbacks.changed && callbacks.changedAt) + if (callbacks.changed && callbacks.changedAt) { throw new Error('Please specify only one of changed() and changedAt()'); + } - if (callbacks.removed && callbacks.removedAt) + if (callbacks.removed && callbacks.removedAt) { throw new Error('Please specify only one of removed() and removedAt()'); + } return !!( callbacks.addedAt || @@ -1284,8 +1360,9 @@ LocalCollection._observeCallbacksAreOrdered = callbacks => { }; LocalCollection._observeChangesCallbacksAreOrdered = callbacks => { - if (callbacks.added && callbacks.addedBefore) + if (callbacks.added && callbacks.addedBefore) { throw new Error('Please specify only one of added() and addedBefore()'); + } return !!(callbacks.addedBefore || callbacks.movedBefore); }; @@ -1319,8 +1396,9 @@ LocalCollection._selectorIsIdPerhapsAsObject = selector => ; LocalCollection._updateInResults = (query, doc, old_doc) => { - if (!EJSON.equals(doc._id, old_doc._id)) + if (!EJSON.equals(doc._id, old_doc._id)) { throw new Error('Can\'t change a doc\'s _id while updating'); + } const projectionFn = query.projectionFn; const changedFields = DiffSequence.makeChangedFields( @@ -1339,11 +1417,13 @@ LocalCollection._updateInResults = (query, doc, old_doc) => { const old_idx = LocalCollection._findInOrderedResults(query, doc); - if (Object.keys(changedFields).length) + if (Object.keys(changedFields).length) { query.changed(doc._id, changedFields); + } - if (!query.sorter) + if (!query.sorter) { return; + } // just take it out and put it back in again, and see if the index changes query.results.splice(old_idx, 1); @@ -1356,10 +1436,11 @@ LocalCollection._updateInResults = (query, doc, old_doc) => { if (old_idx !== new_idx) { let next = query.results[new_idx + 1]; - if (next) + if (next) { next = next._id; - else + } else { next = null; + } query.movedBefore && query.movedBefore(doc._id, next); } @@ -1382,8 +1463,9 @@ const MODIFIERS = { target[field] = new Date(); }, $min(target, field, arg) { - if (typeof arg !== 'number') + if (typeof arg !== 'number') { throw MinimongoError('Modifier $min allowed for numbers only', {field}); + } if (field in target) { if (typeof target[field] !== 'number') { @@ -1393,15 +1475,17 @@ const MODIFIERS = { ); } - if (target[field] > arg) + if (target[field] > arg) { target[field] = arg; + } } else { target[field] = arg; } }, $max(target, field, arg) { - if (typeof arg !== 'number') + if (typeof arg !== 'number') { throw MinimongoError('Modifier $max allowed for numbers only', {field}); + } if (field in target) { if (typeof target[field] !== 'number') { @@ -1411,15 +1495,17 @@ const MODIFIERS = { ); } - if (target[field] < arg) + if (target[field] < arg) { target[field] = arg; + } } else { target[field] = arg; } }, $inc(target, field, arg) { - if (typeof arg !== 'number') + if (typeof arg !== 'number') { throw MinimongoError('Modifier $inc allowed for numbers only', {field}); + } if (field in target) { if (typeof target[field] !== 'number') { @@ -1460,19 +1546,22 @@ const MODIFIERS = { $unset(target, field, arg) { if (target !== undefined) { if (target instanceof Array) { - if (field in target) + if (field in target) { target[field] = null; + } } else { delete target[field]; } } }, $push(target, field, arg) { - if (target[field] === undefined) + if (target[field] === undefined) { target[field] = []; + } - if (!(target[field] instanceof Array)) + if (!(target[field] instanceof Array)) { throw MinimongoError('Cannot apply $push modifier to non-array', {field}); + } if (!(arg && arg.$each)) { // Simple mode: not $each @@ -1485,16 +1574,18 @@ const MODIFIERS = { // Fancy mode: $each (and maybe $slice and $sort and $position) const toPush = arg.$each; - if (!(toPush instanceof Array)) + if (!(toPush instanceof Array)) { throw MinimongoError('$each must be an array', {field}); + } assertHasValidFieldNames(toPush); // Parse $position let position = undefined; if ('$position' in arg) { - if (typeof arg.$position !== 'number') + if (typeof arg.$position !== 'number') { throw MinimongoError('$position must be a numeric value', {field}); + } // XXX should check to make sure integer if (arg.$position < 0) { @@ -1510,8 +1601,9 @@ const MODIFIERS = { // Parse $slice. let slice = undefined; if ('$slice' in arg) { - if (typeof arg.$slice !== 'number') + if (typeof arg.$slice !== 'number') { throw MinimongoError('$slice must be a numeric value', {field}); + } // XXX should check to make sure integer slice = arg.$slice; @@ -1520,8 +1612,9 @@ const MODIFIERS = { // Parse $sort. let sortFunction = undefined; if (arg.$sort) { - if (slice === undefined) + if (slice === undefined) { throw MinimongoError('$sort requires $slice to be present', {field}); + } // XXX this allows us to use a $sort whose value is an array, but that's // actually an extension of the Node driver, so it won't work @@ -1556,8 +1649,9 @@ const MODIFIERS = { } // Actually sort. - if (sortFunction) + if (sortFunction) { target[field].sort(sortFunction); + } // Actually slice. if (slice !== undefined) { @@ -1571,8 +1665,9 @@ const MODIFIERS = { } }, $pushAll(target, field, arg) { - if (!(typeof arg === 'object' && arg instanceof Array)) + if (!(typeof arg === 'object' && arg instanceof Array)) { throw MinimongoError('Modifier $pushAll/pullAll allowed for arrays only'); + } assertHasValidFieldNames(arg); @@ -1614,37 +1709,44 @@ const MODIFIERS = { ); } else { values.forEach(value => { - if (toAdd.some(element => LocalCollection._f._equal(value, element))) + if (toAdd.some(element => LocalCollection._f._equal(value, element))) { return; + } toAdd.push(value); }); } }, $pop(target, field, arg) { - if (target === undefined) + if (target === undefined) { return; + } const toPop = target[field]; - if (toPop === undefined) + if (toPop === undefined) { return; + } - if (!(toPop instanceof Array)) + if (!(toPop instanceof Array)) { throw MinimongoError('Cannot apply $pop modifier to non-array', {field}); + } - if (typeof arg === 'number' && arg < 0) + if (typeof arg === 'number' && arg < 0) { toPop.splice(0, 1); - else + } else { toPop.pop(); + } }, $pull(target, field, arg) { - if (target === undefined) + if (target === undefined) { return; + } const toPull = target[field]; - if (toPull === undefined) + if (toPull === undefined) { return; + } if (!(toPull instanceof Array)) { throw MinimongoError( @@ -1681,13 +1783,15 @@ const MODIFIERS = { ); } - if (target === undefined) + if (target === undefined) { return; + } const toPull = target[field]; - if (toPull === undefined) + if (toPull === undefined) { return; + } if (!(toPull instanceof Array)) { throw MinimongoError( @@ -1702,14 +1806,17 @@ const MODIFIERS = { }, $rename(target, field, arg, keypath, doc) { // no idea why mongo has this restriction.. - if (keypath === arg) + if (keypath === arg) { throw MinimongoError('$rename source must differ from target', {field}); + } - if (target === null) + if (target === null) { throw MinimongoError('$rename source field invalid', {field}); + } - if (typeof arg !== 'string') + if (typeof arg !== 'string') { throw MinimongoError('$rename target must be a string', {field}); + } if (arg.includes('\0')) { // Null bytes are not allowed in Mongo field names @@ -1720,8 +1827,9 @@ const MODIFIERS = { ); } - if (target === undefined) + if (target === undefined) { return; + } const object = target[field]; @@ -1730,8 +1838,9 @@ const MODIFIERS = { const keyparts = arg.split('.'); const target2 = findModTarget(doc, keyparts, {forbidArray: true}); - if (target2 === null) + if (target2 === null) { throw MinimongoError('$rename target field invalid', {field}); + } target2[keyparts.pop()] = object; }, @@ -1771,8 +1880,9 @@ function assertHasValidFieldNames(doc) { function assertIsValidFieldName(key) { let match; - if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) + if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) { throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`); + } } // for a.b.c.2.d.e, keyparts should be ['a', 'b', 'c', '2', 'd', 'e'], @@ -1800,8 +1910,9 @@ function findModTarget(doc, keyparts, options = {}) { let keypart = keyparts[i]; if (!isIndexable(doc)) { - if (options.noCreate) + if (options.noCreate) { return undefined; + } const error = MinimongoError( `cannot use the part '${keypart}' to traverse ${doc}` @@ -1811,12 +1922,14 @@ function findModTarget(doc, keyparts, options = {}) { } if (doc instanceof Array) { - if (options.forbidArray) + if (options.forbidArray) { return null; + } if (keypart === '$') { - if (usedArrayIndex) + if (usedArrayIndex) { throw MinimongoError('Too many positional (i.e. \'$\') elements'); + } if (!options.arrayIndices || !options.arrayIndices.length) { throw MinimongoError( @@ -1830,22 +1943,26 @@ function findModTarget(doc, keyparts, options = {}) { } else if (isNumericKey(keypart)) { keypart = parseInt(keypart); } else { - if (options.noCreate) + if (options.noCreate) { return undefined; + } throw MinimongoError( `can't append to array using string field name [${keypart}]` ); } - if (last) + if (last) { keyparts[i] = keypart; // handle 'a.01' + } - if (options.noCreate && keypart >= doc.length) + if (options.noCreate && keypart >= doc.length) { return undefined; + } - while (doc.length < keypart) + while (doc.length < keypart) { doc.push(null); + } if (!last) { if (doc.length === keypart) { @@ -1861,16 +1978,19 @@ function findModTarget(doc, keyparts, options = {}) { assertIsValidFieldName(keypart); if (!(keypart in doc)) { - if (options.noCreate) + if (options.noCreate) { return undefined; + } - if (!last) + if (!last) { doc[keypart] = {}; + } } } - if (last) + if (last) { return doc; + } doc = doc[keypart]; } diff --git a/packages/minimongo/matcher.js b/packages/minimongo/matcher.js index b450c0cf58..ad48271166 100644 --- a/packages/minimongo/matcher.js +++ b/packages/minimongo/matcher.js @@ -55,8 +55,9 @@ export default class Matcher { } documentMatches(doc) { - if (doc !== Object(doc)) + if (doc !== Object(doc)) { throw Error('documentMatches needs a document'); + } return this._docMatcher(doc); } @@ -106,8 +107,9 @@ export default class Matcher { // Top level can't be an array or true or binary. if (Array.isArray(selector) || EJSON.isBinary(selector) || - typeof selector === 'boolean') + typeof selector === 'boolean') { throw new Error(`Invalid selector: ${selector}`); + } this._selector = EJSON.clone(selector); @@ -129,36 +131,46 @@ export default class Matcher { LocalCollection._f = { // XXX for _all and _in, consider building 'inquery' at compile time.. _type(v) { - if (typeof v === 'number') + if (typeof v === 'number') { return 1; + } - if (typeof v === 'string') + if (typeof v === 'string') { return 2; + } - if (typeof v === 'boolean') + if (typeof v === 'boolean') { return 8; + } - if (Array.isArray(v)) + if (Array.isArray(v)) { return 4; + } - if (v === null) + if (v === null) { return 10; + } // note that typeof(/x/) === "object" - if (v instanceof RegExp) + if (v instanceof RegExp) { return 11; + } - if (typeof v === 'function') + if (typeof v === 'function') { return 13; + } - if (v instanceof Date) + if (v instanceof Date) { return 9; + } - if (EJSON.isBinary(v)) + if (EJSON.isBinary(v)) { return 5; + } - if (v instanceof MongoID.ObjectID) + if (v instanceof MongoID.ObjectID) { return 7; + } // object return 3; @@ -212,11 +224,13 @@ LocalCollection._f = { // any other value.) return negative if a is less, positive if b is // less, or 0 if equal _cmp(a, b) { - if (a === undefined) + if (a === undefined) { return b === undefined ? 0 : -1; + } - if (b === undefined) + if (b === undefined) { return 1; + } let ta = LocalCollection._f._type(a); let tb = LocalCollection._f._type(b); @@ -224,13 +238,15 @@ LocalCollection._f = { const oa = LocalCollection._f._typeorder(ta); const ob = LocalCollection._f._typeorder(tb); - if (oa !== ob) + if (oa !== ob) { return oa < ob ? -1 : 1; + } // XXX need to implement this if we implement Symbol or integers, or // Timestamp - if (ta !== tb) + if (ta !== tb) { throw Error('Missing type coercion logic in _cmp'); + } if (ta === 7) { // ObjectID // Convert to string. @@ -270,38 +286,45 @@ LocalCollection._f = { if (ta === 4) { // Array for (let i = 0; ; i++) { - if (i === a.length) + if (i === a.length) { return i === b.length ? 0 : -1; + } - if (i === b.length) + if (i === b.length) { return 1; + } const s = LocalCollection._f._cmp(a[i], b[i]); - if (s !== 0) + if (s !== 0) { return s; + } } } if (ta === 5) { // binary // Surprisingly, a small binary blob is always less than a large one in // Mongo. - if (a.length !== b.length) + if (a.length !== b.length) { return a.length - b.length; + } for (let i = 0; i < a.length; i++) { - if (a[i] < b[i]) + if (a[i] < b[i]) { return -1; + } - if (a[i] > b[i]) + if (a[i] > b[i]) { return 1; + } } return 0; } if (ta === 8) { // boolean - if (a) + if (a) { return b ? 0 : 1; + } return b ? -1 : 0; } diff --git a/packages/minimongo/minimongo_server.js b/packages/minimongo/minimongo_server.js index 0758088c68..f869a77496 100644 --- a/packages/minimongo/minimongo_server.js +++ b/packages/minimongo/minimongo_server.js @@ -74,11 +74,13 @@ Minimongo.Matcher.prototype.affectedByModifier = function(modifier) { // stay 'false'. // Currently doesn't support $-operators and numeric indices precisely. Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { - if (!this.affectedByModifier(modifier)) + if (!this.affectedByModifier(modifier)) { return false; + } - if (!this.isSimple()) + if (!this.isSimple()) { return true; + } modifier = Object.assign({$set: {}, $unset: {}}, modifier); @@ -88,8 +90,9 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { ); if (this._getPaths().some(pathHasNumericKeys) || - modifierPaths.some(pathHasNumericKeys)) + modifierPaths.some(pathHasNumericKeys)) { return true; + } // check if there is a $set or $unset that indicates something is an // object rather than a scalar in the actual object where we saw $-operator @@ -97,16 +100,18 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { // Example: for selector {'a.b': {$gt: 5}} the modifier {'a.b.c':7} would // definitely set the result to false as 'a.b' appears to be an object. const expectedScalarIsObject = Object.keys(this._selector).some(path => { - if (!isOperatorObject(this._selector[path])) + if (!isOperatorObject(this._selector[path])) { return false; + } return modifierPaths.some(modifierPath => modifierPath.startsWith(`${path}.`) ); }); - if (expectedScalarIsObject) + if (expectedScalarIsObject) { return false; + } // See if we can apply the modifier on the ideally matching object. If it // still matches the selector, then the modifier could have turned the real @@ -114,8 +119,9 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { const matchingDocument = EJSON.clone(this.matchingDocument()); // The selector is too complex, anything can happen. - if (matchingDocument === null) + if (matchingDocument === null) { return true; + } try { LocalCollection._modify(matchingDocument, modifier); @@ -130,8 +136,9 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function(modifier) { // We don't know what real document was like but from the error raised by // $set on a scalar field we can reason that the structure of real document // is completely different. - if (error.name === 'MinimongoError' && error.setPropertyError) + if (error.name === 'MinimongoError' && error.setPropertyError) { return false; + } throw error; } @@ -149,8 +156,9 @@ Minimongo.Matcher.prototype.combineIntoProjection = function(projection) { // on all fields of the document. getSelectorPaths returns a list of paths // selector depends on. If one of the paths is '' (empty string) representing // the root or the whole document, complete projection should be returned. - if (selectorPaths.includes('')) + if (selectorPaths.includes('')) { return {}; + } return combineImportantPathsIntoProjection(selectorPaths, projection); }; @@ -161,8 +169,9 @@ Minimongo.Matcher.prototype.combineIntoProjection = function(projection) { // => { a: { b: { ans: 42 } }, foo: { bar: null, baz: "something" } } Minimongo.Matcher.prototype.matchingDocument = function() { // check if it was computed before - if (this._matchingDocument !== undefined) + if (this._matchingDocument !== undefined) { return this._matchingDocument; + } // If the analysis of this selector is too hard for our implementation // fallback to "YES" @@ -198,22 +207,25 @@ Minimongo.Matcher.prototype.matchingDocument = function() { ['$lte', '$lt'].forEach(op => { if (hasOwn.call(valueSelector, op) && - valueSelector[op] < upperBound) + valueSelector[op] < upperBound) { upperBound = valueSelector[op]; + } }); ['$gte', '$gt'].forEach(op => { if (hasOwn.call(valueSelector, op) && - valueSelector[op] > lowerBound) + valueSelector[op] > lowerBound) { lowerBound = valueSelector[op]; + } }); const middle = (lowerBound + upperBound) / 2; const matcher = new Minimongo.Matcher({placeholder: valueSelector}); if (!matcher.documentMatches({placeholder: middle}).result && - (middle === lowerBound || middle === upperBound)) + (middle === lowerBound || middle === upperBound)) { fallback = true; + } return middle; } @@ -232,8 +244,9 @@ Minimongo.Matcher.prototype.matchingDocument = function() { }, x => x); - if (fallback) + if (fallback) { this._matchingDocument = null; + } return this._matchingDocument; }; @@ -275,8 +288,9 @@ function combineImportantPathsIntoProjection(paths, projection) { const mergedExclProjection = {}; Object.keys(mergedProjection).forEach(path => { - if (!mergedProjection[path]) + if (!mergedProjection[path]) { mergedExclProjection[path] = false; + } }); return mergedExclProjection; @@ -288,12 +302,14 @@ function getPaths(selector) { // XXX remove it? // return Object.keys(selector).map(k => { // // we don't know how to handle $where because it can be anything - // if (k === '$where') + // if (k === '$where') { // return ''; // matches everything + // } // // we branch from $or/$and/$nor operator - // if (['$or', '$and', '$nor'].includes(k)) + // if (['$or', '$and', '$nor'].includes(k)) { // return selector[k].map(getPaths); + // } // // the value is a literal or some comparison operator // return k; @@ -318,10 +334,11 @@ function treeToPaths(tree, prefix = '') { Object.keys(tree).forEach(key => { const value = tree[key]; - if (value === Object(value)) + if (value === Object(value)) { Object.assign(result, treeToPaths(value, `${prefix + key}.`)); - else + } else { result[prefix + key] = value; + } }); return result; diff --git a/packages/minimongo/package.js b/packages/minimongo/package.js index 46fa410580..d3befe47dd 100644 --- a/packages/minimongo/package.js +++ b/packages/minimongo/package.js @@ -21,7 +21,7 @@ Package.onUse(api => { 'mongo-id', 'ordered-dict', 'random', - 'tracker', + 'tracker' ]); api.mainModule('minimongo_client.js', 'client'); @@ -39,7 +39,7 @@ Package.onTest(api => { 'reactive-var', 'test-helpers', 'tinytest', - 'tracker', + 'tracker' ]); api.addFiles('minimongo_tests.js'); diff --git a/packages/minimongo/sorter.js b/packages/minimongo/sorter.js index f1f95f3889..a98f0ff997 100644 --- a/packages/minimongo/sorter.js +++ b/packages/minimongo/sorter.js @@ -27,11 +27,13 @@ export default class Sorter { this._sortFunction = null; const addSpecPart = (path, ascending) => { - if (!path) + if (!path) { throw Error('sort keys must be non-empty'); + } - if (path.charAt(0) === '$') + if (path.charAt(0) === '$') { throw Error(`unsupported sort key: ${path}`); + } this._sortSpecParts.push({ ascending, @@ -59,8 +61,9 @@ export default class Sorter { } // If a function is specified for sorting, we skip the rest. - if (this._sortFunction) + if (this._sortFunction) { return; + } // To implement affectedByModifier, we piggy-back on top of Matcher's // affectedByModifier code; we create a selector that is affected by the @@ -85,8 +88,9 @@ export default class Sorter { // for the different sort spec fields) is compatible with the selector. this._keyFilter = null; - if (options.matcher) + if (options.matcher) { this._useWithMatcher(options.matcher); + } } getComparator(options) { @@ -95,18 +99,21 @@ export default class Sorter { // issue #3599 // https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation // sort effectively overrides $near - if (this._sortSpecParts.length || !options || !options.distances) + if (this._sortSpecParts.length || !options || !options.distances) { return this._getBaseComparator(); + } const distances = options.distances; // Return a comparator which compares using $near distances. return (a, b) => { - if (!distances.has(a._id)) + if (!distances.has(a._id)) { throw Error(`Missing distance for ${a._id}`); + } - if (!distances.has(b._id)) + if (!distances.has(b._id)) { throw Error(`Missing distance for ${b._id}`); + } return distances.get(a._id) - distances.get(b._id); }; @@ -117,8 +124,9 @@ export default class Sorter { // compare fields. _compareKeys(key1, key2) { if (key1.length !== this._sortSpecParts.length || - key2.length !== this._sortSpecParts.length) + key2.length !== this._sortSpecParts.length) { throw Error('Key has wrong length'); + } return this._keyComparator(key1, key2); } @@ -126,8 +134,9 @@ export default class Sorter { // Iterates over each possible "key" from doc (ie, over each branch), calling // 'cb' with the key. _generateKeysFromDoc(doc, cb) { - if (this._sortSpecParts.length === 0) + if (this._sortSpecParts.length === 0) { throw new Error('can\'t generate keys without a spec'); + } const pathFromIndices = indices => `${indices.join(',')},`; @@ -141,10 +150,11 @@ export default class Sorter { // If there are no values for a key (eg, key goes to an empty array), // pretend we found one null value. - if (!branches.length) + if (!branches.length) { branches = [{value: null}]; + } - const element = {}; + const element = Object.create(null); let usedPaths = false; branches.forEach(branch => { @@ -152,8 +162,9 @@ export default class Sorter { // If there are no array indices for a branch, then it must be the // only branch, because the only thing that produces multiple branches // is the use of arrays. - if (branches.length > 1) + if (branches.length > 1) { throw Error('multiple branches but no array used?'); + } element[''] = branch.value; return; @@ -163,8 +174,9 @@ export default class Sorter { const path = pathFromIndices(branch.arrayIndices); - if (hasOwn.call(element, path)) + if (hasOwn.call(element, path)) { throw Error(`duplicate path: ${path}`); + } element[path] = branch.value; @@ -178,16 +190,18 @@ export default class Sorter { // and 'a.x.y' are both arrays, but we don't allow this for now. // #NestedArraySort // XXX achieve full compatibility here - if (knownPaths && !hasOwn.call(knownPaths, path)) + if (knownPaths && !hasOwn.call(knownPaths, path)) { throw Error('cannot index parallel arrays'); + } }); if (knownPaths) { // Similarly to above, paths must match everywhere, unless this is a // non-array field. if (!hasOwn.call(element, '') && - Object.keys(knownPaths).length !== Object.keys(element).length) + Object.keys(knownPaths).length !== Object.keys(element).length) { throw Error('cannot index parallel arrays!'); + } } else if (usedPaths) { knownPaths = {}; @@ -202,8 +216,9 @@ export default class Sorter { if (!knownPaths) { // Easy case: no use of arrays. const soleKey = valuesByIndexAndPath.map(values => { - if (!hasOwn.call(values, '')) + if (!hasOwn.call(values, '')) { throw Error('no value in sole key case?'); + } return values['']; }); @@ -215,11 +230,13 @@ export default class Sorter { Object.keys(knownPaths).forEach(path => { const key = valuesByIndexAndPath.map(values => { - if (hasOwn.call(values, '')) + if (hasOwn.call(values, '')) { return values['']; + } - if (!hasOwn.call(values, path)) + if (!hasOwn.call(values, path)) { throw Error('missing path?'); + } return values[path]; }); @@ -231,13 +248,15 @@ export default class Sorter { // Returns a comparator that represents the sort specification (but not // including a possible geoquery distance tie-breaker). _getBaseComparator() { - if (this._sortFunction) + if (this._sortFunction) { return this._sortFunction; + } // If we're only sorting on geoquery distance and no specs, just say // everything is equal. - if (!this._sortSpecParts.length) + if (!this._sortSpecParts.length) { return (doc1, doc2) => 0; + } return (doc1, doc2) => { const key1 = this._getMinKeyFromDoc(doc1); @@ -260,22 +279,25 @@ export default class Sorter { let minKey = null; this._generateKeysFromDoc(doc, key => { - if (!this._keyCompatibleWithSelector(key)) + if (!this._keyCompatibleWithSelector(key)) { return; + } if (minKey === null) { minKey = key; return; } - if (this._compareKeys(key, minKey) < 0) + if (this._compareKeys(key, minKey) < 0) { minKey = key; + } }); // This could happen if our key filter somehow filters out all the keys even // though somehow the selector matches. - if (minKey === null) + if (minKey === null) { throw Error('sort selector found no keys in doc?'); + } return minKey; } @@ -319,21 +341,24 @@ export default class Sorter { // subtle and undocumented; we've gotten as close as we can figure out based // on our understanding of Mongo's behavior. _useWithMatcher(matcher) { - if (this._keyFilter) + if (this._keyFilter) { throw Error('called _useWithMatcher twice?'); + } // If we are only sorting by distance, then we're not going to bother to // build a key filter. // XXX figure out how geoqueries interact with this stuff - if (!this._sortSpecParts.length) + if (!this._sortSpecParts.length) { return; + } const selector = matcher._selector; // If the user just passed a literal function to find(), then we can't get a // key filter from it. - if (selector instanceof Function) + if (selector instanceof Function) { return; + } const constraintsByPath = {}; @@ -346,8 +371,9 @@ export default class Sorter { // XXX support $and and $or const constraints = constraintsByPath[key]; - if (!constraints) + if (!constraints) { return; + } // XXX it looks like the real MongoDB implementation isn't "does the // regexp match" but "does the value fall into a range named by the @@ -360,8 +386,9 @@ export default class Sorter { // index to use, which means it only cares about regexps that match // one range (with a literal prefix), and both 'i' and 'm' prevent the // literal prefix of the regexp from actually meaning one range. - if (subSelector.ignoreCase || subSelector.multiline) + if (subSelector.ignoreCase || subSelector.multiline) { return; + } constraints.push(regexpElementMatcher(subSelector)); return; @@ -380,13 +407,14 @@ export default class Sorter { } // See comments in the RegExp block above. - if (operator === '$regex' && !subSelector.$options) + if (operator === '$regex' && !subSelector.$options) { constraints.push( ELEMENT_OPERATORS.$regex.compileElementSelector( operand, subSelector ) ); + } // XXX support {$exists: true}, $mod, $type, $in, $elemMatch }); @@ -402,8 +430,9 @@ export default class Sorter { // others; we shouldn't create a key filter unless the first sort field is // restricted, though after that point we can restrict the other sort fields // or not as we wish. - if (!constraintsByPath[this._sortSpecParts[0].path].length) + if (!constraintsByPath[this._sortSpecParts[0].path].length) { return; + } this._keyFilter = key => this._sortSpecParts.every((specPart, index) => @@ -421,8 +450,9 @@ function composeComparators(comparatorArray) { return (a, b) => { for (let i = 0; i < comparatorArray.length; ++i) { const compare = comparatorArray[i](a, b); - if (compare !== 0) + if (compare !== 0) { return compare; + } } return 0; From bf978305ab740b351211dbdc61437a3bfecd20b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Wed, 9 Aug 2017 17:20:57 +0200 Subject: [PATCH 28/28] Fixed typos. --- packages/minimongo/common.js | 14 +++++++------- packages/minimongo/minimongo_server.js | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/minimongo/common.js b/packages/minimongo/common.js index e4e6f82ccf..7441d4f16d 100644 --- a/packages/minimongo/common.js +++ b/packages/minimongo/common.js @@ -538,13 +538,6 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) { const docMatchers = Object.keys(docSelector).map(key => { const subSelector = docSelector[key]; - // Don't add a matcher if subSelector is a function -- this is to match - // the behavior of Meteor on the server (inherited from the node mongodb - // driver), which is to ignore any part of a selector which is a function. - if (typeof subSelector === 'function') { - return undefined; - } - if (key.substr(0, 1) === '$') { // Outer operators are either logical operators (they recurse back into // this function), or $where. @@ -563,6 +556,13 @@ export function compileDocumentSelector(docSelector, matcher, options = {}) { matcher._recordPathUsed(key); } + // Don't add a matcher if subSelector is a function -- this is to match + // the behavior of Meteor on the server (inherited from the node mongodb + // driver), which is to ignore any part of a selector which is a function. + if (typeof subSelector === 'function') { + return undefined; + } + const lookUpByIndex = makeLookupFunction(key); const valueMatcher = compileValueSelector( subSelector, diff --git a/packages/minimongo/minimongo_server.js b/packages/minimongo/minimongo_server.js index f869a77496..373bbe8e07 100644 --- a/packages/minimongo/minimongo_server.js +++ b/packages/minimongo/minimongo_server.js @@ -1,5 +1,6 @@ import './minimongo_common.js'; import { + hasOwn, isNumericKey, isOperatorObject, pathsToTree,