Refactored server.

This commit is contained in:
Radosław Miernik
2017-07-12 20:03:31 +02:00
parent 7233d7aabb
commit df15f09d6a

View File

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