1233 lines
146 KiB
JavaScript
1233 lines
146 KiB
JavaScript
//////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// This is a generated file. You can view the original //
|
|
// source in your browser if your browser supports source maps. //
|
|
// //
|
|
// If you are using Chrome, open the Developer Tools and click the gear //
|
|
// icon in its lower right corner. In the General Settings panel, turn //
|
|
// on 'Enable source maps'. //
|
|
// //
|
|
// If you are using Firefox 23, go to `about:config` and set the //
|
|
// `devtools.debugger.source-maps-enabled` preference to true. //
|
|
// (The preference should be on by default in Firefox 24; versions //
|
|
// older than 23 do not support source maps.) //
|
|
// //
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
(function () {
|
|
|
|
/* Imports */
|
|
var Meteor = Package.meteor.Meteor;
|
|
var Random = Package.random.Random;
|
|
var EJSON = Package.ejson.EJSON;
|
|
var JSON = Package.json.JSON;
|
|
var _ = Package.underscore._;
|
|
var LocalCollection = Package.minimongo.LocalCollection;
|
|
var Minimongo = Package.minimongo.Minimongo;
|
|
var Log = Package.logging.Log;
|
|
var DDP = Package.ddp.DDP;
|
|
var Tracker = Package.tracker.Tracker;
|
|
var Deps = Package.tracker.Deps;
|
|
var check = Package.check.check;
|
|
var Match = Package.check.Match;
|
|
|
|
/* Package-scope variables */
|
|
var Mongo, LocalCollectionDriver;
|
|
|
|
(function () {
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/mongo/local_collection_driver.js //
|
|
// //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
LocalCollectionDriver = function () { // 1
|
|
var self = this; // 2
|
|
self.noConnCollections = {}; // 3
|
|
}; // 4
|
|
// 5
|
|
var ensureCollection = function (name, collections) { // 6
|
|
if (!(name in collections)) // 7
|
|
collections[name] = new LocalCollection(name); // 8
|
|
return collections[name]; // 9
|
|
}; // 10
|
|
// 11
|
|
_.extend(LocalCollectionDriver.prototype, { // 12
|
|
open: function (name, conn) { // 13
|
|
var self = this; // 14
|
|
if (!name) // 15
|
|
return new LocalCollection; // 16
|
|
if (! conn) { // 17
|
|
return ensureCollection(name, self.noConnCollections); // 18
|
|
} // 19
|
|
if (! conn._mongo_livedata_collections) // 20
|
|
conn._mongo_livedata_collections = {}; // 21
|
|
// XXX is there a way to keep track of a connection's collections without // 22
|
|
// dangling it off the connection object? // 23
|
|
return ensureCollection(name, conn._mongo_livedata_collections); // 24
|
|
} // 25
|
|
}); // 26
|
|
// 27
|
|
// singleton // 28
|
|
LocalCollectionDriver = new LocalCollectionDriver; // 29
|
|
// 30
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function () {
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/mongo/collection.js //
|
|
// //
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// options.connection, if given, is a LivedataClient or LivedataServer // 1
|
|
// XXX presently there is no way to destroy/clean up a Collection // 2
|
|
// 3
|
|
/** // 4
|
|
* @summary Namespace for MongoDB-related items // 5
|
|
* @namespace // 6
|
|
*/ // 7
|
|
Mongo = {}; // 8
|
|
// 9
|
|
/** // 10
|
|
* @summary Constructor for a Collection // 11
|
|
* @locus Anywhere // 12
|
|
* @instancename collection // 13
|
|
* @class // 14
|
|
* @param {String} name The name of the collection. If null, creates an unmanaged (unsynchronized) local collection. // 15
|
|
* @param {Object} [options] // 16
|
|
* @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:
|
|
// 19
|
|
- **`'STRING'`**: random strings // 20
|
|
- **`'MONGO'`**: random [`Mongo.ObjectID`](#mongo_object_id) values // 21
|
|
// 22
|
|
The default id generation technique is `'STRING'`. // 23
|
|
* @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.
|
|
*/ // 25
|
|
Mongo.Collection = function (name, options) { // 26
|
|
var self = this; // 27
|
|
if (! (self instanceof Mongo.Collection)) // 28
|
|
throw new Error('use "new" to construct a Mongo.Collection'); // 29
|
|
// 30
|
|
if (!name && (name !== null)) { // 31
|
|
Meteor._debug("Warning: creating anonymous collection. It will not be " + // 32
|
|
"saved or synchronized over the network. (Pass null for " + // 33
|
|
"the collection name to turn off this warning.)"); // 34
|
|
name = null; // 35
|
|
} // 36
|
|
// 37
|
|
if (name !== null && typeof name !== "string") { // 38
|
|
throw new Error( // 39
|
|
"First argument to new Mongo.Collection must be a string or null"); // 40
|
|
} // 41
|
|
// 42
|
|
if (options && options.methods) { // 43
|
|
// Backwards compatibility hack with original signature (which passed // 44
|
|
// "connection" directly instead of in options. (Connections must have a "methods" // 45
|
|
// method.) // 46
|
|
// XXX remove before 1.0 // 47
|
|
options = {connection: options}; // 48
|
|
} // 49
|
|
// Backwards compatibility: "connection" used to be called "manager". // 50
|
|
if (options && options.manager && !options.connection) { // 51
|
|
options.connection = options.manager; // 52
|
|
} // 53
|
|
options = _.extend({ // 54
|
|
connection: undefined, // 55
|
|
idGeneration: 'STRING', // 56
|
|
transform: null, // 57
|
|
_driver: undefined, // 58
|
|
_preventAutopublish: false // 59
|
|
}, options); // 60
|
|
// 61
|
|
switch (options.idGeneration) { // 62
|
|
case 'MONGO': // 63
|
|
self._makeNewID = function () { // 64
|
|
var src = name ? DDP.randomStream('/collection/' + name) : Random; // 65
|
|
return new Mongo.ObjectID(src.hexString(24)); // 66
|
|
}; // 67
|
|
break; // 68
|
|
case 'STRING': // 69
|
|
default: // 70
|
|
self._makeNewID = function () { // 71
|
|
var src = name ? DDP.randomStream('/collection/' + name) : Random; // 72
|
|
return src.id(); // 73
|
|
}; // 74
|
|
break; // 75
|
|
} // 76
|
|
// 77
|
|
self._transform = LocalCollection.wrapTransform(options.transform); // 78
|
|
// 79
|
|
if (! name || options.connection === null) // 80
|
|
// note: nameless collections never have a connection // 81
|
|
self._connection = null; // 82
|
|
else if (options.connection) // 83
|
|
self._connection = options.connection; // 84
|
|
else if (Meteor.isClient) // 85
|
|
self._connection = Meteor.connection; // 86
|
|
else // 87
|
|
self._connection = Meteor.server; // 88
|
|
// 89
|
|
if (!options._driver) { // 90
|
|
// XXX This check assumes that webapp is loaded so that Meteor.server !== // 91
|
|
// null. We should fully support the case of "want to use a Mongo-backed // 92
|
|
// collection from Node code without webapp", but we don't yet. // 93
|
|
// #MeteorServerNull // 94
|
|
if (name && self._connection === Meteor.server && // 95
|
|
typeof MongoInternals !== "undefined" && // 96
|
|
MongoInternals.defaultRemoteCollectionDriver) { // 97
|
|
options._driver = MongoInternals.defaultRemoteCollectionDriver(); // 98
|
|
} else { // 99
|
|
options._driver = LocalCollectionDriver; // 100
|
|
} // 101
|
|
} // 102
|
|
// 103
|
|
self._collection = options._driver.open(name, self._connection); // 104
|
|
self._name = name; // 105
|
|
self._driver = options._driver; // 106
|
|
// 107
|
|
if (self._connection && self._connection.registerStore) { // 108
|
|
// OK, we're going to be a slave, replicating some remote // 109
|
|
// database, except possibly with some temporary divergence while // 110
|
|
// we have unacknowledged RPC's. // 111
|
|
var ok = self._connection.registerStore(name, { // 112
|
|
// Called at the beginning of a batch of updates. batchSize is the number // 113
|
|
// of update calls to expect. // 114
|
|
// // 115
|
|
// XXX This interface is pretty janky. reset probably ought to go back to // 116
|
|
// being its own function, and callers shouldn't have to calculate // 117
|
|
// batchSize. The optimization of not calling pause/remove should be // 118
|
|
// delayed until later: the first call to update() should buffer its // 119
|
|
// message, and then we can either directly apply it at endUpdate time if // 120
|
|
// it was the only update, or do pauseObservers/apply/apply at the next // 121
|
|
// update() if there's another one. // 122
|
|
beginUpdate: function (batchSize, reset) { // 123
|
|
// pause observers so users don't see flicker when updating several // 124
|
|
// objects at once (including the post-reconnect reset-and-reapply // 125
|
|
// stage), and so that a re-sorting of a query can take advantage of the // 126
|
|
// full _diffQuery moved calculation instead of applying change one at a // 127
|
|
// time. // 128
|
|
if (batchSize > 1 || reset) // 129
|
|
self._collection.pauseObservers(); // 130
|
|
// 131
|
|
if (reset) // 132
|
|
self._collection.remove({}); // 133
|
|
}, // 134
|
|
// 135
|
|
// Apply an update. // 136
|
|
// XXX better specify this interface (not in terms of a wire message)? // 137
|
|
update: function (msg) { // 138
|
|
var mongoId = LocalCollection._idParse(msg.id); // 139
|
|
var doc = self._collection.findOne(mongoId); // 140
|
|
// 141
|
|
// Is this a "replace the whole doc" message coming from the quiescence // 142
|
|
// of method writes to an object? (Note that 'undefined' is a valid // 143
|
|
// value meaning "remove it".) // 144
|
|
if (msg.msg === 'replace') { // 145
|
|
var replace = msg.replace; // 146
|
|
if (!replace) { // 147
|
|
if (doc) // 148
|
|
self._collection.remove(mongoId); // 149
|
|
} else if (!doc) { // 150
|
|
self._collection.insert(replace); // 151
|
|
} else { // 152
|
|
// XXX check that replace has no $ ops // 153
|
|
self._collection.update(mongoId, replace); // 154
|
|
} // 155
|
|
return; // 156
|
|
} else if (msg.msg === 'added') { // 157
|
|
if (doc) { // 158
|
|
throw new Error("Expected not to find a document already present for an add"); // 159
|
|
} // 160
|
|
self._collection.insert(_.extend({_id: mongoId}, msg.fields)); // 161
|
|
} else if (msg.msg === 'removed') { // 162
|
|
if (!doc) // 163
|
|
throw new Error("Expected to find a document already present for removed"); // 164
|
|
self._collection.remove(mongoId); // 165
|
|
} else if (msg.msg === 'changed') { // 166
|
|
if (!doc) // 167
|
|
throw new Error("Expected to find a document to change"); // 168
|
|
if (!_.isEmpty(msg.fields)) { // 169
|
|
var modifier = {}; // 170
|
|
_.each(msg.fields, function (value, key) { // 171
|
|
if (value === undefined) { // 172
|
|
if (!modifier.$unset) // 173
|
|
modifier.$unset = {}; // 174
|
|
modifier.$unset[key] = 1; // 175
|
|
} else { // 176
|
|
if (!modifier.$set) // 177
|
|
modifier.$set = {}; // 178
|
|
modifier.$set[key] = value; // 179
|
|
} // 180
|
|
}); // 181
|
|
self._collection.update(mongoId, modifier); // 182
|
|
} // 183
|
|
} else { // 184
|
|
throw new Error("I don't know how to deal with this message"); // 185
|
|
} // 186
|
|
// 187
|
|
}, // 188
|
|
// 189
|
|
// Called at the end of a batch of updates. // 190
|
|
endUpdate: function () { // 191
|
|
self._collection.resumeObservers(); // 192
|
|
}, // 193
|
|
// 194
|
|
// Called around method stub invocations to capture the original versions // 195
|
|
// of modified documents. // 196
|
|
saveOriginals: function () { // 197
|
|
self._collection.saveOriginals(); // 198
|
|
}, // 199
|
|
retrieveOriginals: function () { // 200
|
|
return self._collection.retrieveOriginals(); // 201
|
|
} // 202
|
|
}); // 203
|
|
// 204
|
|
if (!ok) // 205
|
|
throw new Error("There is already a collection named '" + name + "'"); // 206
|
|
} // 207
|
|
// 208
|
|
self._defineMutationMethods(); // 209
|
|
// 210
|
|
// autopublish // 211
|
|
if (Package.autopublish && !options._preventAutopublish && self._connection // 212
|
|
&& self._connection.publish) { // 213
|
|
self._connection.publish(null, function () { // 214
|
|
return self.find(); // 215
|
|
}, {is_auto: true}); // 216
|
|
} // 217
|
|
}; // 218
|
|
// 219
|
|
/// // 220
|
|
/// Main collection API // 221
|
|
/// // 222
|
|
// 223
|
|
// 224
|
|
_.extend(Mongo.Collection.prototype, { // 225
|
|
// 226
|
|
_getFindSelector: function (args) { // 227
|
|
if (args.length == 0) // 228
|
|
return {}; // 229
|
|
else // 230
|
|
return args[0]; // 231
|
|
}, // 232
|
|
// 233
|
|
_getFindOptions: function (args) { // 234
|
|
var self = this; // 235
|
|
if (args.length < 2) { // 236
|
|
return { transform: self._transform }; // 237
|
|
} else { // 238
|
|
check(args[1], Match.Optional(Match.ObjectIncluding({ // 239
|
|
fields: Match.Optional(Match.OneOf(Object, undefined)), // 240
|
|
sort: Match.Optional(Match.OneOf(Object, Array, undefined)), // 241
|
|
limit: Match.Optional(Match.OneOf(Number, undefined)), // 242
|
|
skip: Match.Optional(Match.OneOf(Number, undefined)) // 243
|
|
}))); // 244
|
|
// 245
|
|
return _.extend({ // 246
|
|
transform: self._transform // 247
|
|
}, args[1]); // 248
|
|
} // 249
|
|
}, // 250
|
|
// 251
|
|
/** // 252
|
|
* @summary Find the documents in a collection that match the selector. // 253
|
|
* @locus Anywhere // 254
|
|
* @method find // 255
|
|
* @memberOf Mongo.Collection // 256
|
|
* @instance // 257
|
|
* @param {MongoSelector} [selector] A query describing the documents to find // 258
|
|
* @param {Object} [options] // 259
|
|
* @param {MongoSortSpecifier} options.sort Sort order (default: natural order) // 260
|
|
* @param {Number} options.skip Number of results to skip at the beginning // 261
|
|
* @param {Number} options.limit Maximum number of results to return // 262
|
|
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude. // 263
|
|
* @param {Boolean} options.reactive (Client only) Default `true`; pass `false` to disable reactivity // 264
|
|
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections) for this cursor. Pass `null` to disable transformation.
|
|
* @returns {Mongo.Cursor} // 266
|
|
*/ // 267
|
|
find: function (/* selector, options */) { // 268
|
|
// Collection.find() (return all docs) behaves differently // 269
|
|
// from Collection.find(undefined) (return 0 docs). so be // 270
|
|
// careful about the length of arguments. // 271
|
|
var self = this; // 272
|
|
var argArray = _.toArray(arguments); // 273
|
|
return self._collection.find(self._getFindSelector(argArray), // 274
|
|
self._getFindOptions(argArray)); // 275
|
|
}, // 276
|
|
// 277
|
|
/** // 278
|
|
* @summary Finds the first document that matches the selector, as ordered by sort and skip options. // 279
|
|
* @locus Anywhere // 280
|
|
* @method findOne // 281
|
|
* @memberOf Mongo.Collection // 282
|
|
* @instance // 283
|
|
* @param {MongoSelector} [selector] A query describing the documents to find // 284
|
|
* @param {Object} [options] // 285
|
|
* @param {MongoSortSpecifier} options.sort Sort order (default: natural order) // 286
|
|
* @param {Number} options.skip Number of results to skip at the beginning // 287
|
|
* @param {MongoFieldSpecifier} options.fields Dictionary of fields to return or exclude. // 288
|
|
* @param {Boolean} options.reactive (Client only) Default true; pass false to disable reactivity // 289
|
|
* @param {Function} options.transform Overrides `transform` on the [`Collection`](#collections) for this cursor. Pass `null` to disable transformation.
|
|
* @returns {Object} // 291
|
|
*/ // 292
|
|
findOne: function (/* selector, options */) { // 293
|
|
var self = this; // 294
|
|
var argArray = _.toArray(arguments); // 295
|
|
return self._collection.findOne(self._getFindSelector(argArray), // 296
|
|
self._getFindOptions(argArray)); // 297
|
|
} // 298
|
|
// 299
|
|
}); // 300
|
|
// 301
|
|
Mongo.Collection._publishCursor = function (cursor, sub, collection) { // 302
|
|
var observeHandle = cursor.observeChanges({ // 303
|
|
added: function (id, fields) { // 304
|
|
sub.added(collection, id, fields); // 305
|
|
}, // 306
|
|
changed: function (id, fields) { // 307
|
|
sub.changed(collection, id, fields); // 308
|
|
}, // 309
|
|
removed: function (id) { // 310
|
|
sub.removed(collection, id); // 311
|
|
} // 312
|
|
}); // 313
|
|
// 314
|
|
// We don't call sub.ready() here: it gets called in livedata_server, after // 315
|
|
// possibly calling _publishCursor on multiple returned cursors. // 316
|
|
// 317
|
|
// register stop callback (expects lambda w/ no args). // 318
|
|
sub.onStop(function () {observeHandle.stop();}); // 319
|
|
}; // 320
|
|
// 321
|
|
// protect against dangerous selectors. falsey and {_id: falsey} are both // 322
|
|
// likely programmer error, and not what you want, particularly for destructive // 323
|
|
// operations. JS regexps don't serialize over DDP but can be trivially // 324
|
|
// replaced by $regex. // 325
|
|
Mongo.Collection._rewriteSelector = function (selector) { // 326
|
|
// shorthand -- scalars match _id // 327
|
|
if (LocalCollection._selectorIsId(selector)) // 328
|
|
selector = {_id: selector}; // 329
|
|
// 330
|
|
if (!selector || (('_id' in selector) && !selector._id)) // 331
|
|
// can't match anything // 332
|
|
return {_id: Random.id()}; // 333
|
|
// 334
|
|
var ret = {}; // 335
|
|
_.each(selector, function (value, key) { // 336
|
|
// Mongo supports both {field: /foo/} and {field: {$regex: /foo/}} // 337
|
|
if (value instanceof RegExp) { // 338
|
|
ret[key] = convertRegexpToMongoSelector(value); // 339
|
|
} else if (value && value.$regex instanceof RegExp) { // 340
|
|
ret[key] = convertRegexpToMongoSelector(value.$regex); // 341
|
|
// if value is {$regex: /foo/, $options: ...} then $options // 342
|
|
// override the ones set on $regex. // 343
|
|
if (value.$options !== undefined) // 344
|
|
ret[key].$options = value.$options; // 345
|
|
} // 346
|
|
else if (_.contains(['$or','$and','$nor'], key)) { // 347
|
|
// Translate lower levels of $and/$or/$nor // 348
|
|
ret[key] = _.map(value, function (v) { // 349
|
|
return Mongo.Collection._rewriteSelector(v); // 350
|
|
}); // 351
|
|
} else { // 352
|
|
ret[key] = value; // 353
|
|
} // 354
|
|
}); // 355
|
|
return ret; // 356
|
|
}; // 357
|
|
// 358
|
|
// convert a JS RegExp object to a Mongo {$regex: ..., $options: ...} // 359
|
|
// selector // 360
|
|
var convertRegexpToMongoSelector = function (regexp) { // 361
|
|
check(regexp, RegExp); // safety belt // 362
|
|
// 363
|
|
var selector = {$regex: regexp.source}; // 364
|
|
var regexOptions = ''; // 365
|
|
// JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options // 366
|
|
// support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. // 367
|
|
if (regexp.ignoreCase) // 368
|
|
regexOptions += 'i'; // 369
|
|
if (regexp.multiline) // 370
|
|
regexOptions += 'm'; // 371
|
|
if (regexOptions) // 372
|
|
selector.$options = regexOptions; // 373
|
|
// 374
|
|
return selector; // 375
|
|
}; // 376
|
|
// 377
|
|
var throwIfSelectorIsNotId = function (selector, methodName) { // 378
|
|
if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) { // 379
|
|
throw new Meteor.Error( // 380
|
|
403, "Not permitted. Untrusted code may only " + methodName + // 381
|
|
" documents by ID."); // 382
|
|
} // 383
|
|
}; // 384
|
|
// 385
|
|
// 'insert' immediately returns the inserted document's new _id. // 386
|
|
// The others return values immediately if you are in a stub, an in-memory // 387
|
|
// unmanaged collection, or a mongo-backed collection and you don't pass a // 388
|
|
// callback. 'update' and 'remove' return the number of affected // 389
|
|
// documents. 'upsert' returns an object with keys 'numberAffected' and, if an // 390
|
|
// insert happened, 'insertedId'. // 391
|
|
// // 392
|
|
// Otherwise, the semantics are exactly like other methods: they take // 393
|
|
// a callback as an optional last argument; if no callback is // 394
|
|
// provided, they block until the operation is complete, and throw an // 395
|
|
// exception if it fails; if a callback is provided, then they don't // 396
|
|
// necessarily block, and they call the callback when they finish with error and // 397
|
|
// result arguments. (The insert method provides the document ID as its result; // 398
|
|
// update and remove provide the number of affected docs as the result; upsert // 399
|
|
// provides an object with numberAffected and maybe insertedId.) // 400
|
|
// // 401
|
|
// On the client, blocking is impossible, so if a callback // 402
|
|
// isn't provided, they just return immediately and any error // 403
|
|
// information is lost. // 404
|
|
// // 405
|
|
// There's one more tweak. On the client, if you don't provide a // 406
|
|
// callback, then if there is an error, a message will be logged with // 407
|
|
// Meteor._debug. // 408
|
|
// // 409
|
|
// The intent (though this is actually determined by the underlying // 410
|
|
// drivers) is that the operations should be done synchronously, not // 411
|
|
// generating their result until the database has acknowledged // 412
|
|
// them. In the future maybe we should provide a flag to turn this // 413
|
|
// off. // 414
|
|
// 415
|
|
/** // 416
|
|
* @summary Insert a document in the collection. Returns its unique _id. // 417
|
|
* @locus Anywhere // 418
|
|
* @method insert // 419
|
|
* @memberOf Mongo.Collection // 420
|
|
* @instance // 421
|
|
* @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.
|
|
*/ // 424
|
|
// 425
|
|
/** // 426
|
|
* @summary Modify one or more documents in the collection. Returns the number of affected documents. // 427
|
|
* @locus Anywhere // 428
|
|
* @method update // 429
|
|
* @memberOf Mongo.Collection // 430
|
|
* @instance // 431
|
|
* @param {MongoSelector} selector Specifies which documents to modify // 432
|
|
* @param {MongoModifier} modifier Specifies how to modify the documents // 433
|
|
* @param {Object} [options] // 434
|
|
* @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. // 436
|
|
* @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.
|
|
*/ // 438
|
|
// 439
|
|
/** // 440
|
|
* @summary Remove documents from the collection // 441
|
|
* @locus Anywhere // 442
|
|
* @method remove // 443
|
|
* @memberOf Mongo.Collection // 444
|
|
* @instance // 445
|
|
* @param {MongoSelector} selector Specifies which documents to remove // 446
|
|
* @param {Function} [callback] Optional. If present, called with an error object as its argument. // 447
|
|
*/ // 448
|
|
// 449
|
|
_.each(["insert", "update", "remove"], function (name) { // 450
|
|
Mongo.Collection.prototype[name] = function (/* arguments */) { // 451
|
|
var self = this; // 452
|
|
var args = _.toArray(arguments); // 453
|
|
var callback; // 454
|
|
var insertId; // 455
|
|
var ret; // 456
|
|
// 457
|
|
// Pull off any callback (or perhaps a 'callback' variable that was passed // 458
|
|
// in undefined, like how 'upsert' does it). // 459
|
|
if (args.length && // 460
|
|
(args[args.length - 1] === undefined || // 461
|
|
args[args.length - 1] instanceof Function)) { // 462
|
|
callback = args.pop(); // 463
|
|
} // 464
|
|
// 465
|
|
if (name === "insert") { // 466
|
|
if (!args.length) // 467
|
|
throw new Error("insert requires an argument"); // 468
|
|
// shallow-copy the document and generate an ID // 469
|
|
args[0] = _.extend({}, args[0]); // 470
|
|
if ('_id' in args[0]) { // 471
|
|
insertId = args[0]._id; // 472
|
|
if (!insertId || !(typeof insertId === 'string' // 473
|
|
|| insertId instanceof Mongo.ObjectID)) // 474
|
|
throw new Error("Meteor requires document _id fields to be non-empty strings or ObjectIDs"); // 475
|
|
} else { // 476
|
|
var generateId = true; // 477
|
|
// Don't generate the id if we're the client and the 'outermost' call // 478
|
|
// This optimization saves us passing both the randomSeed and the id // 479
|
|
// Passing both is redundant. // 480
|
|
if (self._connection && self._connection !== Meteor.server) { // 481
|
|
var enclosing = DDP._CurrentInvocation.get(); // 482
|
|
if (!enclosing) { // 483
|
|
generateId = false; // 484
|
|
} // 485
|
|
} // 486
|
|
if (generateId) { // 487
|
|
insertId = args[0]._id = self._makeNewID(); // 488
|
|
} // 489
|
|
} // 490
|
|
} else { // 491
|
|
args[0] = Mongo.Collection._rewriteSelector(args[0]); // 492
|
|
// 493
|
|
if (name === "update") { // 494
|
|
// Mutate args but copy the original options object. We need to add // 495
|
|
// insertedId to options, but don't want to mutate the caller's options // 496
|
|
// object. We need to mutate `args` because we pass `args` into the // 497
|
|
// driver below. // 498
|
|
var options = args[2] = _.clone(args[2]) || {}; // 499
|
|
if (options && typeof options !== "function" && options.upsert) { // 500
|
|
// set `insertedId` if absent. `insertedId` is a Meteor extension. // 501
|
|
if (options.insertedId) { // 502
|
|
if (!(typeof options.insertedId === 'string' // 503
|
|
|| options.insertedId instanceof Mongo.ObjectID)) // 504
|
|
throw new Error("insertedId must be string or ObjectID"); // 505
|
|
} else if (! args[0]._id) { // 506
|
|
options.insertedId = self._makeNewID(); // 507
|
|
} // 508
|
|
} // 509
|
|
} // 510
|
|
} // 511
|
|
// 512
|
|
// On inserts, always return the id that we generated; on all other // 513
|
|
// operations, just return the result from the collection. // 514
|
|
var chooseReturnValueFromCollectionResult = function (result) { // 515
|
|
if (name === "insert") { // 516
|
|
if (!insertId && result) { // 517
|
|
insertId = result; // 518
|
|
} // 519
|
|
return insertId; // 520
|
|
} else { // 521
|
|
return result; // 522
|
|
} // 523
|
|
}; // 524
|
|
// 525
|
|
var wrappedCallback; // 526
|
|
if (callback) { // 527
|
|
wrappedCallback = function (error, result) { // 528
|
|
callback(error, ! error && chooseReturnValueFromCollectionResult(result)); // 529
|
|
}; // 530
|
|
} // 531
|
|
// 532
|
|
// XXX see #MeteorServerNull // 533
|
|
if (self._connection && self._connection !== Meteor.server) { // 534
|
|
// just remote to another endpoint, propagate return value or // 535
|
|
// exception. // 536
|
|
// 537
|
|
var enclosing = DDP._CurrentInvocation.get(); // 538
|
|
var alreadyInSimulation = enclosing && enclosing.isSimulation; // 539
|
|
// 540
|
|
if (Meteor.isClient && !wrappedCallback && ! alreadyInSimulation) { // 541
|
|
// Client can't block, so it can't report errors by exception, // 542
|
|
// only by callback. If they forget the callback, give them a // 543
|
|
// default one that logs the error, so they aren't totally // 544
|
|
// baffled if their writes don't work because their database is // 545
|
|
// down. // 546
|
|
// Don't give a default callback in simulation, because inside stubs we // 547
|
|
// want to return the results from the local collection immediately and // 548
|
|
// not force a callback. // 549
|
|
wrappedCallback = function (err) { // 550
|
|
if (err) // 551
|
|
Meteor._debug(name + " failed: " + (err.reason || err.stack)); // 552
|
|
}; // 553
|
|
} // 554
|
|
// 555
|
|
if (!alreadyInSimulation && name !== "insert") { // 556
|
|
// If we're about to actually send an RPC, we should throw an error if // 557
|
|
// this is a non-ID selector, because the mutation methods only allow // 558
|
|
// single-ID selectors. (If we don't throw here, we'll see flicker.) // 559
|
|
throwIfSelectorIsNotId(args[0], name); // 560
|
|
} // 561
|
|
// 562
|
|
ret = chooseReturnValueFromCollectionResult( // 563
|
|
self._connection.apply(self._prefix + name, args, {returnStubValue: true}, wrappedCallback) // 564
|
|
); // 565
|
|
// 566
|
|
} else { // 567
|
|
// it's my collection. descend into the collection object // 568
|
|
// and propagate any exception. // 569
|
|
args.push(wrappedCallback); // 570
|
|
try { // 571
|
|
// If the user provided a callback and the collection implements this // 572
|
|
// operation asynchronously, then queryRet will be undefined, and the // 573
|
|
// result will be returned through the callback instead. // 574
|
|
var queryRet = self._collection[name].apply(self._collection, args); // 575
|
|
ret = chooseReturnValueFromCollectionResult(queryRet); // 576
|
|
} catch (e) { // 577
|
|
if (callback) { // 578
|
|
callback(e); // 579
|
|
return null; // 580
|
|
} // 581
|
|
throw e; // 582
|
|
} // 583
|
|
} // 584
|
|
// 585
|
|
// both sync and async, unless we threw an exception, return ret // 586
|
|
// (new document ID for insert, num affected for update/remove, object with // 587
|
|
// numberAffected and maybe insertedId for upsert). // 588
|
|
return ret; // 589
|
|
}; // 590
|
|
}); // 591
|
|
// 592
|
|
/** // 593
|
|
* @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 // 595
|
|
* @param {MongoSelector} selector Specifies which documents to modify // 596
|
|
* @param {MongoModifier} modifier Specifies how to modify the documents // 597
|
|
* @param {Object} [options] // 598
|
|
* @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.
|
|
*/ // 601
|
|
Mongo.Collection.prototype.upsert = function (selector, modifier, // 602
|
|
options, callback) { // 603
|
|
var self = this; // 604
|
|
if (! callback && typeof options === "function") { // 605
|
|
callback = options; // 606
|
|
options = {}; // 607
|
|
} // 608
|
|
return self.update(selector, modifier, // 609
|
|
_.extend({}, options, { _returnObject: true, upsert: true }), // 610
|
|
callback); // 611
|
|
}; // 612
|
|
// 613
|
|
// We'll actually design an index API later. For now, we just pass through to // 614
|
|
// Mongo's, but make it synchronous. // 615
|
|
Mongo.Collection.prototype._ensureIndex = function (index, options) { // 616
|
|
var self = this; // 617
|
|
if (!self._collection._ensureIndex) // 618
|
|
throw new Error("Can only call _ensureIndex on server collections"); // 619
|
|
self._collection._ensureIndex(index, options); // 620
|
|
}; // 621
|
|
Mongo.Collection.prototype._dropIndex = function (index) { // 622
|
|
var self = this; // 623
|
|
if (!self._collection._dropIndex) // 624
|
|
throw new Error("Can only call _dropIndex on server collections"); // 625
|
|
self._collection._dropIndex(index); // 626
|
|
}; // 627
|
|
Mongo.Collection.prototype._dropCollection = function () { // 628
|
|
var self = this; // 629
|
|
if (!self._collection.dropCollection) // 630
|
|
throw new Error("Can only call _dropCollection on server collections"); // 631
|
|
self._collection.dropCollection(); // 632
|
|
}; // 633
|
|
Mongo.Collection.prototype._createCappedCollection = function (byteSize, maxDocuments) { // 634
|
|
var self = this; // 635
|
|
if (!self._collection._createCappedCollection) // 636
|
|
throw new Error("Can only call _createCappedCollection on server collections"); // 637
|
|
self._collection._createCappedCollection(byteSize, maxDocuments); // 638
|
|
}; // 639
|
|
// 640
|
|
Mongo.Collection.prototype.rawCollection = function () { // 641
|
|
var self = this; // 642
|
|
if (! self._collection.rawCollection) { // 643
|
|
throw new Error("Can only call rawCollection on server collections"); // 644
|
|
} // 645
|
|
return self._collection.rawCollection(); // 646
|
|
}; // 647
|
|
// 648
|
|
Mongo.Collection.prototype.rawDatabase = function () { // 649
|
|
var self = this; // 650
|
|
if (! (self._driver.mongo && self._driver.mongo.db)) { // 651
|
|
throw new Error("Can only call rawDatabase on server collections"); // 652
|
|
} // 653
|
|
return self._driver.mongo.db; // 654
|
|
}; // 655
|
|
// 656
|
|
// 657
|
|
/** // 658
|
|
* @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 // 660
|
|
* @class // 661
|
|
* @param {String} hexString Optional. The 24-character hexadecimal contents of the ObjectID to create // 662
|
|
*/ // 663
|
|
Mongo.ObjectID = LocalCollection._ObjectID; // 664
|
|
// 665
|
|
/** // 666
|
|
* @summary To create a cursor, use find. To access the documents in a cursor, use forEach, map, or fetch. // 667
|
|
* @class // 668
|
|
* @instanceName cursor // 669
|
|
*/ // 670
|
|
Mongo.Cursor = LocalCollection.Cursor; // 671
|
|
// 672
|
|
/** // 673
|
|
* @deprecated in 0.9.1 // 674
|
|
*/ // 675
|
|
Mongo.Collection.Cursor = Mongo.Cursor; // 676
|
|
// 677
|
|
/** // 678
|
|
* @deprecated in 0.9.1 // 679
|
|
*/ // 680
|
|
Mongo.Collection.ObjectID = Mongo.ObjectID; // 681
|
|
// 682
|
|
/// // 683
|
|
/// Remote methods and access control. // 684
|
|
/// // 685
|
|
// 686
|
|
// Restrict default mutators on collection. allow() and deny() take the // 687
|
|
// same options: // 688
|
|
// // 689
|
|
// options.insert {Function(userId, doc)} // 690
|
|
// return true to allow/deny adding this document // 691
|
|
// // 692
|
|
// options.update {Function(userId, docs, fields, modifier)} // 693
|
|
// return true to allow/deny updating these documents. // 694
|
|
// `fields` is passed as an array of fields that are to be modified // 695
|
|
// // 696
|
|
// options.remove {Function(userId, docs)} // 697
|
|
// return true to allow/deny removing these documents // 698
|
|
// // 699
|
|
// options.fetch {Array} // 700
|
|
// Fields to fetch for these validators. If any call to allow or deny // 701
|
|
// does not have this option then all fields are loaded. // 702
|
|
// // 703
|
|
// allow and deny can be called multiple times. The validators are // 704
|
|
// evaluated as follows: // 705
|
|
// - If neither deny() nor allow() has been called on the collection, // 706
|
|
// then the request is allowed if and only if the "insecure" smart // 707
|
|
// package is in use. // 708
|
|
// - Otherwise, if any deny() function returns true, the request is denied. // 709
|
|
// - Otherwise, if any allow() function returns true, the request is allowed. // 710
|
|
// - Otherwise, the request is denied. // 711
|
|
// // 712
|
|
// Meteor may call your deny() and allow() functions in any order, and may not // 713
|
|
// call all of them if it is able to make a decision without calling them all // 714
|
|
// (so don't include side effects). // 715
|
|
// 716
|
|
(function () { // 717
|
|
var addValidator = function(allowOrDeny, options) { // 718
|
|
// validate keys // 719
|
|
var VALID_KEYS = ['insert', 'update', 'remove', 'fetch', 'transform']; // 720
|
|
_.each(_.keys(options), function (key) { // 721
|
|
if (!_.contains(VALID_KEYS, key)) // 722
|
|
throw new Error(allowOrDeny + ": Invalid key: " + key); // 723
|
|
}); // 724
|
|
// 725
|
|
var self = this; // 726
|
|
self._restricted = true; // 727
|
|
// 728
|
|
_.each(['insert', 'update', 'remove'], function (name) { // 729
|
|
if (options[name]) { // 730
|
|
if (!(options[name] instanceof Function)) { // 731
|
|
throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); // 732
|
|
} // 733
|
|
// 734
|
|
// If the transform is specified at all (including as 'null') in this // 735
|
|
// call, then take that; otherwise, take the transform from the // 736
|
|
// collection. // 737
|
|
if (options.transform === undefined) { // 738
|
|
options[name].transform = self._transform; // already wrapped // 739
|
|
} else { // 740
|
|
options[name].transform = LocalCollection.wrapTransform( // 741
|
|
options.transform); // 742
|
|
} // 743
|
|
// 744
|
|
self._validators[name][allowOrDeny].push(options[name]); // 745
|
|
} // 746
|
|
}); // 747
|
|
// 748
|
|
// Only update the fetch fields if we're passed things that affect // 749
|
|
// fetching. This way allow({}) and allow({insert: f}) don't result in // 750
|
|
// setting fetchAllFields // 751
|
|
if (options.update || options.remove || options.fetch) { // 752
|
|
if (options.fetch && !(options.fetch instanceof Array)) { // 753
|
|
throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); // 754
|
|
} // 755
|
|
self._updateFetch(options.fetch); // 756
|
|
} // 757
|
|
}; // 758
|
|
// 759
|
|
/** // 760
|
|
* @summary Allow users to write directly to this collection from client code, subject to limitations you define. // 761
|
|
* @locus Server // 762
|
|
* @param {Object} options // 763
|
|
* @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.
|
|
*/ // 767
|
|
Mongo.Collection.prototype.allow = function(options) { // 768
|
|
addValidator.call(this, 'allow', options); // 769
|
|
}; // 770
|
|
// 771
|
|
/** // 772
|
|
* @summary Override `allow` rules. // 773
|
|
* @locus Server // 774
|
|
* @param {Object} options // 775
|
|
* @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.
|
|
*/ // 779
|
|
Mongo.Collection.prototype.deny = function(options) { // 780
|
|
addValidator.call(this, 'deny', options); // 781
|
|
}; // 782
|
|
})(); // 783
|
|
// 784
|
|
// 785
|
|
Mongo.Collection.prototype._defineMutationMethods = function() { // 786
|
|
var self = this; // 787
|
|
// 788
|
|
// set to true once we call any allow or deny methods. If true, use // 789
|
|
// allow/deny semantics. If false, use insecure mode semantics. // 790
|
|
self._restricted = false; // 791
|
|
// 792
|
|
// Insecure mode (default to allowing writes). Defaults to 'undefined' which // 793
|
|
// means insecure iff the insecure package is loaded. This property can be // 794
|
|
// overriden by tests or packages wishing to change insecure mode behavior of // 795
|
|
// their collections. // 796
|
|
self._insecure = undefined; // 797
|
|
// 798
|
|
self._validators = { // 799
|
|
insert: {allow: [], deny: []}, // 800
|
|
update: {allow: [], deny: []}, // 801
|
|
remove: {allow: [], deny: []}, // 802
|
|
upsert: {allow: [], deny: []}, // dummy arrays; can't set these! // 803
|
|
fetch: [], // 804
|
|
fetchAllFields: false // 805
|
|
}; // 806
|
|
// 807
|
|
if (!self._name) // 808
|
|
return; // anonymous collection // 809
|
|
// 810
|
|
// XXX Think about method namespacing. Maybe methods should be // 811
|
|
// "Meteor:Mongo:insert/NAME"? // 812
|
|
self._prefix = '/' + self._name + '/'; // 813
|
|
// 814
|
|
// mutation methods // 815
|
|
if (self._connection) { // 816
|
|
var m = {}; // 817
|
|
// 818
|
|
_.each(['insert', 'update', 'remove'], function (method) { // 819
|
|
m[self._prefix + method] = function (/* ... */) { // 820
|
|
// All the methods do their own validation, instead of using check(). // 821
|
|
check(arguments, [Match.Any]); // 822
|
|
var args = _.toArray(arguments); // 823
|
|
try { // 824
|
|
// For an insert, if the client didn't specify an _id, generate one // 825
|
|
// now; because this uses DDP.randomStream, it will be consistent with // 826
|
|
// what the client generated. We generate it now rather than later so // 827
|
|
// that if (eg) an allow/deny rule does an insert to the same // 828
|
|
// collection (not that it really should), the generated _id will // 829
|
|
// still be the first use of the stream and will be consistent. // 830
|
|
// // 831
|
|
// However, we don't actually stick the _id onto the document yet, // 832
|
|
// because we want allow/deny rules to be able to differentiate // 833
|
|
// between arbitrary client-specified _id fields and merely // 834
|
|
// client-controlled-via-randomSeed fields. // 835
|
|
var generatedId = null; // 836
|
|
if (method === "insert" && !_.has(args[0], '_id')) { // 837
|
|
generatedId = self._makeNewID(); // 838
|
|
} // 839
|
|
// 840
|
|
if (this.isSimulation) { // 841
|
|
// In a client simulation, you can do any mutation (even with a // 842
|
|
// complex selector). // 843
|
|
if (generatedId !== null) // 844
|
|
args[0]._id = generatedId; // 845
|
|
return self._collection[method].apply( // 846
|
|
self._collection, args); // 847
|
|
} // 848
|
|
// 849
|
|
// This is the server receiving a method call from the client. // 850
|
|
// 851
|
|
// We don't allow arbitrary selectors in mutations from the client: only // 852
|
|
// single-ID selectors. // 853
|
|
if (method !== 'insert') // 854
|
|
throwIfSelectorIsNotId(args[0], method); // 855
|
|
// 856
|
|
if (self._restricted) { // 857
|
|
// short circuit if there is no way it will pass. // 858
|
|
if (self._validators[method].allow.length === 0) { // 859
|
|
throw new Meteor.Error( // 860
|
|
403, "Access denied. No allow validators set on restricted " + // 861
|
|
"collection for method '" + method + "'."); // 862
|
|
} // 863
|
|
// 864
|
|
var validatedMethodName = // 865
|
|
'_validated' + method.charAt(0).toUpperCase() + method.slice(1); // 866
|
|
args.unshift(this.userId); // 867
|
|
method === 'insert' && args.push(generatedId); // 868
|
|
return self[validatedMethodName].apply(self, args); // 869
|
|
} else if (self._isInsecure()) { // 870
|
|
if (generatedId !== null) // 871
|
|
args[0]._id = generatedId; // 872
|
|
// In insecure mode, allow any mutation (with a simple selector). // 873
|
|
// XXX This is kind of bogus. Instead of blindly passing whatever // 874
|
|
// we get from the network to this function, we should actually // 875
|
|
// know the correct arguments for the function and pass just // 876
|
|
// them. For example, if you have an extraneous extra null // 877
|
|
// argument and this is Mongo on the server, the .wrapAsync'd // 878
|
|
// functions like update will get confused and pass the // 879
|
|
// "fut.resolver()" in the wrong slot, where _update will never // 880
|
|
// invoke it. Bam, broken DDP connection. Probably should just // 881
|
|
// take this whole method and write it three times, invoking // 882
|
|
// helpers for the common code. // 883
|
|
return self._collection[method].apply(self._collection, args); // 884
|
|
} else { // 885
|
|
// In secure mode, if we haven't called allow or deny, then nothing // 886
|
|
// is permitted. // 887
|
|
throw new Meteor.Error(403, "Access denied"); // 888
|
|
} // 889
|
|
} catch (e) { // 890
|
|
if (e.name === 'MongoError' || e.name === 'MinimongoError') { // 891
|
|
throw new Meteor.Error(409, e.toString()); // 892
|
|
} else { // 893
|
|
throw e; // 894
|
|
} // 895
|
|
} // 896
|
|
}; // 897
|
|
}); // 898
|
|
// Minimongo on the server gets no stubs; instead, by default // 899
|
|
// it wait()s until its result is ready, yielding. // 900
|
|
// This matches the behavior of macromongo on the server better. // 901
|
|
// XXX see #MeteorServerNull // 902
|
|
if (Meteor.isClient || self._connection === Meteor.server) // 903
|
|
self._connection.methods(m); // 904
|
|
} // 905
|
|
}; // 906
|
|
// 907
|
|
// 908
|
|
Mongo.Collection.prototype._updateFetch = function (fields) { // 909
|
|
var self = this; // 910
|
|
// 911
|
|
if (!self._validators.fetchAllFields) { // 912
|
|
if (fields) { // 913
|
|
self._validators.fetch = _.union(self._validators.fetch, fields); // 914
|
|
} else { // 915
|
|
self._validators.fetchAllFields = true; // 916
|
|
// clear fetch just to make sure we don't accidentally read it // 917
|
|
self._validators.fetch = null; // 918
|
|
} // 919
|
|
} // 920
|
|
}; // 921
|
|
// 922
|
|
Mongo.Collection.prototype._isInsecure = function () { // 923
|
|
var self = this; // 924
|
|
if (self._insecure === undefined) // 925
|
|
return !!Package.insecure; // 926
|
|
return self._insecure; // 927
|
|
}; // 928
|
|
// 929
|
|
var docToValidate = function (validator, doc, generatedId) { // 930
|
|
var ret = doc; // 931
|
|
if (validator.transform) { // 932
|
|
ret = EJSON.clone(doc); // 933
|
|
// If you set a server-side transform on your collection, then you don't get // 934
|
|
// to tell the difference between "client specified the ID" and "server // 935
|
|
// generated the ID", because transforms expect to get _id. If you want to // 936
|
|
// do that check, you can do it with a specific // 937
|
|
// `C.allow({insert: f, transform: null})` validator. // 938
|
|
if (generatedId !== null) { // 939
|
|
ret._id = generatedId; // 940
|
|
} // 941
|
|
ret = validator.transform(ret); // 942
|
|
} // 943
|
|
return ret; // 944
|
|
}; // 945
|
|
// 946
|
|
Mongo.Collection.prototype._validatedInsert = function (userId, doc, // 947
|
|
generatedId) { // 948
|
|
var self = this; // 949
|
|
// 950
|
|
// call user validators. // 951
|
|
// Any deny returns true means denied. // 952
|
|
if (_.any(self._validators.insert.deny, function(validator) { // 953
|
|
return validator(userId, docToValidate(validator, doc, generatedId)); // 954
|
|
})) { // 955
|
|
throw new Meteor.Error(403, "Access denied"); // 956
|
|
} // 957
|
|
// Any allow returns true means proceed. Throw error if they all fail. // 958
|
|
if (_.all(self._validators.insert.allow, function(validator) { // 959
|
|
return !validator(userId, docToValidate(validator, doc, generatedId)); // 960
|
|
})) { // 961
|
|
throw new Meteor.Error(403, "Access denied"); // 962
|
|
} // 963
|
|
// 964
|
|
// If we generated an ID above, insert it now: after the validation, but // 965
|
|
// before actually inserting. // 966
|
|
if (generatedId !== null) // 967
|
|
doc._id = generatedId; // 968
|
|
// 969
|
|
self._collection.insert.call(self._collection, doc); // 970
|
|
}; // 971
|
|
// 972
|
|
var transformDoc = function (validator, doc) { // 973
|
|
if (validator.transform) // 974
|
|
return validator.transform(doc); // 975
|
|
return doc; // 976
|
|
}; // 977
|
|
// 978
|
|
// Simulate a mongo `update` operation while validating that the access // 979
|
|
// control rules set by calls to `allow/deny` are satisfied. If all // 980
|
|
// pass, rewrite the mongo operation to use $in to set the list of // 981
|
|
// document ids to change ##ValidatedChange // 982
|
|
Mongo.Collection.prototype._validatedUpdate = function( // 983
|
|
userId, selector, mutator, options) { // 984
|
|
var self = this; // 985
|
|
// 986
|
|
check(mutator, Object); // 987
|
|
// 988
|
|
options = _.clone(options) || {}; // 989
|
|
// 990
|
|
if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) // 991
|
|
throw new Error("validated update should be of a single ID"); // 992
|
|
// 993
|
|
// We don't support upserts because they don't fit nicely into allow/deny // 994
|
|
// rules. // 995
|
|
if (options.upsert) // 996
|
|
throw new Meteor.Error(403, "Access denied. Upserts not " + // 997
|
|
"allowed in a restricted collection."); // 998
|
|
// 999
|
|
var noReplaceError = "Access denied. In a restricted collection you can only" + // 1000
|
|
" update documents, not replace them. Use a Mongo update operator, such " + // 1001
|
|
"as '$set'."; // 1002
|
|
// 1003
|
|
// compute modified fields // 1004
|
|
var fields = []; // 1005
|
|
if (_.isEmpty(mutator)) { // 1006
|
|
throw new Meteor.Error(403, noReplaceError); // 1007
|
|
} // 1008
|
|
_.each(mutator, function (params, op) { // 1009
|
|
if (op.charAt(0) !== '$') { // 1010
|
|
throw new Meteor.Error(403, noReplaceError); // 1011
|
|
} else if (!_.has(ALLOWED_UPDATE_OPERATIONS, op)) { // 1012
|
|
throw new Meteor.Error( // 1013
|
|
403, "Access denied. Operator " + op + " not allowed in a restricted collection."); // 1014
|
|
} else { // 1015
|
|
_.each(_.keys(params), function (field) { // 1016
|
|
// treat dotted fields as if they are replacing their // 1017
|
|
// top-level part // 1018
|
|
if (field.indexOf('.') !== -1) // 1019
|
|
field = field.substring(0, field.indexOf('.')); // 1020
|
|
// 1021
|
|
// record the field we are trying to change // 1022
|
|
if (!_.contains(fields, field)) // 1023
|
|
fields.push(field); // 1024
|
|
}); // 1025
|
|
} // 1026
|
|
}); // 1027
|
|
// 1028
|
|
var findOptions = {transform: null}; // 1029
|
|
if (!self._validators.fetchAllFields) { // 1030
|
|
findOptions.fields = {}; // 1031
|
|
_.each(self._validators.fetch, function(fieldName) { // 1032
|
|
findOptions.fields[fieldName] = 1; // 1033
|
|
}); // 1034
|
|
} // 1035
|
|
// 1036
|
|
var doc = self._collection.findOne(selector, findOptions); // 1037
|
|
if (!doc) // none satisfied! // 1038
|
|
return 0; // 1039
|
|
// 1040
|
|
// call user validators. // 1041
|
|
// Any deny returns true means denied. // 1042
|
|
if (_.any(self._validators.update.deny, function(validator) { // 1043
|
|
var factoriedDoc = transformDoc(validator, doc); // 1044
|
|
return validator(userId, // 1045
|
|
factoriedDoc, // 1046
|
|
fields, // 1047
|
|
mutator); // 1048
|
|
})) { // 1049
|
|
throw new Meteor.Error(403, "Access denied"); // 1050
|
|
} // 1051
|
|
// Any allow returns true means proceed. Throw error if they all fail. // 1052
|
|
if (_.all(self._validators.update.allow, function(validator) { // 1053
|
|
var factoriedDoc = transformDoc(validator, doc); // 1054
|
|
return !validator(userId, // 1055
|
|
factoriedDoc, // 1056
|
|
fields, // 1057
|
|
mutator); // 1058
|
|
})) { // 1059
|
|
throw new Meteor.Error(403, "Access denied"); // 1060
|
|
} // 1061
|
|
// 1062
|
|
options._forbidReplace = true; // 1063
|
|
// 1064
|
|
// Back when we supported arbitrary client-provided selectors, we actually // 1065
|
|
// rewrote the selector to include an _id clause before passing to Mongo to // 1066
|
|
// avoid races, but since selector is guaranteed to already just be an ID, we // 1067
|
|
// don't have to any more. // 1068
|
|
// 1069
|
|
return self._collection.update.call( // 1070
|
|
self._collection, selector, mutator, options); // 1071
|
|
}; // 1072
|
|
// 1073
|
|
// Only allow these operations in validated updates. Specifically // 1074
|
|
// whitelist operations, rather than blacklist, so new complex // 1075
|
|
// operations that are added aren't automatically allowed. A complex // 1076
|
|
// operation is one that does more than just modify its target // 1077
|
|
// field. For now this contains all update operations except '$rename'. // 1078
|
|
// http://docs.mongodb.org/manual/reference/operators/#update // 1079
|
|
var ALLOWED_UPDATE_OPERATIONS = { // 1080
|
|
$inc:1, $set:1, $unset:1, $addToSet:1, $pop:1, $pullAll:1, $pull:1, // 1081
|
|
$pushAll:1, $push:1, $bit:1 // 1082
|
|
}; // 1083
|
|
// 1084
|
|
// Simulate a mongo `remove` operation while validating access control // 1085
|
|
// rules. See #ValidatedChange // 1086
|
|
Mongo.Collection.prototype._validatedRemove = function(userId, selector) { // 1087
|
|
var self = this; // 1088
|
|
// 1089
|
|
var findOptions = {transform: null}; // 1090
|
|
if (!self._validators.fetchAllFields) { // 1091
|
|
findOptions.fields = {}; // 1092
|
|
_.each(self._validators.fetch, function(fieldName) { // 1093
|
|
findOptions.fields[fieldName] = 1; // 1094
|
|
}); // 1095
|
|
} // 1096
|
|
// 1097
|
|
var doc = self._collection.findOne(selector, findOptions); // 1098
|
|
if (!doc) // 1099
|
|
return 0; // 1100
|
|
// 1101
|
|
// call user validators. // 1102
|
|
// Any deny returns true means denied. // 1103
|
|
if (_.any(self._validators.remove.deny, function(validator) { // 1104
|
|
return validator(userId, transformDoc(validator, doc)); // 1105
|
|
})) { // 1106
|
|
throw new Meteor.Error(403, "Access denied"); // 1107
|
|
} // 1108
|
|
// Any allow returns true means proceed. Throw error if they all fail. // 1109
|
|
if (_.all(self._validators.remove.allow, function(validator) { // 1110
|
|
return !validator(userId, transformDoc(validator, doc)); // 1111
|
|
})) { // 1112
|
|
throw new Meteor.Error(403, "Access denied"); // 1113
|
|
} // 1114
|
|
// 1115
|
|
// Back when we supported arbitrary client-provided selectors, we actually // 1116
|
|
// rewrote the selector to {_id: {$in: [ids that we found]}} before passing to // 1117
|
|
// Mongo to avoid races, but since selector is guaranteed to already just be // 1118
|
|
// an ID, we don't have to any more. // 1119
|
|
// 1120
|
|
return self._collection.remove.call(self._collection, selector); // 1121
|
|
}; // 1122
|
|
// 1123
|
|
/** // 1124
|
|
* @deprecated in 0.9.1 // 1125
|
|
*/ // 1126
|
|
Meteor.Collection = Mongo.Collection; // 1127
|
|
// 1128
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
/* Exports */
|
|
if (typeof Package === 'undefined') Package = {};
|
|
Package.mongo = {
|
|
Mongo: Mongo
|
|
};
|
|
|
|
})();
|