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] 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