mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
1102 lines
42 KiB
JavaScript
1102 lines
42 KiB
JavaScript
// options.connection, if given, is a LivedataClient or LivedataServer
|
|
// XXX presently there is no way to destroy/clean up a Collection
|
|
|
|
/**
|
|
* @summary Namespace for MongoDB-related items
|
|
* @namespace
|
|
*/
|
|
Mongo = {};
|
|
|
|
/**
|
|
* @summary Constructor for a Collection
|
|
* @locus Anywhere
|
|
* @instancename collection
|
|
* @class
|
|
* @param {String} name The name of the collection. If null, creates an unmanaged (unsynchronized) local collection.
|
|
* @param {Object} [options]
|
|
* @param {Object} options.connection The server connection that will manage this collection. Uses the default connection if not specified. Pass the return value of calling [`DDP.connect`](#ddp_connect) to specify a different server. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection.
|
|
* @param {String} options.idGeneration The method of generating the `_id` fields of new documents in this collection. Possible values:
|
|
|
|
- **`'STRING'`**: random strings
|
|
- **`'MONGO'`**: random [`Mongo.ObjectID`](#mongo_object_id) values
|
|
|
|
The default id generation technique is `'STRING'`.
|
|
* @param {Function} options.transform An optional transformation function. Documents will be passed through this function before being returned from `fetch` or `findOne`, and before being passed to callbacks of `observe`, `map`, `forEach`, `allow`, and `deny`. Transforms are *not* applied for the callbacks of `observeChanges` or to cursors returned from publish functions.
|
|
*/
|
|
Mongo.Collection = function (name, options) {
|
|
var self = this;
|
|
if (! (self instanceof Mongo.Collection))
|
|
throw new Error('use "new" to construct a Mongo.Collection');
|
|
|
|
if (!name && (name !== null)) {
|
|
Meteor._debug("Warning: creating anonymous collection. It will not be " +
|
|
"saved or synchronized over the network. (Pass null for " +
|
|
"the collection name to turn off this warning.)");
|
|
name = null;
|
|
}
|
|
|
|
if (name !== null && typeof name !== "string") {
|
|
throw new Error(
|
|
"First argument to new Mongo.Collection must be a string or null");
|
|
}
|
|
|
|
if (options && options.methods) {
|
|
// Backwards compatibility hack with original signature (which passed
|
|
// "connection" directly instead of in options. (Connections must have a "methods"
|
|
// method.)
|
|
// XXX remove before 1.0
|
|
options = {connection: options};
|
|
}
|
|
// Backwards compatibility: "connection" used to be called "manager".
|
|
if (options && options.manager && !options.connection) {
|
|
options.connection = options.manager;
|
|
}
|
|
options = _.extend({
|
|
connection: undefined,
|
|
idGeneration: 'STRING',
|
|
transform: null,
|
|
_driver: undefined,
|
|
_preventAutopublish: false
|
|
}, options);
|
|
|
|
switch (options.idGeneration) {
|
|
case 'MONGO':
|
|
self._makeNewID = function () {
|
|
var src = name ? DDP.randomStream('/collection/' + name) : Random;
|
|
return new Mongo.ObjectID(src.hexString(24));
|
|
};
|
|
break;
|
|
case 'STRING':
|
|
default:
|
|
self._makeNewID = function () {
|
|
var src = name ? DDP.randomStream('/collection/' + name) : Random;
|
|
return src.id();
|
|
};
|
|
break;
|
|
}
|
|
|
|
self._transform = LocalCollection.wrapTransform(options.transform);
|
|
|
|
if (! name || options.connection === null)
|
|
// note: nameless collections never have a connection
|
|
self._connection = null;
|
|
else if (options.connection)
|
|
self._connection = options.connection;
|
|
else if (Meteor.isClient)
|
|
self._connection = Meteor.connection;
|
|
else
|
|
self._connection = Meteor.server;
|
|
|
|
if (!options._driver) {
|
|
// XXX This check assumes that webapp is loaded so that Meteor.server !==
|
|
// null. We should fully support the case of "want to use a Mongo-backed
|
|
// collection from Node code without webapp", but we don't yet.
|
|
// #MeteorServerNull
|
|
if (name && self._connection === Meteor.server &&
|
|
typeof MongoInternals !== "undefined" &&
|
|
MongoInternals.defaultRemoteCollectionDriver) {
|
|
options._driver = MongoInternals.defaultRemoteCollectionDriver();
|
|
} else {
|
|
options._driver = LocalCollectionDriver;
|
|
}
|
|
}
|
|
|
|
self._collection = options._driver.open(name, self._connection);
|
|
self._name = name;
|
|
|
|
if (self._connection && self._connection.registerStore) {
|
|
// OK, we're going to be a slave, replicating some remote
|
|
// database, except possibly with some temporary divergence while
|
|
// we have unacknowledged RPC's.
|
|
var ok = self._connection.registerStore(name, {
|
|
// Called at the beginning of a batch of updates. batchSize is the number
|
|
// of update calls to expect.
|
|
//
|
|
// XXX This interface is pretty janky. reset probably ought to go back to
|
|
// being its own function, and callers shouldn't have to calculate
|
|
// batchSize. The optimization of not calling pause/remove should be
|
|
// delayed until later: the first call to update() should buffer its
|
|
// message, and then we can either directly apply it at endUpdate time if
|
|
// it was the only update, or do pauseObservers/apply/apply at the next
|
|
// update() if there's another one.
|
|
beginUpdate: function (batchSize, reset) {
|
|
// pause observers so users don't see flicker when updating several
|
|
// objects at once (including the post-reconnect reset-and-reapply
|
|
// stage), and so that a re-sorting of a query can take advantage of the
|
|
// full _diffQuery moved calculation instead of applying change one at a
|
|
// time.
|
|
if (batchSize > 1 || reset)
|
|
self._collection.pauseObservers();
|
|
|
|
if (reset)
|
|
self._collection.remove({});
|
|
},
|
|
|
|
// Apply an update.
|
|
// XXX better specify this interface (not in terms of a wire message)?
|
|
update: function (msg) {
|
|
var mongoId = LocalCollection._idParse(msg.id);
|
|
var doc = self._collection.findOne(mongoId);
|
|
|
|
// Is this a "replace the whole doc" message coming from the quiescence
|
|
// of method writes to an object? (Note that 'undefined' is a valid
|
|
// value meaning "remove it".)
|
|
if (msg.msg === 'replace') {
|
|
var replace = msg.replace;
|
|
if (!replace) {
|
|
if (doc)
|
|
self._collection.remove(mongoId);
|
|
} else if (!doc) {
|
|
self._collection.insert(replace);
|
|
} else {
|
|
// XXX check that replace has no $ ops
|
|
self._collection.update(mongoId, replace);
|
|
}
|
|
return;
|
|
} else if (msg.msg === 'added') {
|
|
if (doc) {
|
|
throw new Error("Expected not to find a document already present for an add");
|
|
}
|
|
self._collection.insert(_.extend({_id: mongoId}, msg.fields));
|
|
} else if (msg.msg === 'removed') {
|
|
if (!doc)
|
|
throw new Error("Expected to find a document already present for removed");
|
|
self._collection.remove(mongoId);
|
|
} else if (msg.msg === 'changed') {
|
|
if (!doc)
|
|
throw new Error("Expected to find a document to change");
|
|
if (!_.isEmpty(msg.fields)) {
|
|
var modifier = {};
|
|
_.each(msg.fields, function (value, key) {
|
|
if (value === undefined) {
|
|
if (!modifier.$unset)
|
|
modifier.$unset = {};
|
|
modifier.$unset[key] = 1;
|
|
} else {
|
|
if (!modifier.$set)
|
|
modifier.$set = {};
|
|
modifier.$set[key] = value;
|
|
}
|
|
});
|
|
self._collection.update(mongoId, modifier);
|
|
}
|
|
} else {
|
|
throw new Error("I don't know how to deal with this message");
|
|
}
|
|
|
|
},
|
|
|
|
// Called at the end of a batch of updates.
|
|
endUpdate: function () {
|
|
self._collection.resumeObservers();
|
|
},
|
|
|
|
// Called around method stub invocations to capture the original versions
|
|
// of modified documents.
|
|
saveOriginals: function () {
|
|
self._collection.saveOriginals();
|
|
},
|
|
retrieveOriginals: function () {
|
|
return self._collection.retrieveOriginals();
|
|
}
|
|
});
|
|
|
|
if (!ok)
|
|
throw new Error("There is already a collection named '" + name + "'");
|
|
}
|
|
|
|
self._defineMutationMethods();
|
|
|
|
// autopublish
|
|
if (Package.autopublish && !options._preventAutopublish && self._connection
|
|
&& self._connection.publish) {
|
|
self._connection.publish(null, function () {
|
|
return self.find();
|
|
}, {is_auto: true});
|
|
}
|
|
};
|
|
|
|
///
|
|
/// Main collection API
|
|
///
|
|
|
|
|
|
_.extend(Mongo.Collection.prototype, {
|
|
|
|
_getFindSelector: function (args) {
|
|
if (args.length == 0)
|
|
return {};
|
|
else
|
|
return args[0];
|
|
},
|
|
|
|
_getFindOptions: function (args) {
|
|
var self = this;
|
|
if (args.length < 2) {
|
|
return { transform: self._transform };
|
|
} else {
|
|
check(args[1], Match.Optional(Match.ObjectIncluding({
|
|
fields: Match.Optional(Match.OneOf(Object, undefined)),
|
|
sort: Match.Optional(Match.OneOf(Object, Array, undefined)),
|
|
limit: Match.Optional(Match.OneOf(Number, undefined)),
|
|
skip: Match.Optional(Match.OneOf(Number, undefined))
|
|
})));
|
|
|
|
return _.extend({
|
|
transform: self._transform
|
|
}, args[1]);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @summary Find the documents in a collection that match the selector.
|
|
* @locus Anywhere
|
|
* @method find
|
|
* @memberOf Mongo.Collection
|
|
* @instance
|
|
* @param {MongoSelector} [selector] A query describing the documents to find
|
|
* @param {Object} [options]
|
|
* @param {MongoSortSpecifier} options.sort Sort order (default: natural order)
|
|
* @param {Number} options.skip Number of results to skip at the beginning
|
|
* @param {Number} options.limit Maximum number of results to return
|
|
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
|
|
* @param {Boolean} options.reactive (Client only) Default `true`; pass `false` to disable reactivity
|
|
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections) for this cursor. Pass `null` to disable transformation.
|
|
*/
|
|
find: function (/* selector, options */) {
|
|
// Collection.find() (return all docs) behaves differently
|
|
// from Collection.find(undefined) (return 0 docs). so be
|
|
// careful about the length of arguments.
|
|
var self = this;
|
|
var argArray = _.toArray(arguments);
|
|
return self._collection.find(self._getFindSelector(argArray),
|
|
self._getFindOptions(argArray));
|
|
},
|
|
|
|
/**
|
|
* @summary Finds the first document that matches the selector, as ordered by sort and skip options.
|
|
* @locus Anywhere
|
|
* @method findOne
|
|
* @memberOf Mongo.Collection
|
|
* @instance
|
|
* @param {MongoSelector} [selector] A query describing the documents to find
|
|
* @param {Object} [options]
|
|
* @param {MongoSortSpecifier} options.sort Sort order (default: natural order)
|
|
* @param {Number} options.skip Number of results to skip at the beginning
|
|
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude.
|
|
* @param {Boolean} options.reactive (Client only) Default true; pass false to disable reactivity
|
|
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections) for this cursor. Pass `null` to disable transformation.
|
|
*/
|
|
findOne: function (/* selector, options */) {
|
|
var self = this;
|
|
var argArray = _.toArray(arguments);
|
|
return self._collection.findOne(self._getFindSelector(argArray),
|
|
self._getFindOptions(argArray));
|
|
}
|
|
|
|
});
|
|
|
|
Mongo.Collection._publishCursor = function (cursor, sub, collection) {
|
|
var observeHandle = cursor.observeChanges({
|
|
added: function (id, fields) {
|
|
sub.added(collection, id, fields);
|
|
},
|
|
changed: function (id, fields) {
|
|
sub.changed(collection, id, fields);
|
|
},
|
|
removed: function (id) {
|
|
sub.removed(collection, id);
|
|
}
|
|
});
|
|
|
|
// We don't call sub.ready() here: it gets called in livedata_server, after
|
|
// possibly calling _publishCursor on multiple returned cursors.
|
|
|
|
// register stop callback (expects lambda w/ no args).
|
|
sub.onStop(function () {observeHandle.stop();});
|
|
};
|
|
|
|
// protect against dangerous selectors. falsey and {_id: falsey} are both
|
|
// likely programmer error, and not what you want, particularly for destructive
|
|
// operations. JS regexps don't serialize over DDP but can be trivially
|
|
// replaced by $regex.
|
|
Mongo.Collection._rewriteSelector = function (selector) {
|
|
// shorthand -- scalars match _id
|
|
if (LocalCollection._selectorIsId(selector))
|
|
selector = {_id: selector};
|
|
|
|
if (!selector || (('_id' in selector) && !selector._id))
|
|
// can't match anything
|
|
return {_id: Random.id()};
|
|
|
|
var ret = {};
|
|
_.each(selector, function (value, key) {
|
|
// Mongo supports both {field: /foo/} and {field: {$regex: /foo/}}
|
|
if (value instanceof RegExp) {
|
|
ret[key] = convertRegexpToMongoSelector(value);
|
|
} else if (value && value.$regex instanceof RegExp) {
|
|
ret[key] = convertRegexpToMongoSelector(value.$regex);
|
|
// if value is {$regex: /foo/, $options: ...} then $options
|
|
// override the ones set on $regex.
|
|
if (value.$options !== undefined)
|
|
ret[key].$options = value.$options;
|
|
}
|
|
else if (_.contains(['$or','$and','$nor'], key)) {
|
|
// Translate lower levels of $and/$or/$nor
|
|
ret[key] = _.map(value, function (v) {
|
|
return Mongo.Collection._rewriteSelector(v);
|
|
});
|
|
} else {
|
|
ret[key] = value;
|
|
}
|
|
});
|
|
return ret;
|
|
};
|
|
|
|
// convert a JS RegExp object to a Mongo {$regex: ..., $options: ...}
|
|
// selector
|
|
var convertRegexpToMongoSelector = function (regexp) {
|
|
check(regexp, RegExp); // safety belt
|
|
|
|
var selector = {$regex: regexp.source};
|
|
var regexOptions = '';
|
|
// JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options
|
|
// support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here.
|
|
if (regexp.ignoreCase)
|
|
regexOptions += 'i';
|
|
if (regexp.multiline)
|
|
regexOptions += 'm';
|
|
if (regexOptions)
|
|
selector.$options = regexOptions;
|
|
|
|
return selector;
|
|
};
|
|
|
|
var throwIfSelectorIsNotId = function (selector, methodName) {
|
|
if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) {
|
|
throw new Meteor.Error(
|
|
403, "Not permitted. Untrusted code may only " + methodName +
|
|
" documents by ID.");
|
|
}
|
|
};
|
|
|
|
// 'insert' immediately returns the inserted document's new _id.
|
|
// The others return values immediately if you are in a stub, an in-memory
|
|
// unmanaged collection, or a mongo-backed collection and you don't pass a
|
|
// callback. 'update' and 'remove' return the number of affected
|
|
// documents. 'upsert' returns an object with keys 'numberAffected' and, if an
|
|
// insert happened, 'insertedId'.
|
|
//
|
|
// Otherwise, the semantics are exactly like other methods: they take
|
|
// a callback as an optional last argument; if no callback is
|
|
// provided, they block until the operation is complete, and throw an
|
|
// exception if it fails; if a callback is provided, then they don't
|
|
// necessarily block, and they call the callback when they finish with error and
|
|
// result arguments. (The insert method provides the document ID as its result;
|
|
// update and remove provide the number of affected docs as the result; upsert
|
|
// provides an object with numberAffected and maybe insertedId.)
|
|
//
|
|
// On the client, blocking is impossible, so if a callback
|
|
// isn't provided, they just return immediately and any error
|
|
// information is lost.
|
|
//
|
|
// There's one more tweak. On the client, if you don't provide a
|
|
// callback, then if there is an error, a message will be logged with
|
|
// Meteor._debug.
|
|
//
|
|
// The intent (though this is actually determined by the underlying
|
|
// drivers) is that the operations should be done synchronously, not
|
|
// generating their result until the database has acknowledged
|
|
// them. In the future maybe we should provide a flag to turn this
|
|
// off.
|
|
|
|
/**
|
|
* @summary Insert a document in the collection. Returns its unique _id.
|
|
* @locus Anywhere
|
|
* @method insert
|
|
* @memberOf Mongo.Collection
|
|
* @instance
|
|
* @param {Object} doc The document to insert. May not yet have an _id attribute, in which case Meteor will generate one for you.
|
|
* @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the _id as the second.
|
|
*/
|
|
|
|
/**
|
|
* @summary Modify one or more documents in the collection. Returns the number of affected documents.
|
|
* @locus Anywhere
|
|
* @method update
|
|
* @memberOf Mongo.Collection
|
|
* @instance
|
|
* @param {MongoSelector} selector Specifies which documents to modify
|
|
* @param {MongoModifier} modifier Specifies how to modify the documents
|
|
* @param {Object} [options]
|
|
* @param {Boolean} options.multi True to modify all matching documents; false to only modify one of the matching documents (the default).
|
|
* @param {Boolean} options.upsert True to insert a document if no matching documents are found.
|
|
* @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second.
|
|
*/
|
|
|
|
/**
|
|
* @summary Remove documents from the collection
|
|
* @locus Anywhere
|
|
* @method remove
|
|
* @memberOf Mongo.Collection
|
|
* @instance
|
|
* @param {MongoSelector} selector Specifies which documents to remove
|
|
* @param {Function} [callback] Optional. If present, called with an error object as its argument.
|
|
*/
|
|
|
|
_.each(["insert", "update", "remove"], function (name) {
|
|
Mongo.Collection.prototype[name] = function (/* arguments */) {
|
|
var self = this;
|
|
var args = _.toArray(arguments);
|
|
var callback;
|
|
var insertId;
|
|
var ret;
|
|
|
|
// Pull off any callback (or perhaps a 'callback' variable that was passed
|
|
// in undefined, like how 'upsert' does it).
|
|
if (args.length &&
|
|
(args[args.length - 1] === undefined ||
|
|
args[args.length - 1] instanceof Function)) {
|
|
callback = args.pop();
|
|
}
|
|
|
|
if (name === "insert") {
|
|
if (!args.length)
|
|
throw new Error("insert requires an argument");
|
|
// shallow-copy the document and generate an ID
|
|
args[0] = _.extend({}, args[0]);
|
|
if ('_id' in args[0]) {
|
|
insertId = args[0]._id;
|
|
if (!insertId || !(typeof insertId === 'string'
|
|
|| insertId instanceof Mongo.ObjectID))
|
|
throw new Error("Meteor requires document _id fields to be non-empty strings or ObjectIDs");
|
|
} else {
|
|
var generateId = true;
|
|
// Don't generate the id if we're the client and the 'outermost' call
|
|
// This optimization saves us passing both the randomSeed and the id
|
|
// Passing both is redundant.
|
|
if (self._connection && self._connection !== Meteor.server) {
|
|
var enclosing = DDP._CurrentInvocation.get();
|
|
if (!enclosing) {
|
|
generateId = false;
|
|
}
|
|
}
|
|
if (generateId) {
|
|
insertId = args[0]._id = self._makeNewID();
|
|
}
|
|
}
|
|
} else {
|
|
args[0] = Mongo.Collection._rewriteSelector(args[0]);
|
|
|
|
if (name === "update") {
|
|
// Mutate args but copy the original options object. We need to add
|
|
// insertedId to options, but don't want to mutate the caller's options
|
|
// object. We need to mutate `args` because we pass `args` into the
|
|
// driver below.
|
|
var options = args[2] = _.clone(args[2]) || {};
|
|
if (options && typeof options !== "function" && options.upsert) {
|
|
// set `insertedId` if absent. `insertedId` is a Meteor extension.
|
|
if (options.insertedId) {
|
|
if (!(typeof options.insertedId === 'string'
|
|
|| options.insertedId instanceof Mongo.ObjectID))
|
|
throw new Error("insertedId must be string or ObjectID");
|
|
} else {
|
|
options.insertedId = self._makeNewID();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// On inserts, always return the id that we generated; on all other
|
|
// operations, just return the result from the collection.
|
|
var chooseReturnValueFromCollectionResult = function (result) {
|
|
if (name === "insert") {
|
|
if (!insertId && result) {
|
|
insertId = result;
|
|
}
|
|
return insertId;
|
|
} else {
|
|
return result;
|
|
}
|
|
};
|
|
|
|
var wrappedCallback;
|
|
if (callback) {
|
|
wrappedCallback = function (error, result) {
|
|
callback(error, ! error && chooseReturnValueFromCollectionResult(result));
|
|
};
|
|
}
|
|
|
|
// XXX see #MeteorServerNull
|
|
if (self._connection && self._connection !== Meteor.server) {
|
|
// just remote to another endpoint, propagate return value or
|
|
// exception.
|
|
|
|
var enclosing = DDP._CurrentInvocation.get();
|
|
var alreadyInSimulation = enclosing && enclosing.isSimulation;
|
|
|
|
if (Meteor.isClient && !wrappedCallback && ! alreadyInSimulation) {
|
|
// Client can't block, so it can't report errors by exception,
|
|
// only by callback. If they forget the callback, give them a
|
|
// default one that logs the error, so they aren't totally
|
|
// baffled if their writes don't work because their database is
|
|
// down.
|
|
// Don't give a default callback in simulation, because inside stubs we
|
|
// want to return the results from the local collection immediately and
|
|
// not force a callback.
|
|
wrappedCallback = function (err) {
|
|
if (err)
|
|
Meteor._debug(name + " failed: " + (err.reason || err.stack));
|
|
};
|
|
}
|
|
|
|
if (!alreadyInSimulation && name !== "insert") {
|
|
// If we're about to actually send an RPC, we should throw an error if
|
|
// this is a non-ID selector, because the mutation methods only allow
|
|
// single-ID selectors. (If we don't throw here, we'll see flicker.)
|
|
throwIfSelectorIsNotId(args[0], name);
|
|
}
|
|
|
|
ret = chooseReturnValueFromCollectionResult(
|
|
self._connection.apply(self._prefix + name, args, {returnStubValue: true}, wrappedCallback)
|
|
);
|
|
|
|
} else {
|
|
// it's my collection. descend into the collection object
|
|
// and propagate any exception.
|
|
args.push(wrappedCallback);
|
|
try {
|
|
// If the user provided a callback and the collection implements this
|
|
// operation asynchronously, then queryRet will be undefined, and the
|
|
// result will be returned through the callback instead.
|
|
var queryRet = self._collection[name].apply(self._collection, args);
|
|
ret = chooseReturnValueFromCollectionResult(queryRet);
|
|
} catch (e) {
|
|
if (callback) {
|
|
callback(e);
|
|
return null;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// both sync and async, unless we threw an exception, return ret
|
|
// (new document ID for insert, num affected for update/remove, object with
|
|
// numberAffected and maybe insertedId for upsert).
|
|
return ret;
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @summary Modify one or more documents in the collection, or insert one if no matching documents were found. Returns an object with keys `numberAffected` (the number of documents modified) and `insertedId` (the unique _id of the document that was inserted, if any).
|
|
* @locus Anywhere
|
|
* @param {MongoSelector} selector Specifies which documents to modify
|
|
* @param {MongoModifier} modifier Specifies how to modify the documents
|
|
* @param {Object} [options]
|
|
* @param {Boolean} options.multi True to modify all matching documents; false to only modify one of the matching documents (the default).
|
|
* @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second.
|
|
*/
|
|
Mongo.Collection.prototype.upsert = function (selector, modifier,
|
|
options, callback) {
|
|
var self = this;
|
|
if (! callback && typeof options === "function") {
|
|
callback = options;
|
|
options = {};
|
|
}
|
|
return self.update(selector, modifier,
|
|
_.extend({}, options, { _returnObject: true, upsert: true }),
|
|
callback);
|
|
};
|
|
|
|
// We'll actually design an index API later. For now, we just pass through to
|
|
// Mongo's, but make it synchronous.
|
|
Mongo.Collection.prototype._ensureIndex = function (index, options) {
|
|
var self = this;
|
|
if (!self._collection._ensureIndex)
|
|
throw new Error("Can only call _ensureIndex on server collections");
|
|
self._collection._ensureIndex(index, options);
|
|
};
|
|
Mongo.Collection.prototype._dropIndex = function (index) {
|
|
var self = this;
|
|
if (!self._collection._dropIndex)
|
|
throw new Error("Can only call _dropIndex on server collections");
|
|
self._collection._dropIndex(index);
|
|
};
|
|
Mongo.Collection.prototype._dropCollection = function () {
|
|
var self = this;
|
|
if (!self._collection.dropCollection)
|
|
throw new Error("Can only call _dropCollection on server collections");
|
|
self._collection.dropCollection();
|
|
};
|
|
Mongo.Collection.prototype._createCappedCollection = function (byteSize, maxDocuments) {
|
|
var self = this;
|
|
if (!self._collection._createCappedCollection)
|
|
throw new Error("Can only call _createCappedCollection on server collections");
|
|
self._collection._createCappedCollection(byteSize, maxDocuments);
|
|
};
|
|
|
|
/**
|
|
* @summary Create a Mongo-style `ObjectID`. If you don't specify a `hexString`, the `ObjectID` will generated randomly (not using MongoDB's ID construction rules).
|
|
* @locus Anywhere
|
|
* @class
|
|
* @param {String} hexString Optional. The 24-character hexadecimal contents of the ObjectID to create
|
|
*/
|
|
Mongo.ObjectID = LocalCollection._ObjectID;
|
|
|
|
/**
|
|
* @summary To create a cursor, use find. To access the documents in a cursor, use forEach, map, or fetch.
|
|
* @class
|
|
* @instanceName cursor
|
|
*/
|
|
Mongo.Cursor = LocalCollection.Cursor;
|
|
|
|
/**
|
|
* @deprecated in 0.9.1
|
|
*/
|
|
Mongo.Collection.Cursor = Mongo.Cursor;
|
|
|
|
/**
|
|
* @deprecated in 0.9.1
|
|
*/
|
|
Mongo.Collection.ObjectID = Mongo.ObjectID;
|
|
|
|
///
|
|
/// Remote methods and access control.
|
|
///
|
|
|
|
// Restrict default mutators on collection. allow() and deny() take the
|
|
// same options:
|
|
//
|
|
// options.insert {Function(userId, doc)}
|
|
// return true to allow/deny adding this document
|
|
//
|
|
// options.update {Function(userId, docs, fields, modifier)}
|
|
// return true to allow/deny updating these documents.
|
|
// `fields` is passed as an array of fields that are to be modified
|
|
//
|
|
// options.remove {Function(userId, docs)}
|
|
// return true to allow/deny removing these documents
|
|
//
|
|
// options.fetch {Array}
|
|
// Fields to fetch for these validators. If any call to allow or deny
|
|
// does not have this option then all fields are loaded.
|
|
//
|
|
// allow and deny can be called multiple times. The validators are
|
|
// evaluated as follows:
|
|
// - If neither deny() nor allow() has been called on the collection,
|
|
// then the request is allowed if and only if the "insecure" smart
|
|
// package is in use.
|
|
// - Otherwise, if any deny() function returns true, the request is denied.
|
|
// - Otherwise, if any allow() function returns true, the request is allowed.
|
|
// - Otherwise, the request is denied.
|
|
//
|
|
// Meteor may call your deny() and allow() functions in any order, and may not
|
|
// call all of them if it is able to make a decision without calling them all
|
|
// (so don't include side effects).
|
|
|
|
(function () {
|
|
var addValidator = function(allowOrDeny, options) {
|
|
// validate keys
|
|
var VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform'];
|
|
_.each(_.keys(options), function (key) {
|
|
if (!_.contains(VALID_KEYS, key))
|
|
throw new Error(allowOrDeny + ": Invalid key: " + key);
|
|
});
|
|
|
|
var self = this;
|
|
self._restricted = true;
|
|
|
|
_.each(['insert', 'update', 'remove'], function (name) {
|
|
if (options[name]) {
|
|
if (!(options[name] instanceof Function)) {
|
|
throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function");
|
|
}
|
|
|
|
// If the transform is specified at all (including as 'null') in this
|
|
// call, then take that; otherwise, take the transform from the
|
|
// collection.
|
|
if (options.transform === undefined) {
|
|
options[name].transform = self._transform; // already wrapped
|
|
} else {
|
|
options[name].transform = LocalCollection.wrapTransform(
|
|
options.transform);
|
|
}
|
|
|
|
self._validators[name][allowOrDeny].push(options[name]);
|
|
}
|
|
});
|
|
|
|
// Only update the fetch fields if we're passed things that affect
|
|
// fetching. This way allow({}) and allow({insert: f}) don't result in
|
|
// setting fetchAllFields
|
|
if (options.update || options.remove || options.fetch) {
|
|
if (options.fetch && !(options.fetch instanceof Array)) {
|
|
throw new Error(allowOrDeny + ": Value for `fetch` must be an array");
|
|
}
|
|
self._updateFetch(options.fetch);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @summary Allow users to write directly to this collection from client code, subject to limitations you define.
|
|
* @locus Server
|
|
* @param {Object} options
|
|
* @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be allowed.
|
|
* @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions.
|
|
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation.
|
|
*/
|
|
Mongo.Collection.prototype.allow = function(options) {
|
|
addValidator.call(this, 'allow', options);
|
|
};
|
|
|
|
/**
|
|
* @summary Override `allow` rules.
|
|
* @locus Server
|
|
* @param {Object} options
|
|
* @param {Function} options.insert,update,remove Functions that look at a proposed modification to the database and return true if it should be denied, even if an [allow](#allow) rule says otherwise.
|
|
* @param {String[]} options.fetch Optional performance enhancement. Limits the fields that will be fetched from the database for inspection by your `update` and `remove` functions.
|
|
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections). Pass `null` to disable transformation.
|
|
*/
|
|
Mongo.Collection.prototype.deny = function(options) {
|
|
addValidator.call(this, 'deny', options);
|
|
};
|
|
})();
|
|
|
|
|
|
Mongo.Collection.prototype._defineMutationMethods = function() {
|
|
var self = this;
|
|
|
|
// set to true once we call any allow or deny methods. If true, use
|
|
// allow/deny semantics. If false, use insecure mode semantics.
|
|
self._restricted = false;
|
|
|
|
// Insecure mode (default to allowing writes). Defaults to 'undefined' which
|
|
// means insecure iff the insecure package is loaded. This property can be
|
|
// overriden by tests or packages wishing to change insecure mode behavior of
|
|
// their collections.
|
|
self._insecure = undefined;
|
|
|
|
self._validators = {
|
|
insert: {allow: [], deny: []},
|
|
update: {allow: [], deny: []},
|
|
remove: {allow: [], deny: []},
|
|
upsert: {allow: [], deny: []}, // dummy arrays; can't set these!
|
|
fetch: [],
|
|
fetchAllFields: false
|
|
};
|
|
|
|
if (!self._name)
|
|
return; // anonymous collection
|
|
|
|
// XXX Think about method namespacing. Maybe methods should be
|
|
// "Meteor:Mongo:insert/NAME"?
|
|
self._prefix = '/' + self._name + '/';
|
|
|
|
// mutation methods
|
|
if (self._connection) {
|
|
var m = {};
|
|
|
|
_.each(['insert', 'update', 'remove'], function (method) {
|
|
m[self._prefix + method] = function (/* ... */) {
|
|
// All the methods do their own validation, instead of using check().
|
|
check(arguments, [Match.Any]);
|
|
var args = _.toArray(arguments);
|
|
try {
|
|
// For an insert, if the client didn't specify an _id, generate one
|
|
// now; because this uses DDP.randomStream, it will be consistent with
|
|
// what the client generated. We generate it now rather than later so
|
|
// that if (eg) an allow/deny rule does an insert to the same
|
|
// collection (not that it really should), the generated _id will
|
|
// still be the first use of the stream and will be consistent.
|
|
//
|
|
// However, we don't actually stick the _id onto the document yet,
|
|
// because we want allow/deny rules to be able to differentiate
|
|
// between arbitrary client-specified _id fields and merely
|
|
// client-controlled-via-randomSeed fields.
|
|
var generatedId = null;
|
|
if (method === "insert" && !_.has(args[0], '_id')) {
|
|
generatedId = self._makeNewID();
|
|
}
|
|
|
|
if (this.isSimulation) {
|
|
// In a client simulation, you can do any mutation (even with a
|
|
// complex selector).
|
|
if (generatedId !== null)
|
|
args[0]._id = generatedId;
|
|
return self._collection[method].apply(
|
|
self._collection, args);
|
|
}
|
|
|
|
// This is the server receiving a method call from the client.
|
|
|
|
// We don't allow arbitrary selectors in mutations from the client: only
|
|
// single-ID selectors.
|
|
if (method !== 'insert')
|
|
throwIfSelectorIsNotId(args[0], method);
|
|
|
|
if (self._restricted) {
|
|
// short circuit if there is no way it will pass.
|
|
if (self._validators[method].allow.length === 0) {
|
|
throw new Meteor.Error(
|
|
403, "Access denied. No allow validators set on restricted " +
|
|
"collection for method '" + method + "'.");
|
|
}
|
|
|
|
var validatedMethodName =
|
|
'_validated' + method.charAt(0).toUpperCase() + method.slice(1);
|
|
args.unshift(this.userId);
|
|
method === 'insert' && args.push(generatedId);
|
|
return self[validatedMethodName].apply(self, args);
|
|
} else if (self._isInsecure()) {
|
|
if (generatedId !== null)
|
|
args[0]._id = generatedId;
|
|
// In insecure mode, allow any mutation (with a simple selector).
|
|
// XXX This is kind of bogus. Instead of blindly passing whatever
|
|
// we get from the network to this function, we should actually
|
|
// know the correct arguments for the function and pass just
|
|
// them. For example, if you have an extraneous extra null
|
|
// argument and this is Mongo on the server, the _wrapAsync'd
|
|
// functions like update will get confused and pass the
|
|
// "fut.resolver()" in the wrong slot, where _update will never
|
|
// invoke it. Bam, broken DDP connection. Probably should just
|
|
// take this whole method and write it three times, invoking
|
|
// helpers for the common code.
|
|
return self._collection[method].apply(self._collection, args);
|
|
} else {
|
|
// In secure mode, if we haven't called allow or deny, then nothing
|
|
// is permitted.
|
|
throw new Meteor.Error(403, "Access denied");
|
|
}
|
|
} catch (e) {
|
|
if (e.name === 'MongoError' || e.name === 'MinimongoError') {
|
|
throw new Meteor.Error(409, e.toString());
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
};
|
|
});
|
|
// Minimongo on the server gets no stubs; instead, by default
|
|
// it wait()s until its result is ready, yielding.
|
|
// This matches the behavior of macromongo on the server better.
|
|
// XXX see #MeteorServerNull
|
|
if (Meteor.isClient || self._connection === Meteor.server)
|
|
self._connection.methods(m);
|
|
}
|
|
};
|
|
|
|
|
|
Mongo.Collection.prototype._updateFetch = function (fields) {
|
|
var self = this;
|
|
|
|
if (!self._validators.fetchAllFields) {
|
|
if (fields) {
|
|
self._validators.fetch = _.union(self._validators.fetch, fields);
|
|
} else {
|
|
self._validators.fetchAllFields = true;
|
|
// clear fetch just to make sure we don't accidentally read it
|
|
self._validators.fetch = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
Mongo.Collection.prototype._isInsecure = function () {
|
|
var self = this;
|
|
if (self._insecure === undefined)
|
|
return !!Package.insecure;
|
|
return self._insecure;
|
|
};
|
|
|
|
var docToValidate = function (validator, doc, generatedId) {
|
|
var ret = doc;
|
|
if (validator.transform) {
|
|
ret = EJSON.clone(doc);
|
|
// If you set a server-side transform on your collection, then you don't get
|
|
// to tell the difference between "client specified the ID" and "server
|
|
// generated the ID", because transforms expect to get _id. If you want to
|
|
// do that check, you can do it with a specific
|
|
// `C.allow({insert: f, transform: null})` validator.
|
|
if (generatedId !== null) {
|
|
ret._id = generatedId;
|
|
}
|
|
ret = validator.transform(ret);
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
Mongo.Collection.prototype._validatedInsert = function (userId, doc,
|
|
generatedId) {
|
|
var self = this;
|
|
|
|
// call user validators.
|
|
// Any deny returns true means denied.
|
|
if (_.any(self._validators.insert.deny, function(validator) {
|
|
return validator(userId, docToValidate(validator, doc, generatedId));
|
|
})) {
|
|
throw new Meteor.Error(403, "Access denied");
|
|
}
|
|
// Any allow returns true means proceed. Throw error if they all fail.
|
|
if (_.all(self._validators.insert.allow, function(validator) {
|
|
return !validator(userId, docToValidate(validator, doc, generatedId));
|
|
})) {
|
|
throw new Meteor.Error(403, "Access denied");
|
|
}
|
|
|
|
// If we generated an ID above, insert it now: after the validation, but
|
|
// before actually inserting.
|
|
if (generatedId !== null)
|
|
doc._id = generatedId;
|
|
|
|
self._collection.insert.call(self._collection, doc);
|
|
};
|
|
|
|
var transformDoc = function (validator, doc) {
|
|
if (validator.transform)
|
|
return validator.transform(doc);
|
|
return doc;
|
|
};
|
|
|
|
// Simulate a mongo `update` operation while validating that the access
|
|
// control rules set by calls to `allow/deny` are satisfied. If all
|
|
// pass, rewrite the mongo operation to use $in to set the list of
|
|
// document ids to change ##ValidatedChange
|
|
Mongo.Collection.prototype._validatedUpdate = function(
|
|
userId, selector, mutator, options) {
|
|
var self = this;
|
|
|
|
options = options || {};
|
|
|
|
if (!LocalCollection._selectorIsIdPerhapsAsObject(selector))
|
|
throw new Error("validated update should be of a single ID");
|
|
|
|
// We don't support upserts because they don't fit nicely into allow/deny
|
|
// rules.
|
|
if (options.upsert)
|
|
throw new Meteor.Error(403, "Access denied. Upserts not " +
|
|
"allowed in a restricted collection.");
|
|
|
|
// compute modified fields
|
|
var fields = [];
|
|
_.each(mutator, function (params, op) {
|
|
if (op.charAt(0) !== '$') {
|
|
throw new Meteor.Error(
|
|
403, "Access denied. In a restricted collection you can only update documents, not replace them. Use a Mongo update operator, such as '$set'.");
|
|
} else if (!_.has(ALLOWED_UPDATE_OPERATIONS, op)) {
|
|
throw new Meteor.Error(
|
|
403, "Access denied. Operator " + op + " not allowed in a restricted collection.");
|
|
} else {
|
|
_.each(_.keys(params), function (field) {
|
|
// treat dotted fields as if they are replacing their
|
|
// top-level part
|
|
if (field.indexOf('.') !== -1)
|
|
field = field.substring(0, field.indexOf('.'));
|
|
|
|
// record the field we are trying to change
|
|
if (!_.contains(fields, field))
|
|
fields.push(field);
|
|
});
|
|
}
|
|
});
|
|
|
|
var findOptions = {transform: null};
|
|
if (!self._validators.fetchAllFields) {
|
|
findOptions.fields = {};
|
|
_.each(self._validators.fetch, function(fieldName) {
|
|
findOptions.fields[fieldName] = 1;
|
|
});
|
|
}
|
|
|
|
var doc = self._collection.findOne(selector, findOptions);
|
|
if (!doc) // none satisfied!
|
|
return 0;
|
|
|
|
var factoriedDoc;
|
|
|
|
// call user validators.
|
|
// Any deny returns true means denied.
|
|
if (_.any(self._validators.update.deny, function(validator) {
|
|
if (!factoriedDoc)
|
|
factoriedDoc = transformDoc(validator, doc);
|
|
return validator(userId,
|
|
factoriedDoc,
|
|
fields,
|
|
mutator);
|
|
})) {
|
|
throw new Meteor.Error(403, "Access denied");
|
|
}
|
|
// Any allow returns true means proceed. Throw error if they all fail.
|
|
if (_.all(self._validators.update.allow, function(validator) {
|
|
if (!factoriedDoc)
|
|
factoriedDoc = transformDoc(validator, doc);
|
|
return !validator(userId,
|
|
factoriedDoc,
|
|
fields,
|
|
mutator);
|
|
})) {
|
|
throw new Meteor.Error(403, "Access denied");
|
|
}
|
|
|
|
// Back when we supported arbitrary client-provided selectors, we actually
|
|
// rewrote the selector to include an _id clause before passing to Mongo to
|
|
// avoid races, but since selector is guaranteed to already just be an ID, we
|
|
// don't have to any more.
|
|
|
|
return self._collection.update.call(
|
|
self._collection, selector, mutator, options);
|
|
};
|
|
|
|
// Only allow these operations in validated updates. Specifically
|
|
// whitelist operations, rather than blacklist, so new complex
|
|
// operations that are added aren't automatically allowed. A complex
|
|
// operation is one that does more than just modify its target
|
|
// field. For now this contains all update operations except '$rename'.
|
|
// http://docs.mongodb.org/manual/reference/operators/#update
|
|
var ALLOWED_UPDATE_OPERATIONS = {
|
|
$inc:1, $set:1, $unset:1, $addToSet:1, $pop:1, $pullAll:1, $pull:1,
|
|
$pushAll:1, $push:1, $bit:1
|
|
};
|
|
|
|
// Simulate a mongo `remove` operation while validating access control
|
|
// rules. See #ValidatedChange
|
|
Mongo.Collection.prototype._validatedRemove = function(userId, selector) {
|
|
var self = this;
|
|
|
|
var findOptions = {transform: null};
|
|
if (!self._validators.fetchAllFields) {
|
|
findOptions.fields = {};
|
|
_.each(self._validators.fetch, function(fieldName) {
|
|
findOptions.fields[fieldName] = 1;
|
|
});
|
|
}
|
|
|
|
var doc = self._collection.findOne(selector, findOptions);
|
|
if (!doc)
|
|
return 0;
|
|
|
|
// call user validators.
|
|
// Any deny returns true means denied.
|
|
if (_.any(self._validators.remove.deny, function(validator) {
|
|
return validator(userId, transformDoc(validator, doc));
|
|
})) {
|
|
throw new Meteor.Error(403, "Access denied");
|
|
}
|
|
// Any allow returns true means proceed. Throw error if they all fail.
|
|
if (_.all(self._validators.remove.allow, function(validator) {
|
|
return !validator(userId, transformDoc(validator, doc));
|
|
})) {
|
|
throw new Meteor.Error(403, "Access denied");
|
|
}
|
|
|
|
// Back when we supported arbitrary client-provided selectors, we actually
|
|
// rewrote the selector to {_id: {$in: [ids that we found]}} before passing to
|
|
// Mongo to avoid races, but since selector is guaranteed to already just be
|
|
// an ID, we don't have to any more.
|
|
|
|
return self._collection.remove.call(self._collection, selector);
|
|
};
|
|
|
|
/**
|
|
* @deprecated in 0.9.1
|
|
*/
|
|
Meteor.Collection = Mongo.Collection;
|