mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge pull request #8893 from radekmie/minimongo-without-underscore
Minimongo ES5/ES6 refactoring and performance improvements
This commit is contained in:
1376
packages/minimongo/common.js
Normal file
1376
packages/minimongo/common.js
Normal file
File diff suppressed because it is too large
Load Diff
468
packages/minimongo/cursor.js
Normal file
468
packages/minimongo/cursor.js
Normal file
@@ -0,0 +1,468 @@
|
||||
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 default class Cursor {
|
||||
// don't call this ctor directly. use LocalCollection.find().
|
||||
constructor(collection, selector, options = {}) {
|
||||
this.collection = collection;
|
||||
this.sorter = null;
|
||||
this.matcher = new Minimongo.Matcher(selector);
|
||||
|
||||
if (LocalCollection._selectorIsIdPerhapsAsObject(selector)) {
|
||||
// stash for fast _id and { _id }
|
||||
this._selectorId = selector._id || selector;
|
||||
} else {
|
||||
this._selectorId = undefined;
|
||||
|
||||
if (this.matcher.hasGeoQuery() || options.sort) {
|
||||
this.sorter = new Minimongo.Sorter(
|
||||
options.sort || [],
|
||||
{matcher: this.matcher}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.skip = options.skip || 0;
|
||||
this.limit = options.limit;
|
||||
this.fields = options.fields;
|
||||
|
||||
this._projectionFn = LocalCollection._compileProjection(this.fields || {});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Returns the number of documents that match a query.
|
||||
* @memberOf Mongo.Cursor
|
||||
* @method count
|
||||
* @instance
|
||||
* @locus Anywhere
|
||||
* @returns {Number}
|
||||
*/
|
||||
count() {
|
||||
if (this.reactive) {
|
||||
// allow the observe to be unordered
|
||||
this._depend({added: true, removed: true}, true);
|
||||
}
|
||||
|
||||
return this._getRawObjects({ordered: true}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Return all matching documents as an Array.
|
||||
* @memberOf Mongo.Cursor
|
||||
* @method fetch
|
||||
* @instance
|
||||
* @locus Anywhere
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
fetch() {
|
||||
const result = [];
|
||||
|
||||
this.forEach(doc => {
|
||||
result.push(doc);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 <em>cursor</em>
|
||||
* itself.
|
||||
* @param {Any} [thisArg] An object which will be the value of `this` inside
|
||||
* `callback`.
|
||||
*/
|
||||
forEach(callback, thisArg) {
|
||||
if (this.reactive) {
|
||||
this._depend({
|
||||
addedBefore: true,
|
||||
removed: true,
|
||||
changed: true,
|
||||
movedBefore: true});
|
||||
}
|
||||
|
||||
this._getRawObjects({ordered: true}).forEach((element, i) => {
|
||||
// This doubles as a clone operation.
|
||||
element = this._projectionFn(element);
|
||||
|
||||
if (this._transform) {
|
||||
element = this._transform(element);
|
||||
}
|
||||
|
||||
callback.call(thisArg, element, i, this);
|
||||
});
|
||||
}
|
||||
|
||||
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 <em>cursor</em>
|
||||
* itself.
|
||||
* @param {Any} [thisArg] An object which will be the value of `this` inside
|
||||
* `callback`.
|
||||
*/
|
||||
map(callback, thisArg) {
|
||||
const result = [];
|
||||
|
||||
this.forEach((doc, i) => {
|
||||
result.push(callback.call(thisArg, doc, i, this));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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.
|
||||
* @locus Anywhere
|
||||
* @memberOf Mongo.Cursor
|
||||
* @instance
|
||||
* @param {Object} callbacks Functions to call to deliver the result set as it
|
||||
* changes
|
||||
*/
|
||||
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 (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,
|
||||
matcher: this.matcher, // not fast pathed
|
||||
ordered,
|
||||
projectionFn: this._projectionFn,
|
||||
resultsSnapshot: null,
|
||||
sorter: ordered && this.sorter
|
||||
};
|
||||
|
||||
let qid;
|
||||
|
||||
// Non-reactive queries call added[Before] and then never call anything
|
||||
// else.
|
||||
if (this.reactive) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.
|
||||
const wrapCallback = fn => {
|
||||
if (!fn) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const self = this;
|
||||
return function(/* args*/) {
|
||||
if (self.collection.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
const args = arguments;
|
||||
|
||||
self.collection._observeQueue.queueTask(() => {
|
||||
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 = 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);
|
||||
}
|
||||
|
||||
query.added(doc._id, this._projectionFn(fields));
|
||||
});
|
||||
}
|
||||
|
||||
const handle = Object.assign(new LocalCollection.ObserveHandle, {
|
||||
collection: this.collection,
|
||||
stop: () => {
|
||||
if (this.reactive) {
|
||||
delete this.collection.queries[qid];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
// repurposed if the same observe is performed, using logic
|
||||
// similar to that of Meteor.subscribe.
|
||||
Tracker.onInvalidate(() => {
|
||||
handle.stop();
|
||||
});
|
||||
}
|
||||
|
||||
// run the observe callbacks resulting from the initial contents
|
||||
// before we leave the observe.
|
||||
this.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) {
|
||||
if (Tracker.active) {
|
||||
const dependency = new Tracker.Dependency;
|
||||
const notify = dependency.changed.bind(dependency);
|
||||
|
||||
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
|
||||
this.observeChanges(options);
|
||||
}
|
||||
}
|
||||
|
||||
_getCollectionName() {
|
||||
return this.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 = {}) {
|
||||
// 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 (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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
let distances;
|
||||
if (this.matcher.hasGeoQuery() && options.ordered) {
|
||||
if (options.distances) {
|
||||
distances = options.distances;
|
||||
distances.clear();
|
||||
} else {
|
||||
distances = new LocalCollection._IdMap();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
results.set(id, doc);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
);
|
||||
});
|
||||
|
||||
if (!options.ordered) {
|
||||
return results;
|
||||
}
|
||||
|
||||
if (this.sorter) {
|
||||
results.sort(this.sorter.getComparator({distances}));
|
||||
}
|
||||
|
||||
if (!this.limit && !this.skip) {
|
||||
return results;
|
||||
}
|
||||
|
||||
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 (!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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 _.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;
|
||||
_.each(valueSelector, function (value, 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);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
LocalCollection._IdMap = function () {
|
||||
var self = this;
|
||||
IdMap.call(self, MongoID.idStringify, MongoID.idParse);
|
||||
};
|
||||
|
||||
Meteor._inherits(LocalCollection._IdMap, IdMap);
|
||||
|
||||
1999
packages/minimongo/local_collection.js
Normal file
1999
packages/minimongo/local_collection.js
Normal file
File diff suppressed because it is too large
Load Diff
351
packages/minimongo/matcher.js
Normal file
351
packages/minimongo/matcher.js
Normal file
@@ -0,0 +1,351 @@
|
||||
import LocalCollection from './local_collection.js';
|
||||
import {
|
||||
compileDocumentSelector,
|
||||
hasOwn,
|
||||
nothingMatcher,
|
||||
} 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 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
|
||||
// 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 -- 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, _id)});
|
||||
}
|
||||
|
||||
// protect against dangerous selectors. falsey and {_id: falsey} are both
|
||||
// likely programmer error, and not what you want, particularly for
|
||||
// destructive operations.
|
||||
if (!selector || hasOwn.call(selector, '_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(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;
|
||||
}
|
||||
|
||||
// note that typeof(/x/) === "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
|
||||
// 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(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(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(a, b) {
|
||||
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;
|
||||
}
|
||||
|
||||
// XXX need to implement this if we implement Symbol or integers, or
|
||||
// Timestamp
|
||||
if (ta !== tb) {
|
||||
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 ...
|
||||
const toArray = object => {
|
||||
const result = [];
|
||||
|
||||
for (let key in object) {
|
||||
result.push(key);
|
||||
result.push(object[key]);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const 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 (let 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');
|
||||
},
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
1
packages/minimongo/minimongo_client.js
Normal file
1
packages/minimongo/minimongo_client.js
Normal file
@@ -0,0 +1 @@
|
||||
import './minimongo_common.js';
|
||||
10
packages/minimongo/minimongo_common.js
Normal file
10
packages/minimongo/minimongo_common.js
Normal file
@@ -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
|
||||
};
|
||||
346
packages/minimongo/minimongo_server.js
Normal file
346
packages/minimongo/minimongo_server.js
Normal file
@@ -0,0 +1,346 @@
|
||||
import './minimongo_common.js';
|
||||
import {
|
||||
hasOwn,
|
||||
isNumericKey,
|
||||
isOperatorObject,
|
||||
pathsToTree,
|
||||
projectionDetails,
|
||||
} from './common.js';
|
||||
|
||||
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
|
||||
// The modifier is always in a form of Object:
|
||||
// - $set
|
||||
// - 'a.b.22.z': value
|
||||
// - 'foo.bar': 42
|
||||
// - $unset
|
||||
// - 'abc.d': 1
|
||||
Minimongo.Matcher.prototype.affectedByModifier = function(modifier) {
|
||||
// safe check for $set/$unset being objects
|
||||
modifier = Object.assign({$set: {}, $unset: {}}, modifier);
|
||||
|
||||
const meaningfulPaths = this._getPaths();
|
||||
const modifiedPaths = [].concat(
|
||||
Object.keys(modifier.$set),
|
||||
Object.keys(modifier.$unset)
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
// One is a prefix of another, taking numeric fields into account
|
||||
return true;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// @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
|
||||
// accepting the modified value.
|
||||
// NOTE: assumes that document affected by modifier didn't match this Matcher
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (!this.isSimple()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
modifier = Object.assign({$set: {}, $unset: {}}, modifier);
|
||||
|
||||
const modifierPaths = [].concat(
|
||||
Object.keys(modifier.$set),
|
||||
Object.keys(modifier.$unset)
|
||||
);
|
||||
|
||||
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
|
||||
// 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(this._selector).some(path => {
|
||||
if (!isOperatorObject(this._selector[path])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return modifierPaths.some(modifierPath =>
|
||||
modifierPath.startsWith(`${path}.`)
|
||||
);
|
||||
});
|
||||
|
||||
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
|
||||
// object in the database into something matching.
|
||||
const matchingDocument = EJSON.clone(this.matchingDocument());
|
||||
|
||||
// The selector is too complex, anything can happen.
|
||||
if (matchingDocument === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
LocalCollection._modify(matchingDocument, modifier);
|
||||
} catch (error) {
|
||||
// Couldn't set a property on a field which is a scalar or null in the
|
||||
// selector.
|
||||
// Example:
|
||||
// real document: { 'a.b': 3 }
|
||||
// selector: { 'a': 12 }
|
||||
// converted selector (ideal document): { 'a': 12 }
|
||||
// modifier: { $set: { 'a.b': 4 } }
|
||||
// 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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
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 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 {};
|
||||
}
|
||||
|
||||
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" }
|
||||
// => { 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 the analysis of this selector is too hard for our implementation
|
||||
// fallback to "YES"
|
||||
let fallback = false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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(placeholder =>
|
||||
matcher.documentMatches({placeholder}).result
|
||||
);
|
||||
}
|
||||
|
||||
if (onlyContainsKeys(valueSelector, ['$gt', '$gte', '$lt', '$lte'])) {
|
||||
let lowerBound = -Infinity;
|
||||
let upperBound = Infinity;
|
||||
|
||||
['$lte', '$lt'].forEach(op => {
|
||||
if (hasOwn.call(valueSelector, op) &&
|
||||
valueSelector[op] < upperBound) {
|
||||
upperBound = valueSelector[op];
|
||||
}
|
||||
});
|
||||
|
||||
['$gte', '$gt'].forEach(op => {
|
||||
if (hasOwn.call(valueSelector, 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;
|
||||
}
|
||||
|
||||
return middle;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
return this._selectorForAffectedByModifier.affectedByModifier(modifier);
|
||||
};
|
||||
|
||||
Minimongo.Sorter.prototype.combineIntoProjection = function(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 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 => {
|
||||
if (!mergedProjection[path]) {
|
||||
mergedExclProjection[path] = false;
|
||||
}
|
||||
});
|
||||
|
||||
return mergedExclProjection;
|
||||
}
|
||||
|
||||
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
|
||||
function onlyContainsKeys(obj, keys) {
|
||||
return Object.keys(obj).every(k => keys.includes(k));
|
||||
}
|
||||
|
||||
function pathHasNumericKeys(path) {
|
||||
return path.split('.').some(isNumericKey);
|
||||
}
|
||||
|
||||
// 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 value = tree[key];
|
||||
if (value === Object(value)) {
|
||||
Object.assign(result, treeToPaths(value, `${prefix + key}.`));
|
||||
} else {
|
||||
result[prefix + key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,571 +0,0 @@
|
||||
Tinytest.add("minimongo - modifier affects selector", function (test) {
|
||||
function testSelectorPaths (sel, paths, desc) {
|
||||
var matcher = new Minimongo.Matcher(sel);
|
||||
test.equal(matcher._getPaths(), paths, desc);
|
||||
}
|
||||
|
||||
testSelectorPaths({
|
||||
foo: {
|
||||
bar: 3,
|
||||
baz: 42
|
||||
}
|
||||
}, ['foo'], "literal");
|
||||
|
||||
testSelectorPaths({
|
||||
foo: 42,
|
||||
bar: 33
|
||||
}, ['foo', 'bar'], "literal");
|
||||
|
||||
testSelectorPaths({
|
||||
foo: [ 'something' ],
|
||||
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");
|
||||
|
||||
testSelectorPaths({
|
||||
$or: [{ 'a.b': 1 }, { 'a.b.c': { $lt: 22 } },
|
||||
{$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,
|
||||
// so when you query col.find({ a: { foo: 1, bar: 2 } })
|
||||
// it doesn't mean you are looking for anything that has 'a.foo' to be 1 and
|
||||
// 'a.bar' to be 2, instead you are looking for 'a' to be exatly that object
|
||||
// with exatly that order of keys. { a: { foo: 1, bar: 2, baz: 3 } } wouldn't
|
||||
// match it. That's why in this selector 'a' would be important key, not a.foo
|
||||
// and a.bar.
|
||||
testSelectorPaths({
|
||||
a: {
|
||||
foo: 1,
|
||||
bar: 2
|
||||
},
|
||||
'b.c': {
|
||||
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");
|
||||
|
||||
function testSelectorAffectedByModifier (sel, mod, yes, desc) {
|
||||
var matcher = new Minimongo.Matcher(sel);
|
||||
test.equal(matcher.affectedByModifier(mod), yes, desc);
|
||||
}
|
||||
|
||||
function affected(sel, mod, desc) {
|
||||
testSelectorAffectedByModifier(sel, mod, true, desc);
|
||||
}
|
||||
function notAffected(sel, mod, desc) {
|
||||
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: { '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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - selector and projection combination", function (test) {
|
||||
function testSelProjectionComb (sel, proj, expected, desc) {
|
||||
var 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.b': { $lt: 3 },
|
||||
'y.0': -1,
|
||||
'a.c': 15
|
||||
}, {
|
||||
'd': 1,
|
||||
'z': 1
|
||||
}, {
|
||||
'a.b': true,
|
||||
'y': true,
|
||||
'a.c': true,
|
||||
'd': true,
|
||||
'z': true
|
||||
}, "multikey paths in selector - incl");
|
||||
|
||||
testSelProjectionComb({
|
||||
foo: 1234,
|
||||
$and: [{ k: -1 }, { $or: [{ b: 15 }] }]
|
||||
}, {
|
||||
'foo.bar': 1,
|
||||
'foo.zzz': 1,
|
||||
'b.asdf': 1
|
||||
}, {
|
||||
foo: true,
|
||||
b: true,
|
||||
k: true
|
||||
}, "multikey paths in fields - incl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 123,
|
||||
'a.b.d': 321,
|
||||
'b.c.0': 111,
|
||||
'a.e': 12345
|
||||
}, {
|
||||
'a.b.z': 1,
|
||||
'a.b.d.g': 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");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c.d': 123,
|
||||
'a.b1.c.d': 421,
|
||||
'a.b.c.e': 111
|
||||
}, {
|
||||
'a.b': 1
|
||||
}, {
|
||||
'a.b': true,
|
||||
'a.b1.c.d': true
|
||||
}, "shadowing one another - incl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b': 123,
|
||||
'foo.bar': false
|
||||
}, {
|
||||
'a.b.c.d': 1,
|
||||
'foo': 1
|
||||
}, {
|
||||
'a.b': true,
|
||||
'foo': true
|
||||
}, "shadowing one another - incl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 1
|
||||
}, {
|
||||
'a.b.c': 1
|
||||
}, {
|
||||
'a.b.c': true
|
||||
}, "same paths - incl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'x.4.y': 42,
|
||||
'z.0.1': 33
|
||||
}, {
|
||||
'x.x': 1
|
||||
}, {
|
||||
'x.x': true,
|
||||
'x.y': true,
|
||||
'z': true
|
||||
}, "numbered keys in selector - incl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 42,
|
||||
$where: function () { return true; }
|
||||
}, {
|
||||
'a.b': 1,
|
||||
'z.z': 1
|
||||
}, {}, "$where in the selector - incl");
|
||||
|
||||
testSelProjectionComb({
|
||||
$or: [
|
||||
{'a.b.c': 42},
|
||||
{$where: function () { return true; } }
|
||||
]
|
||||
}, {
|
||||
'a.b': 1,
|
||||
'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.b': { $lt: 3 },
|
||||
'y.0': -1,
|
||||
'a.c': 15
|
||||
}, {
|
||||
'd': 0,
|
||||
'z': 0
|
||||
}, {
|
||||
d: false,
|
||||
z: false
|
||||
}, "multikey paths in selector - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
foo: 1234,
|
||||
$and: [{ k: -1 }, { $or: [{ b: 15 }] }]
|
||||
}, {
|
||||
'foo.bar': 0,
|
||||
'foo.zzz': 0,
|
||||
'b.asdf': 0
|
||||
}, {
|
||||
}, "multikey paths in fields - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 123,
|
||||
'a.b.d': 321,
|
||||
'b.c.0': 111,
|
||||
'a.e': 12345
|
||||
}, {
|
||||
'a.b.z': 0,
|
||||
'a.b.d.g': 0,
|
||||
'c.c.c': 0
|
||||
}, {
|
||||
'a.b.z': false,
|
||||
'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': 0
|
||||
}, {
|
||||
}, "shadowing one another - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b': 123,
|
||||
'foo.bar': false
|
||||
}, {
|
||||
'a.b.c.d': 0,
|
||||
'foo': 0
|
||||
}, {
|
||||
}, "shadowing one another - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 1
|
||||
}, {
|
||||
'a.b.c': 0
|
||||
}, {
|
||||
}, "same paths - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b': 123,
|
||||
'a.c.d': 222,
|
||||
'ddd': 123
|
||||
}, {
|
||||
'a.b': 0,
|
||||
'a.c.e': 0,
|
||||
'asdf': 0
|
||||
}, {
|
||||
'a.c.e': false,
|
||||
'asdf': false
|
||||
}, "intercept the selector path - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 14
|
||||
}, {
|
||||
'a.b.d': 0
|
||||
}, {
|
||||
'a.b.d': false
|
||||
}, "different branches - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c.d': "124",
|
||||
'foo.bar.baz.que': "some value"
|
||||
}, {
|
||||
'a.b.c.d.e': 0,
|
||||
'foo.bar': 0
|
||||
}, {
|
||||
}, "excl on incl paths - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'x.4.y': 42,
|
||||
'z.0.1': 33
|
||||
}, {
|
||||
'x.x': 0,
|
||||
'x.y': 0
|
||||
}, {
|
||||
'x.x': false,
|
||||
}, "numbered keys in selector - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 42,
|
||||
$where: function () { return true; }
|
||||
}, {
|
||||
'a.b': 0,
|
||||
'z.z': 0
|
||||
}, {}, "$where in the selector - excl");
|
||||
|
||||
testSelProjectionComb({
|
||||
$or: [
|
||||
{'a.b.c': 42},
|
||||
{$where: function () { return true; } }
|
||||
]
|
||||
}, {
|
||||
'a.b': 0,
|
||||
'z.z': 0
|
||||
}, {}, "$where in the selector - excl");
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - sorter and projection combination", function (test) {
|
||||
function testSorterProjectionComb (sortSpec, proj, expected, desc) {
|
||||
var 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");
|
||||
|
||||
// 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");
|
||||
});
|
||||
|
||||
|
||||
(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)
|
||||
// - tests with $and/$or/$nor/$not branches (are absent)
|
||||
// - more tests with arrays fields and numeric keys (incomplete and test "not
|
||||
// ideal" implementation)
|
||||
// - tests when numeric keys actually mean numeric keys, not array indexes
|
||||
// (are absent)
|
||||
// - tests with $-operators in the selector (are incomplete and test "not
|
||||
// ideal" implementation)
|
||||
// * gives up on $-operators with non-scalar values ({$ne: {x: 1}})
|
||||
// * analyses $in
|
||||
// * analyses $nin/$ne
|
||||
// * analyses $gt, $gte, $lt, $lte
|
||||
// * 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
|
||||
// T - should return true
|
||||
// F - should return false
|
||||
var oneTest = function (sel, mod, expected, desc) {
|
||||
var matcher = new Minimongo.Matcher(sel);
|
||||
test.equal(matcher.canBecomeTrueByModifier(mod), expected, desc);
|
||||
};
|
||||
function T (sel, mod, desc) {
|
||||
oneTest(sel, mod, true, desc);
|
||||
}
|
||||
function F (sel, mod, desc) {
|
||||
oneTest(sel, mod, false, desc);
|
||||
}
|
||||
|
||||
Tinytest.add("minimongo - can selector become true by modifier - literals (structured tests)", function (t) {
|
||||
test = t;
|
||||
|
||||
var selector = {
|
||||
'a.b.c': 2,
|
||||
'foo.bar': {
|
||||
z: { y: 1 }
|
||||
},
|
||||
'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': {} }});
|
||||
|
||||
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 }});
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - can selector become true by modifier - literals (adhoc tests)", function (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");
|
||||
|
||||
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", function (t) {
|
||||
test = t;
|
||||
|
||||
// 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");
|
||||
// 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 }, { $set: { x: 1 } }, "don't touch regexp");
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - can selector become true by modifier - undefined/null", function (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");
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - can selector become true by modifier - literals with arrays", function (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");
|
||||
// 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");
|
||||
// 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");
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - can selector become true by modifier - set an object literal whose fields are selected", function (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");
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - can selector become true by modifier - $-scalar selectors and simple tests", function (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");
|
||||
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?");
|
||||
// XXX is this sufficient?
|
||||
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");
|
||||
// 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)");
|
||||
});
|
||||
|
||||
Tinytest.add("minimongo - can selector become true by modifier - $-nonscalar selectors and simple tests", function (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
|
||||
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");
|
||||
// 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");
|
||||
// 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");
|
||||
// XXX this test should be F, but it is not implemented yet
|
||||
T({ a: { $ne: { a: 2 } } }, { $set: { a: { a: 2 } } }, "$ne object");
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
3810
packages/minimongo/minimongo_tests_client.js
Normal file
3810
packages/minimongo/minimongo_tests_client.js
Normal file
File diff suppressed because it is too large
Load Diff
569
packages/minimongo/minimongo_tests_server.js
Normal file
569
packages/minimongo/minimongo_tests_server.js
Normal file
@@ -0,0 +1,569 @@
|
||||
Tinytest.add('minimongo - modifier affects selector', test => {
|
||||
function testSelectorPaths(sel, paths, desc) {
|
||||
const matcher = new Minimongo.Matcher(sel);
|
||||
test.equal(matcher._getPaths(), paths, desc);
|
||||
}
|
||||
|
||||
testSelectorPaths({
|
||||
foo: {
|
||||
bar: 3,
|
||||
baz: 42,
|
||||
},
|
||||
}, ['foo'], 'literal');
|
||||
|
||||
testSelectorPaths({
|
||||
foo: 42,
|
||||
bar: 33,
|
||||
}, ['foo', 'bar'], 'literal');
|
||||
|
||||
testSelectorPaths({
|
||||
foo: [ 'something' ],
|
||||
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');
|
||||
|
||||
testSelectorPaths({
|
||||
$or: [{ 'a.b': 1 }, { 'a.b.c': { $lt: 22 } },
|
||||
{$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,
|
||||
// so when you query col.find({ a: { foo: 1, bar: 2 } })
|
||||
// it doesn't mean you are looking for anything that has 'a.foo' to be 1 and
|
||||
// 'a.bar' to be 2, instead you are looking for 'a' to be exatly that object
|
||||
// with exatly that order of keys. { a: { foo: 1, bar: 2, baz: 3 } } wouldn't
|
||||
// match it. That's why in this selector 'a' would be important key, not a.foo
|
||||
// and a.bar.
|
||||
testSelectorPaths({
|
||||
a: {
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
},
|
||||
'b.c': {
|
||||
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');
|
||||
|
||||
function testSelectorAffectedByModifier(sel, mod, yes, desc) {
|
||||
const matcher = new Minimongo.Matcher(sel);
|
||||
test.equal(matcher.affectedByModifier(mod), yes, desc);
|
||||
}
|
||||
|
||||
function affected(sel, mod, desc) {
|
||||
testSelectorAffectedByModifier(sel, mod, true, desc);
|
||||
}
|
||||
function notAffected(sel, mod, desc) {
|
||||
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: { 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');
|
||||
|
||||
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');
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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.b': { $lt: 3 },
|
||||
'y.0': -1,
|
||||
'a.c': 15,
|
||||
}, {
|
||||
d: 1,
|
||||
z: 1,
|
||||
}, {
|
||||
'a.b': true,
|
||||
y: true,
|
||||
'a.c': true,
|
||||
d: true,
|
||||
z: true,
|
||||
}, 'multikey paths in selector - incl');
|
||||
|
||||
testSelProjectionComb({
|
||||
foo: 1234,
|
||||
$and: [{ k: -1 }, { $or: [{ b: 15 }] }],
|
||||
}, {
|
||||
'foo.bar': 1,
|
||||
'foo.zzz': 1,
|
||||
'b.asdf': 1,
|
||||
}, {
|
||||
foo: true,
|
||||
b: true,
|
||||
k: true,
|
||||
}, 'multikey paths in fields - incl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 123,
|
||||
'a.b.d': 321,
|
||||
'b.c.0': 111,
|
||||
'a.e': 12345,
|
||||
}, {
|
||||
'a.b.z': 1,
|
||||
'a.b.d.g': 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');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c.d': 123,
|
||||
'a.b1.c.d': 421,
|
||||
'a.b.c.e': 111,
|
||||
}, {
|
||||
'a.b': 1,
|
||||
}, {
|
||||
'a.b': true,
|
||||
'a.b1.c.d': true,
|
||||
}, 'shadowing one another - incl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b': 123,
|
||||
'foo.bar': false,
|
||||
}, {
|
||||
'a.b.c.d': 1,
|
||||
foo: 1,
|
||||
}, {
|
||||
'a.b': true,
|
||||
foo: true,
|
||||
}, 'shadowing one another - incl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 1,
|
||||
}, {
|
||||
'a.b.c': 1,
|
||||
}, {
|
||||
'a.b.c': true,
|
||||
}, 'same paths - incl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'x.4.y': 42,
|
||||
'z.0.1': 33,
|
||||
}, {
|
||||
'x.x': 1,
|
||||
}, {
|
||||
'x.x': true,
|
||||
'x.y': true,
|
||||
z: true,
|
||||
}, 'numbered keys in selector - incl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 42,
|
||||
$where() { return true; },
|
||||
}, {
|
||||
'a.b': 1,
|
||||
'z.z': 1,
|
||||
}, {}, '$where in the selector - incl');
|
||||
|
||||
testSelProjectionComb({
|
||||
$or: [
|
||||
{'a.b.c': 42},
|
||||
{$where() { return true; } },
|
||||
],
|
||||
}, {
|
||||
'a.b': 1,
|
||||
'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.b': { $lt: 3 },
|
||||
'y.0': -1,
|
||||
'a.c': 15,
|
||||
}, {
|
||||
d: 0,
|
||||
z: 0,
|
||||
}, {
|
||||
d: false,
|
||||
z: false,
|
||||
}, 'multikey paths in selector - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
foo: 1234,
|
||||
$and: [{ k: -1 }, { $or: [{ b: 15 }] }],
|
||||
}, {
|
||||
'foo.bar': 0,
|
||||
'foo.zzz': 0,
|
||||
'b.asdf': 0,
|
||||
}, {
|
||||
}, 'multikey paths in fields - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 123,
|
||||
'a.b.d': 321,
|
||||
'b.c.0': 111,
|
||||
'a.e': 12345,
|
||||
}, {
|
||||
'a.b.z': 0,
|
||||
'a.b.d.g': 0,
|
||||
'c.c.c': 0,
|
||||
}, {
|
||||
'a.b.z': false,
|
||||
'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': 0,
|
||||
}, {
|
||||
}, 'shadowing one another - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b': 123,
|
||||
'foo.bar': false,
|
||||
}, {
|
||||
'a.b.c.d': 0,
|
||||
foo: 0,
|
||||
}, {
|
||||
}, 'shadowing one another - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 1,
|
||||
}, {
|
||||
'a.b.c': 0,
|
||||
}, {
|
||||
}, 'same paths - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b': 123,
|
||||
'a.c.d': 222,
|
||||
ddd: 123,
|
||||
}, {
|
||||
'a.b': 0,
|
||||
'a.c.e': 0,
|
||||
asdf: 0,
|
||||
}, {
|
||||
'a.c.e': false,
|
||||
asdf: false,
|
||||
}, 'intercept the selector path - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 14,
|
||||
}, {
|
||||
'a.b.d': 0,
|
||||
}, {
|
||||
'a.b.d': false,
|
||||
}, 'different branches - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c.d': '124',
|
||||
'foo.bar.baz.que': 'some value',
|
||||
}, {
|
||||
'a.b.c.d.e': 0,
|
||||
'foo.bar': 0,
|
||||
}, {
|
||||
}, 'excl on incl paths - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'x.4.y': 42,
|
||||
'z.0.1': 33,
|
||||
}, {
|
||||
'x.x': 0,
|
||||
'x.y': 0,
|
||||
}, {
|
||||
'x.x': false,
|
||||
}, 'numbered keys in selector - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
'a.b.c': 42,
|
||||
$where() { return true; },
|
||||
}, {
|
||||
'a.b': 0,
|
||||
'z.z': 0,
|
||||
}, {}, '$where in the selector - excl');
|
||||
|
||||
testSelProjectionComb({
|
||||
$or: [
|
||||
{'a.b.c': 42},
|
||||
{$where() { return true; } },
|
||||
],
|
||||
}, {
|
||||
'a.b': 0,
|
||||
'z.z': 0,
|
||||
}, {}, '$where in the selector - excl');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
||||
|
||||
((() => {
|
||||
// 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)
|
||||
// - tests with $and/$or/$nor/$not branches (are absent)
|
||||
// - more tests with arrays fields and numeric keys (incomplete and test "not
|
||||
// ideal" implementation)
|
||||
// - tests when numeric keys actually mean numeric keys, not array indexes
|
||||
// (are absent)
|
||||
// - tests with $-operators in the selector (are incomplete and test "not
|
||||
// ideal" implementation)
|
||||
// * gives up on $-operators with non-scalar values ({$ne: {x: 1}})
|
||||
// * analyses $in
|
||||
// * analyses $nin/$ne
|
||||
// * analyses $gt, $gte, $lt, $lte
|
||||
// * gives up on a combination of $gt/$gte/$lt/$lte and $ne/$nin
|
||||
// * doesn't support $eq properly
|
||||
|
||||
let test = null; // set this global in the beginning of every test
|
||||
// T - should return true
|
||||
// F - should return false
|
||||
const oneTest = (sel, mod, expected, desc) => {
|
||||
const matcher = new Minimongo.Matcher(sel);
|
||||
test.equal(matcher.canBecomeTrueByModifier(mod), expected, desc);
|
||||
};
|
||||
function T(sel, mod, desc) {
|
||||
oneTest(sel, mod, true, desc);
|
||||
}
|
||||
function F(sel, mod, desc) {
|
||||
oneTest(sel, mod, false, desc);
|
||||
}
|
||||
|
||||
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 },
|
||||
},
|
||||
'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': {} }});
|
||||
|
||||
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 }});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
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 => {
|
||||
test = t;
|
||||
|
||||
// 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');
|
||||
// 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 }, { $set: { x: 1 } }, "don't touch regexp");
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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");
|
||||
// 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");
|
||||
// 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");
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
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?');
|
||||
// XXX is this sufficient?
|
||||
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');
|
||||
// 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)');
|
||||
});
|
||||
|
||||
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
|
||||
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');
|
||||
// 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');
|
||||
// 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');
|
||||
// XXX this test should be F, but it is not implemented yet
|
||||
T({ a: { $ne: { a: 2 } } }, { $set: { a: { a: 2 } } }, '$ne object');
|
||||
});
|
||||
}))();
|
||||
@@ -1,503 +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 && doc._id && !EJSON.equals(doc._id, mod._id)) {
|
||||
throw MinimongoError(`The _id field cannot be changed from {_id: "${doc._id}"} to {_id: "${mod._id}"}`);
|
||||
}
|
||||
|
||||
// replace the whole document
|
||||
assertHasValidFieldNames(mod);
|
||||
newDoc = mod;
|
||||
} else {
|
||||
// apply modifiers to the doc.
|
||||
newDoc = EJSON.clone(doc);
|
||||
|
||||
_.each(mod, function (operand, 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) {
|
||||
if (keypath === '') {
|
||||
throw MinimongoError("An empty update path is not valid.");
|
||||
}
|
||||
|
||||
var keyparts = keypath.split('.');
|
||||
|
||||
if (! _.all(keyparts, _.identity)) {
|
||||
throw MinimongoError(
|
||||
"The update path '" + keypath +
|
||||
"' contains an empty field name, which is not allowed.");
|
||||
}
|
||||
|
||||
var noCreate = _.has(NO_CREATE_MODIFIERS, 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);
|
||||
});
|
||||
});
|
||||
|
||||
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}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// move new document into place.
|
||||
_.each(_.keys(doc), 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;
|
||||
});
|
||||
};
|
||||
|
||||
// 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 (!_.isObject(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 {
|
||||
_.each(values, 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 });
|
||||
}
|
||||
};
|
||||
@@ -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) &&
|
||||
_.size(selector) === 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 (_.has(selector, '_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)) {
|
||||
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 && _.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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (_.has(options, '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(_.extend(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 = _.extend(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;
|
||||
};
|
||||
2
packages/minimongo/observe_handle.js
Normal file
2
packages/minimongo/observe_handle.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// ObserveHandle: the return value of a live query.
|
||||
export default class ObserveHandle {}
|
||||
@@ -1,67 +1,48 @@
|
||||
Package.describe({
|
||||
summary: "Meteor's client-side datastore: a port of MongoDB to Javascript",
|
||||
version: '1.2.1'
|
||||
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([
|
||||
'underscore',
|
||||
'ejson',
|
||||
'id-map',
|
||||
'ordered-dict',
|
||||
'tracker',
|
||||
'mongo-id',
|
||||
'random',
|
||||
// This package is used to get diff results on arrays and objects
|
||||
'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',
|
||||
'upsert_document.js',
|
||||
'selector.js',
|
||||
'sort.js',
|
||||
'projection.js',
|
||||
'modify.js',
|
||||
'diff.js',
|
||||
'id_map.js',
|
||||
'observe.js',
|
||||
'objectid.js'
|
||||
]);
|
||||
|
||||
// Functionality used only by oplog tailing on the server side
|
||||
api.addFiles([
|
||||
'selector_projection.js',
|
||||
'selector_modifier.js',
|
||||
'sorter_projection.js'
|
||||
], 'server');
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('minimongo', ['client', 'server']);
|
||||
api.use('test-helpers', 'client');
|
||||
api.use([
|
||||
'tinytest',
|
||||
'underscore',
|
||||
'ecmascript',
|
||||
'ejson',
|
||||
// This package is used for geo-location queries such as $near
|
||||
'geojson-utils',
|
||||
'id-map',
|
||||
'mongo-id',
|
||||
'ordered-dict',
|
||||
'random',
|
||||
'tracker',
|
||||
'reactive-var',
|
||||
'mongo-id',
|
||||
'ecmascript'
|
||||
'tracker'
|
||||
]);
|
||||
api.addFiles('minimongo_tests.js', 'client');
|
||||
api.addFiles('wrap_transform_tests.js');
|
||||
api.addFiles('minimongo_server_tests.js', 'server');
|
||||
|
||||
api.mainModule('minimongo_client.js', 'client');
|
||||
api.mainModule('minimongo_server.js', 'server');
|
||||
});
|
||||
|
||||
Package.onTest(api => {
|
||||
api.use('minimongo');
|
||||
api.use([
|
||||
'ecmascript',
|
||||
'ejson',
|
||||
'mongo-id',
|
||||
'ordered-dict',
|
||||
'random',
|
||||
'reactive-var',
|
||||
'test-helpers',
|
||||
'tinytest',
|
||||
'tracker'
|
||||
]);
|
||||
|
||||
api.addFiles('minimongo_tests.js');
|
||||
api.addFiles('minimongo_tests_client.js', 'client');
|
||||
api.addFiles('minimongo_tests_server.js', 'server');
|
||||
});
|
||||
|
||||
@@ -1,174 +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 = _.isUndefined(fields._id) ? 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); });
|
||||
|
||||
var res = details.including ? {} : EJSON.clone(doc);
|
||||
_.each(ruleTree, function (rule, key) {
|
||||
if (!_.has(doc, key))
|
||||
return;
|
||||
if (_.isObject(rule)) {
|
||||
// For sub-objects/subsets we branch
|
||||
if (_.isObject(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 && _.has(obj, '_id'))
|
||||
res._id = obj._id;
|
||||
if (!_idProjection && _.has(res, '_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 = _.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') &&
|
||||
!(_.contains(fieldsKeys, '_id') && fields['_id']))
|
||||
fieldsKeys = _.reject(fieldsKeys, function (key) { return key === '_id'; });
|
||||
|
||||
var including = null; // Unknown
|
||||
|
||||
_.each(fieldsKeys, 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 || {};
|
||||
_.each(paths, 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))
|
||||
treePos[key] = {};
|
||||
else if (!_.isObject(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]))
|
||||
return false;
|
||||
}
|
||||
|
||||
treePos = treePos[key];
|
||||
return true;
|
||||
});
|
||||
|
||||
if (success) {
|
||||
var lastKey = _.last(pathArr);
|
||||
if (!_.has(treePos, lastKey))
|
||||
treePos[lastKey] = newLeafFn(keyPath);
|
||||
else
|
||||
treePos[lastKey] = conflictFn(treePos[lastKey], keyPath, keyPath);
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
LocalCollection._checkSupportedProjection = function (fields) {
|
||||
if (!_.isObject(fields) || _.isArray(fields))
|
||||
throw MinimongoError("fields option must be an object");
|
||||
|
||||
_.each(fields, function (val, keyPath) {
|
||||
if (_.contains(keyPath.split('.'), '$'))
|
||||
throw MinimongoError("Minimongo doesn't support $ operator in projections yet.");
|
||||
if (typeof val === 'object' && _.intersection(['$elemMatch', '$meta', '$slice'], _.keys(val)).length > 0)
|
||||
throw MinimongoError("Minimongo doesn't support operators in projections yet.");
|
||||
if (_.indexOf([1, 0, true, false], val) === -1)
|
||||
throw MinimongoError("Projection values should be one of 1, 0, true, or false");
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,221 +0,0 @@
|
||||
// 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:
|
||||
// - $set
|
||||
// - 'a.b.22.z': value
|
||||
// - 'foo.bar': 42
|
||||
// - $unset
|
||||
// - 'abc.d': 1
|
||||
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));
|
||||
var meaningfulPaths = self._getPaths();
|
||||
|
||||
return _.any(modifiedPaths, function (path) {
|
||||
var mod = path.split('.');
|
||||
return _.any(meaningfulPaths, function (meaningfulPath) {
|
||||
var sel = meaningfulPath.split('.');
|
||||
var 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;
|
||||
} 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;
|
||||
}
|
||||
|
||||
// One is a prefix of another, taking numeric fields into account
|
||||
return true;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 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
|
||||
// accepting the modified value.
|
||||
// NOTE: assumes that document affected by modifier didn't match this Matcher
|
||||
// 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) {
|
||||
var self = this;
|
||||
if (!this.affectedByModifier(modifier))
|
||||
return false;
|
||||
|
||||
modifier = _.extend({$set:{}, $unset:{}}, modifier);
|
||||
var modifierPaths = _.keys(modifier.$set).concat(_.keys(modifier.$unset));
|
||||
|
||||
if (!self.isSimple())
|
||||
return true;
|
||||
|
||||
if (_.any(self._getPaths(), pathHasNumericKeys) ||
|
||||
_.any(modifierPaths, 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
|
||||
// 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) {
|
||||
if (! isOperatorObject(sel))
|
||||
return false;
|
||||
return _.any(modifierPaths, function (modifierPath) {
|
||||
return startsWith(modifierPath, path + '.');
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
// object in the database into something matching.
|
||||
var matchingDocument = EJSON.clone(self.matchingDocument());
|
||||
|
||||
// The selector is too complex, anything can happen.
|
||||
if (matchingDocument === null)
|
||||
return true;
|
||||
|
||||
try {
|
||||
LocalCollection._modify(matchingDocument, modifier);
|
||||
} catch (e) {
|
||||
// Couldn't set a property on a field which is a scalar or null in the
|
||||
// selector.
|
||||
// Example:
|
||||
// real document: { 'a.b': 3 }
|
||||
// selector: { 'a': 12 }
|
||||
// converted selector (ideal document): { 'a': 12 }
|
||||
// modifier: { $set: { 'a.b': 4 } }
|
||||
// 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;
|
||||
}
|
||||
|
||||
return self.documentMatches(matchingDocument).result;
|
||||
};
|
||||
|
||||
// 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" }
|
||||
// => { a: { b: { ans: 42 } }, foo: { bar: null, baz: "something" } }
|
||||
Minimongo.Matcher.prototype.matchingDocument = function () {
|
||||
var self = this;
|
||||
|
||||
// check if it was computed before
|
||||
if (self._matchingDocument !== undefined)
|
||||
return self._matchingDocument;
|
||||
|
||||
// If the analysis of this selector is too hard for our implementation
|
||||
// fallback to "YES"
|
||||
var fallback = false;
|
||||
self._matchingDocument = pathsToTree(self._getPaths(),
|
||||
function (path) {
|
||||
var 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"
|
||||
// dummy value
|
||||
if (valueSelector.$eq) {
|
||||
return valueSelector.$eq;
|
||||
} else if (valueSelector.$in) {
|
||||
var 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 _.find(valueSelector.$in, 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)
|
||||
upperBound = valueSelector[op];
|
||||
});
|
||||
_.each(['$gte', '$gt'], function (op) {
|
||||
if (_.has(valueSelector, op) && valueSelector[op] > lowerBound)
|
||||
lowerBound = valueSelector[op];
|
||||
});
|
||||
|
||||
var middle = (lowerBound + upperBound) / 2;
|
||||
var 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'])) {
|
||||
// Since self._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 {};
|
||||
} else {
|
||||
fallback = true;
|
||||
}
|
||||
}
|
||||
return self._selector[path];
|
||||
},
|
||||
_.identity /*conflict resolution is no resolution*/);
|
||||
|
||||
if (fallback)
|
||||
self._matchingDocument = null;
|
||||
|
||||
return self._matchingDocument;
|
||||
};
|
||||
|
||||
var getPaths = function (sel) {
|
||||
return _.keys(new Minimongo.Matcher(sel)._paths);
|
||||
return _.chain(sel).map(function (v, 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);
|
||||
// the value is a literal or some comparison operator
|
||||
return k;
|
||||
}).flatten().uniq().value();
|
||||
};
|
||||
|
||||
// A helper to ensure object has only certain keys
|
||||
var onlyContainsKeys = function (obj, keys) {
|
||||
return _.all(obj, function (v, k) {
|
||||
return _.contains(keys, k);
|
||||
});
|
||||
};
|
||||
|
||||
var pathHasNumericKeys = function (path) {
|
||||
return _.any(path.split('.'), isNumericKey);
|
||||
}
|
||||
|
||||
// XXX from Underscore.String (http://epeli.github.com/underscore.string/)
|
||||
var startsWith = function(str, starts) {
|
||||
return str.length >= starts.length &&
|
||||
str.substring(0, starts.length) === starts;
|
||||
};
|
||||
@@ -1,69 +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 (_.contains(selectorPaths, ''))
|
||||
return {};
|
||||
|
||||
return combineImportantPathsIntoProjection(selectorPaths, projection);
|
||||
};
|
||||
|
||||
Minimongo._pathsElidingNumericKeys = function (paths) {
|
||||
var self = this;
|
||||
return _.map(paths, function (path) {
|
||||
return _.reject(path.split('.'), isNumericKey).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 = {};
|
||||
_.each(mergedProjection, function (incl, 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 = {};
|
||||
|
||||
_.each(tree, function (val, key) {
|
||||
if (_.isObject(val))
|
||||
_.extend(result, treeToPaths(val, prefix + key + '.'));
|
||||
else
|
||||
result[prefix + key] = val;
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import {
|
||||
ELEMENT_OPERATORS,
|
||||
equalityElementMatcher,
|
||||
expandArraysInBranches,
|
||||
hasOwn,
|
||||
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"]]
|
||||
@@ -11,186 +21,164 @@
|
||||
// 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 default class Sorter {
|
||||
constructor(spec, options = {}) {
|
||||
this._sortSpecParts = [];
|
||||
this._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
|
||||
});
|
||||
};
|
||||
|
||||
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");
|
||||
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({
|
||||
ascending,
|
||||
lookup: makeLookupFunction(path, {forSort: true}),
|
||||
path
|
||||
});
|
||||
};
|
||||
|
||||
if (spec instanceof Array) {
|
||||
spec.forEach(element => {
|
||||
if (typeof element === 'string') {
|
||||
addSpecPart(element, true);
|
||||
} else {
|
||||
addSpecPart(element[0], element[1] !== 'desc');
|
||||
}
|
||||
});
|
||||
} else if (typeof spec === 'object') {
|
||||
Object.keys(spec).forEach(key => {
|
||||
addSpecPart(key, spec[key] >= 0);
|
||||
});
|
||||
} 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;
|
||||
}
|
||||
|
||||
// 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))
|
||||
);
|
||||
|
||||
// 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;
|
||||
|
||||
if (options.matcher) {
|
||||
this._useWithMatcher(options.matcher);
|
||||
}
|
||||
} else if (typeof spec === "object") {
|
||||
_.each(spec, function (value, 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 = {};
|
||||
_.each(self._sortSpecParts, function (spec) {
|
||||
selector[spec.path] = 1;
|
||||
});
|
||||
self._selectorForAffectedByModifier = new Minimongo.Matcher(selector);
|
||||
}
|
||||
|
||||
self._keyComparator = composeComparators(
|
||||
_.map(self._sortSpecParts, 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.
|
||||
_.extend(Minimongo.Sorter.prototype, {
|
||||
getComparator: function (options) {
|
||||
var self = this;
|
||||
|
||||
// If sort is specified or have no distances, just use the comparator from
|
||||
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
|
||||
// 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();
|
||||
}
|
||||
|
||||
var distances = options.distances;
|
||||
const 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 (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 _.pluck(self._sortSpecParts, 'path');
|
||||
},
|
||||
// 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) {
|
||||
if (key1.length !== this._sortSpecParts.length ||
|
||||
key2.length !== this._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 this._keyComparator(key1, key2);
|
||||
}
|
||||
|
||||
// Iterates over each possible "key" from doc (ie, over each branch), calling
|
||||
// 'cb' with the key.
|
||||
_generateKeysFromDoc: function (doc, cb) {
|
||||
var self = this;
|
||||
_generateKeysFromDoc(doc, cb) {
|
||||
if (this._sortSpecParts.length === 0) {
|
||||
throw new Error('can\'t generate keys without a spec');
|
||||
}
|
||||
|
||||
if (self._sortSpecParts.length === 0)
|
||||
throw new Error("can't generate keys without a spec");
|
||||
const pathFromIndices = indices => `${indices.join(',')},`;
|
||||
|
||||
let knownPaths = null;
|
||||
|
||||
// maps index -> ({'' -> value} or {path -> value})
|
||||
var valuesByIndexAndPath = [];
|
||||
|
||||
var pathFromIndices = function (indices) {
|
||||
return indices.join(',') + ',';
|
||||
};
|
||||
|
||||
var knownPaths = null;
|
||||
|
||||
_.each(self._sortSpecParts, function (spec, whichField) {
|
||||
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.)
|
||||
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)
|
||||
if (!branches.length) {
|
||||
branches = [{value: null}];
|
||||
}
|
||||
|
||||
var usedPaths = false;
|
||||
valuesByIndexAndPath[whichField] = {};
|
||||
_.each(branches, function (branch) {
|
||||
const element = Object.create(null);
|
||||
let usedPaths = false;
|
||||
|
||||
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;
|
||||
var path = pathFromIndices(branch.arrayIndices);
|
||||
if (_.has(valuesByIndexAndPath[whichField], path))
|
||||
throw Error("duplicate path: " + path);
|
||||
valuesByIndexAndPath[whichField][path] = branch.value;
|
||||
|
||||
const path = pathFromIndices(branch.arrayIndices);
|
||||
|
||||
if (hasOwn.call(element, 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
|
||||
@@ -202,97 +190,136 @@ _.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)) {
|
||||
throw Error("cannot index parallel arrays");
|
||||
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 (!_.has(valuesByIndexAndPath[whichField], '') &&
|
||||
_.size(knownPaths) !== _.size(valuesByIndexAndPath[whichField])) {
|
||||
throw Error("cannot index parallel arrays!");
|
||||
if (!hasOwn.call(element, '') &&
|
||||
Object.keys(knownPaths).length !== Object.keys(element).length) {
|
||||
throw Error('cannot index parallel arrays!');
|
||||
}
|
||||
} else if (usedPaths) {
|
||||
knownPaths = {};
|
||||
_.each(valuesByIndexAndPath[whichField], function (x, path) {
|
||||
|
||||
Object.keys(element).forEach(path => {
|
||||
knownPaths[path] = true;
|
||||
});
|
||||
}
|
||||
|
||||
return element;
|
||||
});
|
||||
|
||||
if (!knownPaths) {
|
||||
// Easy case: no use of arrays.
|
||||
var soleKey = _.map(valuesByIndexAndPath, function (values) {
|
||||
if (!_.has(values, ''))
|
||||
throw Error("no value in sole key case?");
|
||||
const soleKey = valuesByIndexAndPath.map(values => {
|
||||
if (!hasOwn.call(values, '')) {
|
||||
throw Error('no value in sole key case?');
|
||||
}
|
||||
|
||||
return values[''];
|
||||
});
|
||||
|
||||
cb(soleKey);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(knownPaths, function (x, path) {
|
||||
var key = _.map(valuesByIndexAndPath, function (values) {
|
||||
if (_.has(values, ''))
|
||||
Object.keys(knownPaths).forEach(path => {
|
||||
const key = valuesByIndexAndPath.map(values => {
|
||||
if (hasOwn.call(values, '')) {
|
||||
return values[''];
|
||||
if (!_.has(values, path))
|
||||
throw Error("missing path?");
|
||||
}
|
||||
|
||||
if (!hasOwn.call(values, 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;
|
||||
_getBaseComparator() {
|
||||
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) {
|
||||
return function (doc1, doc2) {
|
||||
return 0;
|
||||
};
|
||||
if (!this._sortSpecParts.length) {
|
||||
return (doc1, doc2) => 0;
|
||||
}
|
||||
|
||||
return function (doc1, doc2) {
|
||||
var key1 = self._getMinKeyFromDoc(doc1);
|
||||
var key2 = self._getMinKeyFromDoc(doc2);
|
||||
return self._compareKeys(key1, key2);
|
||||
return (doc1, doc2) => {
|
||||
const key1 = this._getMinKeyFromDoc(doc1);
|
||||
const key2 = this._getMinKeyFromDoc(doc2);
|
||||
return this._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) {
|
||||
let minKey = null;
|
||||
|
||||
this._generateKeysFromDoc(doc, key => {
|
||||
if (!this._keyCompatibleWithSelector(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (minKey === null) {
|
||||
minKey = key;
|
||||
return;
|
||||
}
|
||||
|
||||
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?');
|
||||
}
|
||||
|
||||
return minKey;
|
||||
}
|
||||
|
||||
_getPaths() {
|
||||
return this._sortSpecParts.map(part => part.path);
|
||||
}
|
||||
|
||||
_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) {
|
||||
const invert = !this._sortSpecParts[i].ascending;
|
||||
|
||||
return (key1, key2) => {
|
||||
const compare = LocalCollection._f._cmp(key1[i], key2[i]);
|
||||
return invert ? -compare : compare;
|
||||
};
|
||||
}
|
||||
|
||||
// In MongoDB, if you have documents
|
||||
// {_id: 'x', a: [1, 10]} and
|
||||
@@ -313,36 +340,40 @@ _.extend(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) {
|
||||
var self = this;
|
||||
|
||||
if (self._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 (_.isEmpty(self._sortSpecParts))
|
||||
if (!this._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)
|
||||
if (selector instanceof Function) {
|
||||
return;
|
||||
}
|
||||
|
||||
var constraintsByPath = {};
|
||||
_.each(self._sortSpecParts, function (spec, i) {
|
||||
const constraintsByPath = {};
|
||||
|
||||
this._sortSpecParts.forEach(spec => {
|
||||
constraintsByPath[spec.path] = [];
|
||||
});
|
||||
|
||||
_.each(selector, function (subSelector, key) {
|
||||
// XXX support $and and $or
|
||||
Object.keys(selector).forEach(key => {
|
||||
const subSelector = selector[key];
|
||||
|
||||
var constraints = constraintsByPath[key];
|
||||
if (!constraints)
|
||||
// XXX support $and and $or
|
||||
const 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
|
||||
@@ -355,30 +386,39 @@ _.extend(Minimongo.Sorter.prototype, {
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (isOperatorObject(subSelector)) {
|
||||
_.each(subSelector, function (operand, operator) {
|
||||
if (_.contains(['$lt', '$lte', '$gt', '$gte'], 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.
|
||||
constraints.push(
|
||||
ELEMENT_OPERATORS[operator].compileElementSelector(operand));
|
||||
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));
|
||||
operand,
|
||||
subSelector
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// XXX support {$exists: true}, $mod, $type, $in, $elemMatch
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,30 +430,31 @@ _.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[this._sortSpecParts[0].path].length) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._keyFilter = function (key) {
|
||||
return _.all(self._sortSpecParts, function (specPart, index) {
|
||||
return _.all(constraintsByPath[specPart.path], function (f) {
|
||||
return f(key[index]);
|
||||
});
|
||||
});
|
||||
};
|
||||
this._keyFilter = key =>
|
||||
this._sortSpecParts.every((specPart, index) =>
|
||||
constraintsByPath[specPart.path].every(fn => fn(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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
Minimongo.Sorter.prototype.combineIntoProjection = function (projection) {
|
||||
var self = this;
|
||||
var specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths());
|
||||
return combineImportantPathsIntoProjection(specPaths, projection);
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
// 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
|
||||
|
||||
// Some rules (found mainly with trial & error, so there might be more):
|
||||
// - handle all childs of $and (or implicit $and)
|
||||
// - handle $or nodes with exactly 1 child
|
||||
// - ignore $or nodes with more than 1 child
|
||||
// - 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)
|
||||
// - 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
|
||||
|
||||
// Fills a document with certain fields from an upsert selector
|
||||
export default function populateDocumentWithQueryFields (query, document = {}) {
|
||||
if (Object.getPrototypeOf(query) === Object.prototype) {
|
||||
// handle implicit $and
|
||||
Object.keys(query).forEach(function (key) {
|
||||
const value = query[key];
|
||||
if (key === '$and') {
|
||||
// handle explicit $and
|
||||
value.forEach(sq => populateDocumentWithQueryFields(sq, document));
|
||||
} else if (key === '$or') {
|
||||
// handle $or nodes with exactly 1 child
|
||||
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)) {
|
||||
insertIntoDocument(document, '_id', query);
|
||||
}
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
// Handles one key/value pair to put in the selector document
|
||||
function populateDocumentWithKeyValue (document, key, value) {
|
||||
if (value && Object.getPrototypeOf(value) === Object.prototype) {
|
||||
populateDocumentWithObject(document, key, value);
|
||||
} else if (!(value instanceof RegExp)) {
|
||||
insertIntoDocument(document, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Handles a key, value pair to put in the selector document
|
||||
// if the value is an object
|
||||
function populateDocumentWithObject (document, key, value) {
|
||||
const keys = Object.keys(value);
|
||||
const unprefixedKeys = keys.filter(k => k[0] !== '$');
|
||||
|
||||
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) {
|
||||
throw new Error(`unknown operator: ${unprefixedKeys[0]}`);
|
||||
}
|
||||
validateObject(value, key);
|
||||
insertIntoDocument(document, key, value);
|
||||
} else {
|
||||
Object.keys(value).forEach(function (k) {
|
||||
const v = value[k];
|
||||
if (k === '$eq') {
|
||||
populateDocumentWithKeyValue(document, key, v);
|
||||
} else if (k === '$all') {
|
||||
// every value for $all should be dealt with as separate $eq-s
|
||||
v.forEach(vx => populateDocumentWithKeyValue(document, key, vx));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actually inserts a key value into the selector document
|
||||
// However, this checks there is no ambiguity in setting
|
||||
// the value for the given key, throws otherwise
|
||||
function insertIntoDocument (document, key, value) {
|
||||
Object.keys(document).forEach(existingKey => {
|
||||
if (
|
||||
(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`);
|
||||
} else if (existingKey === key) {
|
||||
throw new Error(`cannot infer query fields to set, path '${key}' ` +
|
||||
'is matched twice');
|
||||
}
|
||||
});
|
||||
|
||||
document[key] = value;
|
||||
}
|
||||
|
||||
// Recursively validates an object that is nested more than one level deep
|
||||
function validateObject (obj, path) {
|
||||
if (obj && Object.getPrototypeOf(obj) === Object.prototype) {
|
||||
Object.keys(obj).forEach(function (key) {
|
||||
validateKeyInPath(key, path);
|
||||
validateObject(obj[key], path + '.' + key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validates the key in a path.
|
||||
// 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.');
|
||||
}
|
||||
}
|
||||
@@ -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 (_.isString(key) && (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;
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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 (!_.has(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");
|
||||
}
|
||||
|
||||
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 (_.has(transformed, '_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;
|
||||
};
|
||||
@@ -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(_.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
|
||||
];
|
||||
_.each(invalidObjects, 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();
|
||||
});
|
||||
Reference in New Issue
Block a user