mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Refactored server.
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user