mongoose-diff-tracking
Version:
Manage Mongo Collection diff History and versions
1,776 lines (1,563 loc) • 89.6 kB
JavaScript
/*!
* Module dependencies.
*/
var Document = require('./document');
var MongooseError = require('./error');
var VersionError = MongooseError.VersionError;
var DivergentArrayError = MongooseError.DivergentArrayError;
var Query = require('./query');
var Aggregate = require('./aggregate');
var Schema = require('./schema');
var utils = require('./utils');
var hasOwnProperty = utils.object.hasOwnProperty;
var isMongooseObject = utils.isMongooseObject;
var EventEmitter = require('events').EventEmitter;
var Promise = require('./promise');
var util = require('util');
var tick = utils.tick;
var async = require('async');
var PromiseProvider = require('./promise_provider');
var VERSION_WHERE = 1,
VERSION_INC = 2,
VERSION_ALL = VERSION_WHERE | VERSION_INC;
/**
* Model constructor
*
* Provides the interface to MongoDB collections as well as creates document instances.
*
* @param {Object} doc values with which to create the document
* @inherits Document
* @event `error`: If listening to this event, it is emitted when a document was saved without passing a callback and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model.
* @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event.
* @event `index-single-start`: Emitted when an individual index starts within `Model#ensureIndexes`. The fields and options being used to build the index are also passed with the event.
* @event `index-single-done`: Emitted when an individual index finishes within `Model#ensureIndexes`. If an error occurred it is passed with the event. The fields, options, and index name are also passed.
* @api public
*/
function Model(doc, fields, skipId) {
Document.call(this, doc, fields, skipId);
}
/*!
* Inherits from Document.
*
* All Model.prototype features are available on
* top level (non-sub) documents.
*/
Model.prototype.__proto__ = Document.prototype;
/**
* Connection the model uses.
*
* @api public
* @property db
*/
Model.prototype.db;
/**
* Collection the model uses.
*
* @api public
* @property collection
*/
Model.prototype.collection;
/**
* The name of the model
*
* @api public
* @property modelName
*/
Model.prototype.modelName;
/**
* If this is a discriminator model, `baseModelName` is the name of
* the base model.
*
* @api public
* @property baseModelName
*/
Model.prototype.baseModelName;
Model.prototype.$__handleSave = function(options, callback) {
var _this = this;
if (!options.safe && this.schema.options.safe) {
options.safe = this.schema.options.safe;
}
if (typeof options.safe === 'boolean') {
options.safe = null;
}
if (this.isNew) {
// send entire doc
var toObjectOptions = {};
if (this.schema.options.toObject &&
this.schema.options.toObject.retainKeyOrder) {
toObjectOptions.retainKeyOrder = true;
}
toObjectOptions.depopulate = 1;
toObjectOptions._skipDepopulateTopLevel = true;
toObjectOptions.transform = false;
var obj = this.toObject(toObjectOptions);
if (!utils.object.hasOwnProperty(obj || {}, '_id')) {
// documents must have an _id else mongoose won't know
// what to update later if more changes are made. the user
// wouldn't know what _id was generated by mongodb either
// nor would the ObjectId generated my mongodb necessarily
// match the schema definition.
setTimeout(function() {
callback(new Error('document must have an _id before saving'));
}, 0);
return;
}
this.$__version(true, obj);
this.collection.insert(obj, options.safe, function(err, ret) {
if (err) {
_this.isNew = true;
_this.emit('isNew', true);
callback(err);
return;
}
callback(null, ret);
});
this.$__reset();
this.isNew = false;
this.emit('isNew', false);
// Make it possible to retry the insert
this.$__.inserting = true;
} else {
// Make sure we don't treat it as a new object on error,
// since it already exists
this.$__.inserting = false;
var delta = this.$__delta();
if (delta) {
if (delta instanceof Error) {
callback(delta);
return;
}
var where = this.$__where(delta[0]);
if (where instanceof Error) {
callback(where);
return;
}
this.collection.update(where, delta[1], options.safe, function(err, ret) {
if (err) {
callback(err);
return;
}
callback(null, ret);
});
} else {
this.$__reset();
callback();
return;
}
this.emit('isNew', false);
}
};
/*!
* ignore
*/
Model.prototype.$__save = function(options, callback) {
var _this = this;
_this.$__handleSave(options, function(error, result) {
if (error) {
return callback(error);
}
_this.$__reset();
_this.$__storeShard();
var numAffected = 0;
if (result) {
if (Array.isArray(result)) {
numAffected = result.length;
} else if (result.result && result.result.n !== undefined) {
numAffected = result.result.n;
} else if (result.result && result.result.nModified !== undefined) {
numAffected = result.result.nModified;
} else {
numAffected = result;
}
}
// was this an update that required a version bump?
if (_this.$__.version && !_this.$__.inserting) {
var doIncrement = VERSION_INC === (VERSION_INC & _this.$__.version);
_this.$__.version = undefined;
if (numAffected <= 0) {
// the update failed. pass an error back
var err = new VersionError();
return callback(err);
}
// increment version if was successful
if (doIncrement) {
var key = _this.schema.options.versionKey;
var version = _this.getValue(key) | 0;
_this.setValue(key, version + 1);
}
}
_this.emit('save', _this, numAffected);
callback(null, _this, numAffected);
});
};
/**
* Saves this document.
*
* ####Example:
*
* product.sold = Date.now();
* product.save(function (err, product, numAffected) {
* if (err) ..
* })
*
* The callback will receive three parameters
*
* 1. `err` if an error occurred
* 2. `product` which is the saved `product`
* 3. `numAffected` will be 1 when the document was successfully persisted to MongoDB, otherwise 0. Unless you tweak mongoose's internals, you don't need to worry about checking this parameter for errors - checking `err` is sufficient to make sure your document was properly saved.
*
* As an extra measure of flow control, save will return a Promise.
* ####Example:
* product.save().then(function(product) {
* ...
* });
*
* For legacy reasons, mongoose stores object keys in reverse order on initial
* save. That is, `{ a: 1, b: 2 }` will be saved as `{ b: 2, a: 1 }` in
* MongoDB. To override this behavior, set
* [the `toObject.retainKeyOrder` option](http://mongoosejs.com/docs/api.html#document_Document-toObject)
* to true on your schema.
*
* @param {Object} [options] options optional options
* @param {Object} [options.safe] overrides [schema's safe option](http://mongoosejs.com//docs/guide.html#safe)
* @param {Boolean} [options.validateBeforeSave] set to false to save without validating.
* @param {Function} [fn] optional callback
* @return {Promise} Promise
* @api public
* @see middleware http://mongoosejs.com/docs/middleware.html
*/
Model.prototype.save = function(options, fn) {
if (typeof options === 'function') {
fn = options;
options = undefined;
}
if (!options) {
options = {};
}
if (options.__noPromise) {
return this.$__save(options, fn);
}
var _this = this;
var Promise = PromiseProvider.get();
return new Promise.ES6(function(resolve, reject) {
_this.$__save(options, function(error, doc, numAffected) {
if (error) {
fn && fn(error);
reject(error);
return;
}
fn && fn(null, doc, numAffected);
resolve(doc, numAffected);
});
});
};
/**
* Determines whether versioning should be skipped for the given path
*
* @param {Document} self
* @param {String} path
* @return {Boolean} true if versioning should be skipped for the given path
*/
function shouldSkipVersioning(self, path) {
var skipVersioning = self.schema.options.skipVersioning;
if (!skipVersioning) return false;
// Remove any array indexes from the path
path = path.replace(/\.\d+\./, '.');
return skipVersioning[path];
}
/*!
* Apply the operation to the delta (update) clause as
* well as track versioning for our where clause.
*
* @param {Document} self
* @param {Object} where
* @param {Object} delta
* @param {Object} data
* @param {Mixed} val
* @param {String} [operation]
*/
function operand(self, where, delta, data, val, op) {
// delta
op || (op = '$set');
if (!delta[op]) delta[op] = {};
delta[op][data.path] = val;
// disabled versioning?
if (self.schema.options.versionKey === false) return;
// path excluded from versioning?
if (shouldSkipVersioning(self, data.path)) return;
// already marked for versioning?
if (VERSION_ALL === (VERSION_ALL & self.$__.version)) return;
switch (op) {
case '$set':
case '$unset':
case '$pop':
case '$pull':
case '$pullAll':
case '$push':
case '$pushAll':
case '$addToSet':
break;
default:
// nothing to do
return;
}
// ensure updates sent with positional notation are
// editing the correct array element.
// only increment the version if an array position changes.
// modifying elements of an array is ok if position does not change.
if (op === '$push' || op === '$pushAll' || op === '$addToSet') {
self.$__.version = VERSION_INC;
} else if (/^\$p/.test(op)) {
// potentially changing array positions
self.increment();
} else if (Array.isArray(val)) {
// $set an array
self.increment();
} else if (/\.\d+\.|\.\d+$/.test(data.path)) {
// now handling $set, $unset
// subpath of array
self.$__.version = VERSION_WHERE;
}
}
/*!
* Compiles an update and where clause for a `val` with _atomics.
*
* @param {Document} self
* @param {Object} where
* @param {Object} delta
* @param {Object} data
* @param {Array} value
*/
function handleAtomics(self, where, delta, data, value) {
if (delta.$set && delta.$set[data.path]) {
// $set has precedence over other atomics
return;
}
if (typeof value.$__getAtomics === 'function') {
value.$__getAtomics().forEach(function(atomic) {
var op = atomic[0];
var val = atomic[1];
operand(self, where, delta, data, val, op);
});
return;
}
// legacy support for plugins
var atomics = value._atomics,
ops = Object.keys(atomics),
i = ops.length,
val,
op;
if (i === 0) {
// $set
if (isMongooseObject(value)) {
value = value.toObject({depopulate: 1});
} else if (value.valueOf) {
value = value.valueOf();
}
return operand(self, where, delta, data, value);
}
function iter(mem) {
return isMongooseObject(mem)
? mem.toObject({depopulate: 1})
: mem;
}
while (i--) {
op = ops[i];
val = atomics[op];
if (isMongooseObject(val)) {
val = val.toObject({depopulate: 1});
} else if (Array.isArray(val)) {
val = val.map(iter);
} else if (val.valueOf) {
val = val.valueOf();
}
if (op === '$addToSet') {
val = {$each: val};
}
operand(self, where, delta, data, val, op);
}
}
/**
* Produces a special query document of the modified properties used in updates.
*
* @api private
* @method $__delta
* @memberOf Model
*/
Model.prototype.$__delta = function() {
var dirty = this.$__dirty();
if (!dirty.length && VERSION_ALL !== this.$__.version) return;
var where = {},
delta = {},
len = dirty.length,
divergent = [],
d = 0;
where._id = this._doc._id;
for (; d < len; ++d) {
var data = dirty[d];
var value = data.value;
var match = checkDivergentArray(this, data.path, value);
if (match) {
divergent.push(match);
continue;
}
if (divergent.length) continue;
if (undefined === value) {
operand(this, where, delta, data, 1, '$unset');
} else if (value === null) {
operand(this, where, delta, data, null);
} else if (value._path && value._atomics) {
// arrays and other custom types (support plugins etc)
handleAtomics(this, where, delta, data, value);
} else if (value._path && Buffer.isBuffer(value)) {
// MongooseBuffer
value = value.toObject();
operand(this, where, delta, data, value);
} else {
value = utils.clone(value, {depopulate: 1});
operand(this, where, delta, data, value);
}
}
if (divergent.length) {
return new DivergentArrayError(divergent);
}
if (this.$__.version) {
this.$__version(where, delta);
}
return [where, delta];
};
/*!
* Determine if array was populated with some form of filter and is now
* being updated in a manner which could overwrite data unintentionally.
*
* @see https://github.com/Automattic/mongoose/issues/1334
* @param {Document} doc
* @param {String} path
* @return {String|undefined}
*/
function checkDivergentArray(doc, path, array) {
// see if we populated this path
var pop = doc.populated(path, true);
if (!pop && doc.$__.selected) {
// If any array was selected using an $elemMatch projection, we deny the update.
// NOTE: MongoDB only supports projected $elemMatch on top level array.
var top = path.split('.')[0];
if ((doc.$__.selected[top] && doc.$__.selected[top].$elemMatch) ||
doc.$__.selected[top + '.$']) {
return top;
}
}
if (!(pop && array && array.isMongooseArray)) return;
// If the array was populated using options that prevented all
// documents from being returned (match, skip, limit) or they
// deselected the _id field, $pop and $set of the array are
// not safe operations. If _id was deselected, we do not know
// how to remove elements. $pop will pop off the _id from the end
// of the array in the db which is not guaranteed to be the
// same as the last element we have here. $set of the entire array
// would be similarily destructive as we never received all
// elements of the array and potentially would overwrite data.
var check = pop.options.match ||
pop.options.options && hasOwnProperty(pop.options.options, 'limit') || // 0 is not permitted
pop.options.options && pop.options.options.skip || // 0 is permitted
pop.options.select && // deselected _id?
(pop.options.select._id === 0 ||
/\s?-_id\s?/.test(pop.options.select));
if (check) {
var atomics = array._atomics;
if (Object.keys(atomics).length === 0 || atomics.$set || atomics.$pop) {
return path;
}
}
}
/**
* Appends versioning to the where and update clauses.
*
* @api private
* @method $__version
* @memberOf Model
*/
Model.prototype.$__version = function(where, delta) {
var key = this.schema.options.versionKey;
if (where === true) {
// this is an insert
if (key) this.setValue(key, delta[key] = 0);
return;
}
// updates
// only apply versioning if our versionKey was selected. else
// there is no way to select the correct version. we could fail
// fast here and force them to include the versionKey but
// thats a bit intrusive. can we do this automatically?
if (!this.isSelected(key)) {
return;
}
// $push $addToSet don't need the where clause set
if (VERSION_WHERE === (VERSION_WHERE & this.$__.version)) {
where[key] = this.getValue(key);
}
if (VERSION_INC === (VERSION_INC & this.$__.version)) {
if (!delta.$set || typeof delta.$set[key] === 'undefined') {
delta.$inc || (delta.$inc = {});
delta.$inc[key] = 1;
}
}
};
/**
* Signal that we desire an increment of this documents version.
*
* ####Example:
*
* Model.findById(id, function (err, doc) {
* doc.increment();
* doc.save(function (err) { .. })
* })
*
* @see versionKeys http://mongoosejs.com/docs/guide.html#versionKey
* @api public
*/
Model.prototype.increment = function increment() {
this.$__.version = VERSION_ALL;
return this;
};
/**
* Returns a query object which applies shardkeys if they exist.
*
* @api private
* @method $__where
* @memberOf Model
*/
Model.prototype.$__where = function _where(where) {
where || (where = {});
var paths,
len;
if (!where._id) {
where._id = this._doc._id;
}
if (this.$__.shardval) {
paths = Object.keys(this.$__.shardval);
len = paths.length;
for (var i = 0; i < len; ++i) {
where[paths[i]] = this.$__.shardval[paths[i]];
}
}
if (this._doc._id == null) {
return new Error('No _id found on document!');
}
return where;
};
/**
* Removes this document from the db.
*
* ####Example:
* product.remove(function (err, product) {
* if (err) return handleError(err);
* Product.findById(product._id, function (err, product) {
* console.log(product) // null
* })
* })
*
*
* As an extra measure of flow control, remove will return a Promise (bound to `fn` if passed) so it could be chained, or hooked to recive errors
*
* ####Example:
* product.remove().then(function (product) {
* ...
* }).onRejected(function (err) {
* assert.ok(err)
* })
*
* @param {function(err,product)} [fn] optional callback
* @return {Promise} Promise
* @api public
*/
Model.prototype.remove = function remove(options, fn) {
if (typeof options === 'function') {
fn = options;
options = undefined;
}
if (!options) {
options = {};
}
if (this.$__.removing) {
if (fn) {
this.$__.removing.then(
function(res) { fn(null, res); },
function(err) { fn(err); });
}
return this;
}
var _this = this;
var Promise = PromiseProvider.get();
this.$__.removing = new Promise.ES6(function(resolve, reject) {
var where = _this.$__where();
if (where instanceof Error) {
reject(where);
fn && fn(where);
return;
}
if (!options.safe && _this.schema.options.safe) {
options.safe = _this.schema.options.safe;
}
_this.collection.remove(where, options, function(err) {
if (!err) {
_this.emit('remove', _this);
resolve(_this);
fn && fn(null, _this);
return;
}
reject(err);
fn && fn(err);
});
});
return this.$__.removing;
};
/**
* Returns another Model instance.
*
* ####Example:
*
* var doc = new Tank;
* doc.model('User').findById(id, callback);
*
* @param {String} name model name
* @api public
*/
Model.prototype.model = function model(name) {
return this.db.model(name);
};
/**
* Adds a discriminator type.
*
* ####Example:
*
* function BaseSchema() {
* Schema.apply(this, arguments);
*
* this.add({
* name: String,
* createdAt: Date
* });
* }
* util.inherits(BaseSchema, Schema);
*
* var PersonSchema = new BaseSchema();
* var BossSchema = new BaseSchema({ department: String });
*
* var Person = mongoose.model('Person', PersonSchema);
* var Boss = Person.discriminator('Boss', BossSchema);
*
* @param {String} name discriminator model name
* @param {Schema} schema discriminator model schema
* @api public
*/
Model.discriminator = function discriminator(name, schema) {
if (!(schema && schema.instanceOfSchema)) {
throw new Error('You must pass a valid discriminator Schema');
}
if (this.schema.discriminatorMapping && !this.schema.discriminatorMapping.isRoot) {
throw new Error('Discriminator "' + name +
'" can only be a discriminator of the root model');
}
var key = this.schema.options.discriminatorKey;
if (schema.path(key)) {
throw new Error('Discriminator "' + name +
'" cannot have field with name "' + key + '"');
}
function clean(a, b) {
a = utils.clone(a);
b = utils.clone(b);
delete a.toJSON;
delete a.toObject;
delete b.toJSON;
delete b.toObject;
delete a._id;
delete b._id;
if (!utils.deepEqual(a, b)) {
throw new Error('Discriminator options are not customizable ' +
'(except toJSON, toObject, _id)');
}
}
function merge(schema, baseSchema) {
utils.merge(schema, baseSchema);
var obj = {};
obj[key] = {type: String, default: name};
schema.add(obj);
schema.discriminatorMapping = {key: key, value: name, isRoot: false};
if (baseSchema.options.collection) {
schema.options.collection = baseSchema.options.collection;
}
// throws error if options are invalid
clean(schema.options, baseSchema.options);
var toJSON = schema.options.toJSON;
var toObject = schema.options.toObject;
var _id = schema.options._id;
schema.options = utils.clone(baseSchema.options);
if (toJSON) schema.options.toJSON = toJSON;
if (toObject) schema.options.toObject = toObject;
if (typeof _id !== 'undefined') {
schema.options._id = _id;
}
schema.callQueue = baseSchema.callQueue.concat(schema.callQueue.slice(schema._defaultMiddleware.length));
schema._requiredpaths = undefined; // reset just in case Schema#requiredPaths() was called on either schema
}
// merges base schema into new discriminator schema and sets new type field.
merge(schema, this.schema);
if (!this.discriminators) {
this.discriminators = {};
}
if (!this.schema.discriminatorMapping) {
this.schema.discriminatorMapping = {key: key, value: null, isRoot: true};
}
if (this.discriminators[name]) {
throw new Error('Discriminator with name "' + name + '" already exists');
}
this.discriminators[name] = this.db.model(name, schema, this.collection.name);
this.discriminators[name].prototype.__proto__ = this.prototype;
Object.defineProperty(this.discriminators[name], 'baseModelName', {
value: this.modelName,
configurable: true,
writable: false
});
// apply methods and statics
applyMethods(this.discriminators[name], schema);
applyStatics(this.discriminators[name], schema);
return this.discriminators[name];
};
// Model (class) features
/*!
* Give the constructor the ability to emit events.
*/
for (var i in EventEmitter.prototype) {
Model[i] = EventEmitter.prototype[i];
}
/**
* Called when the model compiles.
*
* @api private
*/
Model.init = function init() {
if ((this.schema.options.autoIndex) ||
(this.schema.options.autoIndex === null && this.db.config.autoIndex)) {
this.ensureIndexes({ __noPromise: true });
}
this.schema.emit('init', this);
};
/**
* Sends `ensureIndex` commands to mongo for each index declared in the schema.
*
* ####Example:
*
* Event.ensureIndexes(function (err) {
* if (err) return handleError(err);
* });
*
* After completion, an `index` event is emitted on this `Model` passing an error if one occurred.
*
* ####Example:
*
* var eventSchema = new Schema({ thing: { type: 'string', unique: true }})
* var Event = mongoose.model('Event', eventSchema);
*
* Event.on('index', function (err) {
* if (err) console.error(err); // error occurred during index creation
* })
*
* _NOTE: It is not recommended that you run this in production. Index creation may impact database performance depending on your load. Use with caution._
*
* The `ensureIndex` commands are not sent in parallel. This is to avoid the `MongoError: cannot add index with a background operation in progress` error. See [this ticket](https://github.com/Automattic/mongoose/issues/1365) for more information.
*
* @param {Object} [options] internal options
* @param {Function} [cb] optional callback
* @return {Promise}
* @api public
*/
Model.ensureIndexes = function ensureIndexes(options, cb) {
if (typeof options === 'function') {
cb = options;
options = null;
}
if (options && options.__noPromise) {
_ensureIndexes(this, cb);
return;
}
var _this = this;
var Promise = PromiseProvider.get();
return new Promise.ES6(function(resolve, reject) {
_ensureIndexes(_this, function(error) {
if (error) {
cb && cb(error);
reject(error);
}
cb && cb();
resolve();
});
});
};
function _ensureIndexes(model, callback) {
var indexes = model.schema.indexes();
if (!indexes.length) {
process.nextTick(function() {
callback && callback();
});
return;
}
// Indexes are created one-by-one to support how MongoDB < 2.4 deals
// with background indexes.
var done = function(err) {
if (err && model.schema.options.emitIndexErrors) {
model.emit('error', err);
}
model.emit('index', err);
callback && callback(err);
};
var indexSingleDone = function(err, fields, options, name) {
model.emit('index-single-done', err, fields, options, name);
};
var indexSingleStart = function(fields, options) {
model.emit('index-single-start', fields, options);
};
var create = function() {
var index = indexes.shift();
if (!index) return done();
var indexFields = index[0];
var options = index[1];
_handleSafe(options);
indexSingleStart(indexFields, options);
model.collection.ensureIndex(indexFields, options, tick(function(err, name) {
indexSingleDone(err, indexFields, options, name);
if (err) {
return done(err);
}
create();
}));
};
create();
}
function _handleSafe(options) {
if (options.safe) {
if (typeof options.safe === 'boolean') {
options.w = options.safe;
delete options.safe;
}
if (typeof options.safe === 'object') {
options.w = options.safe.w;
options.j = options.safe.j;
options.wtimeout = options.safe.wtimeout;
delete options.safe;
}
}
}
/**
* Schema the model uses.
*
* @property schema
* @receiver Model
* @api public
*/
Model.schema;
/*!
* Connection instance the model uses.
*
* @property db
* @receiver Model
* @api public
*/
Model.db;
/*!
* Collection the model uses.
*
* @property collection
* @receiver Model
* @api public
*/
Model.collection;
/**
* Base Mongoose instance the model uses.
*
* @property base
* @receiver Model
* @api public
*/
Model.base;
/**
* Registered discriminators for this model.
*
* @property discriminators
* @receiver Model
* @api public
*/
Model.discriminators;
/**
* Removes documents from the collection.
*
* ####Example:
*
* Comment.remove({ title: 'baby born from alien father' }, function (err) {
*
* });
*
* ####Note:
*
* To remove documents without waiting for a response from MongoDB, do not pass a `callback`, then call `exec` on the returned [Query](#query-js):
*
* var query = Comment.remove({ _id: id });
* query.exec();
*
* ####Note:
*
* This method sends a remove command directly to MongoDB, no Mongoose documents are involved. Because no Mongoose documents are involved, _no middleware (hooks) are executed_.
*
* @param {Object} conditions
* @param {Function} [callback]
* @return {Promise} Promise
* @api public
*/
Model.remove = function remove(conditions, callback) {
if (typeof conditions === 'function') {
callback = conditions;
conditions = {};
}
// get the mongodb collection object
var mq = new Query(conditions, {}, this, this.collection);
return mq.remove(callback);
};
/**
* Finds documents
*
* The `conditions` are cast to their respective SchemaTypes before the command is sent.
*
* ####Examples:
*
* // named john and at least 18
* MyModel.find({ name: 'john', age: { $gte: 18 }});
*
* // executes immediately, passing results to callback
* MyModel.find({ name: 'john', age: { $gte: 18 }}, function (err, docs) {});
*
* // name LIKE john and only selecting the "name" and "friends" fields, executing immediately
* MyModel.find({ name: /john/i }, 'name friends', function (err, docs) { })
*
* // passing options
* MyModel.find({ name: /john/i }, null, { skip: 10 })
*
* // passing options and executing immediately
* MyModel.find({ name: /john/i }, null, { skip: 10 }, function (err, docs) {});
*
* // executing a query explicitly
* var query = MyModel.find({ name: /john/i }, null, { skip: 10 })
* query.exec(function (err, docs) {});
*
* // using the promise returned from executing a query
* var query = MyModel.find({ name: /john/i }, null, { skip: 10 });
* var promise = query.exec();
* promise.addBack(function (err, docs) {});
*
* @param {Object} conditions
* @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo)
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @see promise #promise-js
* @api public
*/
Model.find = function find(conditions, projection, options, callback) {
if (typeof conditions === 'function') {
callback = conditions;
conditions = {};
projection = null;
options = null;
} else if (typeof projection === 'function') {
callback = projection;
projection = null;
options = null;
} else if (typeof options === 'function') {
callback = options;
options = null;
}
// get the raw mongodb collection object
var mq = new Query({}, {}, this, this.collection);
mq.select(projection);
mq.setOptions(options);
if (this.schema.discriminatorMapping && mq.selectedInclusively()) {
mq.select(this.schema.options.discriminatorKey);
}
return mq.find(conditions, callback);
};
/**
* Finds a single document by its _id field. `findById(id)` is almost*
* equivalent to `findOne({ _id: id })`.
*
* The `id` is cast based on the Schema before sending the command.
*
* Note: `findById()` triggers `findOne` hooks.
*
* * Except for how it treats `undefined`. Because the MongoDB driver
* deletes keys that have value `undefined`, `findById(undefined)` gets
* translated to `findById({ _id: null })`.
*
* ####Example:
*
* // find adventure by id and execute immediately
* Adventure.findById(id, function (err, adventure) {});
*
* // same as above
* Adventure.findById(id).exec(callback);
*
* // select only the adventures name and length
* Adventure.findById(id, 'name length', function (err, adventure) {});
*
* // same as above
* Adventure.findById(id, 'name length').exec(callback);
*
* // include all properties except for `length`
* Adventure.findById(id, '-length').exec(function (err, adventure) {});
*
* // passing options (in this case return the raw js objects, not mongoose documents by passing `lean`
* Adventure.findById(id, 'name', { lean: true }, function (err, doc) {});
*
* // same as above
* Adventure.findById(id, 'name').lean().exec(function (err, doc) {});
*
* @param {Object|String|Number} id value of `_id` to query by
* @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo)
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @see lean queries #query_Query-lean
* @api public
*/
Model.findById = function findById(id, projection, options, callback) {
if (typeof id === 'undefined') {
id = null;
}
return this.findOne({_id: id}, projection, options, callback);
};
/**
* Finds one document.
*
* The `conditions` are cast to their respective SchemaTypes before the command is sent.
*
* ####Example:
*
* // find one iphone adventures - iphone adventures??
* Adventure.findOne({ type: 'iphone' }, function (err, adventure) {});
*
* // same as above
* Adventure.findOne({ type: 'iphone' }).exec(function (err, adventure) {});
*
* // select only the adventures name
* Adventure.findOne({ type: 'iphone' }, 'name', function (err, adventure) {});
*
* // same as above
* Adventure.findOne({ type: 'iphone' }, 'name').exec(function (err, adventure) {});
*
* // specify options, in this case lean
* Adventure.findOne({ type: 'iphone' }, 'name', { lean: true }, callback);
*
* // same as above
* Adventure.findOne({ type: 'iphone' }, 'name', { lean: true }).exec(callback);
*
* // chaining findOne queries (same as above)
* Adventure.findOne({ type: 'iphone' }).select('name').lean().exec(callback);
*
* @param {Object} [conditions]
* @param {Object} [projection] optional fields to return (http://bit.ly/1HotzBo)
* @param {Object} [options] optional
* @param {Function} [callback]
* @return {Query}
* @see field selection #query_Query-select
* @see lean queries #query_Query-lean
* @api public
*/
Model.findOne = function findOne(conditions, projection, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
} else if (typeof projection === 'function') {
callback = projection;
projection = null;
options = null;
} else if (typeof conditions === 'function') {
callback = conditions;
conditions = {};
projection = null;
options = null;
}
// get the mongodb collection object
var mq = new Query({}, {}, this, this.collection);
mq.select(projection);
mq.setOptions(options);
if (this.schema.discriminatorMapping && mq.selectedInclusively()) {
mq.select(this.schema.options.discriminatorKey);
}
return mq.findOne(conditions, callback);
};
/**
* Counts number of matching documents in a database collection.
*
* ####Example:
*
* Adventure.count({ type: 'jungle' }, function (err, count) {
* if (err) ..
* console.log('there are %d jungle adventures', count);
* });
*
* @param {Object} conditions
* @param {Function} [callback]
* @return {Query}
* @api public
*/
Model.count = function count(conditions, callback) {
if (typeof conditions === 'function') {
callback = conditions;
conditions = {};
}
// get the mongodb collection object
var mq = new Query({}, {}, this, this.collection);
return mq.count(conditions, callback);
};
/**
* Creates a Query for a `distinct` operation.
*
* Passing a `callback` immediately executes the query.
*
* ####Example
*
* Link.distinct('url', { clicks: {$gt: 100}}, function (err, result) {
* if (err) return handleError(err);
*
* assert(Array.isArray(result));
* console.log('unique urls with more than 100 clicks', result);
* })
*
* var query = Link.distinct('url');
* query.exec(callback);
*
* @param {String} field
* @param {Object} [conditions] optional
* @param {Function} [callback]
* @return {Query}
* @api public
*/
Model.distinct = function distinct(field, conditions, callback) {
// get the mongodb collection object
var mq = new Query({}, {}, this, this.collection);
if (typeof conditions === 'function') {
callback = conditions;
conditions = {};
}
return mq.distinct(field, conditions, callback);
};
/**
* Creates a Query, applies the passed conditions, and returns the Query.
*
* For example, instead of writing:
*
* User.find({age: {$gte: 21, $lte: 65}}, callback);
*
* we can instead write:
*
* User.where('age').gte(21).lte(65).exec(callback);
*
* Since the Query class also supports `where` you can continue chaining
*
* User
* .where('age').gte(21).lte(65)
* .where('name', /^b/i)
* ... etc
*
* @param {String} path
* @param {Object} [val] optional value
* @return {Query}
* @api public
*/
Model.where = function where(path, val) {
void val; // eslint
// get the mongodb collection object
var mq = new Query({}, {}, this, this.collection).find({});
return mq.where.apply(mq, arguments);
};
/**
* Creates a `Query` and specifies a `$where` condition.
*
* Sometimes you need to query for things in mongodb using a JavaScript expression. You can do so via `find({ $where: javascript })`, or you can use the mongoose shortcut method $where via a Query chain or from your mongoose Model.
*
* Blog.$where('this.username.indexOf("val") !== -1').exec(function (err, docs) {});
*
* @param {String|Function} argument is a javascript string or anonymous function
* @method $where
* @memberOf Model
* @return {Query}
* @see Query.$where #query_Query-%24where
* @api public
*/
Model.$where = function $where() {
var mq = new Query({}, {}, this, this.collection).find({});
return mq.$where.apply(mq, arguments);
};
/**
* Issues a mongodb findAndModify update command.
*
* Finds a matching document, updates it according to the `update` arg, passing any `options`, and returns the found document (if any) to the callback. The query executes immediately if `callback` is passed else a Query object is returned.
*
* ####Options:
*
* - `new`: bool - if true, return the modified document rather than the original. defaults to false (changed in 4.0)
* - `upsert`: bool - creates the object if it doesn't exist. defaults to false.
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `select`: sets the document fields to return
*
* ####Examples:
*
* A.findOneAndUpdate(conditions, update, options, callback) // executes
* A.findOneAndUpdate(conditions, update, options) // returns Query
* A.findOneAndUpdate(conditions, update, callback) // executes
* A.findOneAndUpdate(conditions, update) // returns Query
* A.findOneAndUpdate() // returns Query
*
* ####Note:
*
* All top level update keys which are not `atomic` operation names are treated as set operations:
*
* ####Example:
*
* var query = { name: 'borne' };
* Model.findOneAndUpdate(query, { name: 'jason borne' }, options, callback)
*
* // is sent as
* Model.findOneAndUpdate(query, { $set: { name: 'jason borne' }}, options, callback)
*
* This helps prevent accidentally overwriting your document with `{ name: 'jason borne' }`.
*
* ####Note:
*
* Values are cast to their appropriate types when using the findAndModify helpers.
* However, the below are never executed.
*
* - defaults
* - setters
*
* `findAndModify` helpers support limited defaults and validation. You can
* enable these by setting the `setDefaultsOnInsert` and `runValidators` options,
* respectively.
*
* If you need full-fledged validation, use the traditional approach of first
* retrieving the document.
*
* Model.findById(id, function (err, doc) {
* if (err) ..
* doc.name = 'jason borne';
* doc.save(callback);
* });
*
* @param {Object} [conditions]
* @param {Object} [update]
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @api public
*/
Model.findOneAndUpdate = function(conditions, update, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
} else if (arguments.length === 1) {
if (typeof conditions === 'function') {
var msg = 'Model.findOneAndUpdate(): First argument must not be a function.\n\n'
+ ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options, callback)\n'
+ ' ' + this.modelName + '.findOneAndUpdate(conditions, update, options)\n'
+ ' ' + this.modelName + '.findOneAndUpdate(conditions, update)\n'
+ ' ' + this.modelName + '.findOneAndUpdate(update)\n'
+ ' ' + this.modelName + '.findOneAndUpdate()\n';
throw new TypeError(msg);
}
update = conditions;
conditions = undefined;
}
var fields;
if (options && options.fields) {
fields = options.fields;
options.fields = undefined;
}
update = utils.clone(update, {depopulate: 1});
if (this.schema.options.versionKey && options && options.upsert) {
if (!update.$setOnInsert) {
update.$setOnInsert = {};
}
update.$setOnInsert[this.schema.options.versionKey] = 0;
}
var mq = new Query({}, {}, this, this.collection);
mq.select(fields);
return mq.findOneAndUpdate(conditions, update, options, callback);
};
/**
* Issues a mongodb findAndModify update command by a document's _id field.
* `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`.
*
* Finds a matching document, updates it according to the `update` arg,
* passing any `options`, and returns the found document (if any) to the
* callback. The query executes immediately if `callback` is passed else a
* Query object is returned.
*
* This function triggers `findOneAndUpdate` middleware.
*
* ####Options:
*
* - `new`: bool - true to return the modified document rather than the original. defaults to false
* - `upsert`: bool - creates the object if it doesn't exist. defaults to false.
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `select`: sets the document fields to return
*
* ####Examples:
*
* A.findByIdAndUpdate(id, update, options, callback) // executes
* A.findByIdAndUpdate(id, update, options) // returns Query
* A.findByIdAndUpdate(id, update, callback) // executes
* A.findByIdAndUpdate(id, update) // returns Query
* A.findByIdAndUpdate() // returns Query
*
* ####Note:
*
* All top level update keys which are not `atomic` operation names are treated as set operations:
*
* ####Example:
*
* Model.findByIdAndUpdate(id, { name: 'jason borne' }, options, callback)
*
* // is sent as
* Model.findByIdAndUpdate(id, { $set: { name: 'jason borne' }}, options, callback)
*
* This helps prevent accidentally overwriting your document with `{ name: 'jason borne' }`.
*
* ####Note:
*
* Values are cast to their appropriate types when using the findAndModify helpers.
* However, the below are never executed.
*
* - defaults
* - setters
*
* `findAndModify` helpers support limited defaults and validation. You can
* enable these by setting the `setDefaultsOnInsert` and `runValidators` options,
* respectively.
*
* If you need full-fledged validation, use the traditional approach of first
* retrieving the document.
*
* Model.findById(id, function (err, doc) {
* if (err) ..
* doc.name = 'jason borne';
* doc.save(callback);
* });
*
* @param {Object|Number|String} id value of `_id` to query by
* @param {Object} [update]
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see Model.findOneAndUpdate #model_Model.findOneAndUpdate
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @api public
*/
Model.findByIdAndUpdate = function(id, update, options, callback) {
if (arguments.length === 1) {
if (typeof id === 'function') {
var msg = 'Model.findByIdAndUpdate(): First argument must not be a function.\n\n'
+ ' ' + this.modelName + '.findByIdAndUpdate(id, callback)\n'
+ ' ' + this.modelName + '.findByIdAndUpdate(id)\n'
+ ' ' + this.modelName + '.findByIdAndUpdate()\n';
throw new TypeError(msg);
}
return this.findOneAndUpdate({_id: id}, undefined);
}
// if a model is passed in instead of an id
if (id instanceof Document) {
id = id._id;
}
return this.findOneAndUpdate.call(this, {_id: id}, update, options, callback);
};
/**
* Issue a mongodb findAndModify remove command.
*
* Finds a matching document, removes it, passing the found document (if any) to the callback.
*
* Executes immediately if `callback` is passed else a Query object is returned.
*
* ####Options:
*
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `select`: sets the document fields to return
*
* ####Examples:
*
* A.findOneAndRemove(conditions, options, callback) // executes
* A.findOneAndRemove(conditions, options) // return Query
* A.findOneAndRemove(conditions, callback) // executes
* A.findOneAndRemove(conditions) // returns Query
* A.findOneAndRemove() // returns Query
*
* Values are cast to their appropriate types when using the findAndModify helpers.
* However, the below are never executed.
*
* - defaults
* - setters
*
* `findAndModify` helpers support limited defaults and validation. You can
* enable these by setting the `setDefaultsOnInsert` and `runValidators` options,
* respectively.
*
* If you need full-fledged validation, use the traditional approach of first
* retrieving the document.
*
* Model.findById(id, function (err, doc) {
* if (err) ..
* doc.name = 'jason borne';
* doc.save(callback);
* });
*
* @param {Object} conditions
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @api public
*/
Model.findOneAndRemove = function(conditions, options, callback) {
if (arguments.length === 1 && typeof conditions === 'function') {
var msg = 'Model.findOneAndRemove(): First argument must not be a function.\n\n'
+ ' ' + this.modelName + '.findOneAndRemove(conditions, callback)\n'
+ ' ' + this.modelName + '.findOneAndRemove(conditions)\n'
+ ' ' + this.modelName + '.findOneAndRemove()\n';
throw new TypeError(msg);
}
if (typeof options === 'function') {
callback = options;
options = undefined;
}
var fields;
if (options) {
fields = options.select;
options.select = undefined;
}
var mq = new Query({}, {}, this, this.collection);
mq.select(fields);
return mq.findOneAndRemove(conditions, options, callback);
};
/**
* Issue a mongodb findAndModify remove command by a document's _id field. `findByIdAndRemove(id, ...)` is equivalent to `findOneAndRemove({ _id: id }, ...)`.
*
* Finds a matching document, removes it, passing the found document (if any) to the callback.
*
* Executes immediately if `callback` is passed, else a `Query` object is returned.
*
* ####Options:
*
* - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
* - `select`: sets the document fields to return
*
* ####Examples:
*
* A.findByIdAndRemove(id, options, callback) // executes
* A.findByIdAndRemove(id, options) // return Query
* A.findByIdAndRemove(id, callback) // executes
* A.findByIdAndRemove(id) // returns Query
* A.findByIdAndRemove() // returns Query
*
* @param {Object|Number|String} id value of `_id` to query by
* @param {Object} [options]
* @param {Function} [callback]
* @return {Query}
* @see Model.findOneAndRemove #model_Model.findOneAndRemove
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
*/
Model.findByIdAndRemove = function(id, options, callback) {
if (arguments.length === 1 && typeof id === 'function') {
var msg = 'Model.findByIdAndRemove(): First argument must not be a function.\n\n'
+ ' ' + this.modelName + '.findByIdAndRemove(id, callback)\n'
+ ' ' + this.modelName + '.findByIdAndRemove(id)\n'
+ ' ' + this.modelName + '.findByIdAndRemove()\n';
throw new TypeError(msg);
}
return this.findOneAndRemove({_id: id}, options, callback);
};
/**
* Shortcut for creating a new Document that is automatically saved to the db
* if valid.
*
* ####Example:
*
* // pass individual docs
* Candy.create({ type: 'jelly bean' }, { type: 'snickers' }, function (err, jellybean, snickers) {
* if (err) // ...
* });
*
* // pass an array
* var array = [{ type: 'jelly bean' }, { type: 'snickers' }];
* Candy.create(array, function (err, candies) {
* if (err) // ...
*
* var jellybean = candies[0];
* var snickers = candies[1];
* // ...
* });
*
* // callback is optional; use the returned promise if you like:
* var promise = Candy.create({ type: 'jawbreaker' });
* promise.then(function (jawbreaker) {
* // ...
* })
*
* @param {Array|Object|*} doc(s)
* @param {Function} [callback] callback
* @return {Promise}
* @api public
*/
Model.create = function create(doc, callback) {
var args,
cb;
if (Array.isArray(doc)) {
args = doc;
cb = callback;
} else {
var last = arguments[arguments.length - 1];
if (typeof last === 'function') {
cb = last;
args = utils.args(arguments, 0, arguments.length - 1);
} else {
args = utils.args(arguments);
}
}
var Promise = PromiseProvider.get();
var _this = this;
var promise = new Promise.ES6(function(resolve, reject) {
if (args.length === 0) {
process.nextTick(function() {
cb && cb(null);
resolve(null);
});
return;
}
var toExecute = [];
args.forEach(function(doc) {
toExecute.push(function(callback) {
var toSave = new _this(doc);
var callbackWrapper = function(error, doc) {
if (error) {
return callback(error);
}
callback(null, doc);
};
// Hack to avoid getting a promise because of
// $__registerHooksFromSchema
if (toSave.$__original_save) {
toSave.$__original_save({__noPromise: true}, callbackWrapper);
} else {
toSave.save({__noPromise: true}, callbackWrapper);
}
});
});
async.parallel(toExecute, function(error