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