ethstats-server/web-app/.meteor/local/build/programs/web.browser/packages/mongo.js

1233 lines
146 KiB
JavaScript
Raw Normal View History

2015-08-14 19:22:53 +02:00
//////////////////////////////////////////////////////////////////////////
// //
// 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
};
})();