summaryrefslogtreecommitdiff
path: root/node_modules/mongoose/lib/model.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/mongoose/lib/model.js')
-rw-r--r--node_modules/mongoose/lib/model.js917
1 files changed, 917 insertions, 0 deletions
diff --git a/node_modules/mongoose/lib/model.js b/node_modules/mongoose/lib/model.js
new file mode 100644
index 0000000..75bfda3
--- /dev/null
+++ b/node_modules/mongoose/lib/model.js
@@ -0,0 +1,917 @@
+
+/**
+ * Module dependencies.
+ */
+
+var Document = require('./document')
+ , MongooseArray = require('./types/array')
+ , MongooseBuffer = require('./types/buffer')
+ , MongooseError = require('./error')
+ , Query = require('./query')
+ , utils = require('./utils')
+ , isMongooseObject = utils.isMongooseObject
+ , EventEmitter = utils.EventEmitter
+ , merge = utils.merge
+ , Promise = require('./promise')
+ , tick = utils.tick
+
+/**
+ * Model constructor
+ *
+ * @param {Object} values to set
+ * @api public
+ */
+
+function Model (doc, fields) {
+ Document.call(this, doc, fields);
+};
+
+/**
+ * Inherits from Document.
+ */
+
+Model.prototype.__proto__ = Document.prototype;
+
+/**
+ * Connection the model uses. Set by the Connection or if absent set to the
+ * default mongoose connection;
+ *
+ * @api public
+ */
+
+Model.prototype.db;
+
+/**
+ * Collection the model uses. Set by Mongoose instance
+ *
+ * @api public
+ */
+
+Model.prototype.collection;
+
+/**
+ * Model name.
+ *
+ * @api public
+ */
+
+Model.prototype.modelName;
+
+/**
+ * Returns what paths can be populated
+ *
+ * @param {query} query object
+ * @return {Object] population paths
+ * @api private
+ */
+
+Model.prototype._getPopulationKeys = function getPopulationKeys (query) {
+ if (!(query && query.options.populate)) return;
+
+ var names = Object.keys(query.options.populate)
+ , n = names.length
+ , name
+ , paths = {}
+ , hasKeys
+ , schema
+
+ while (n--) {
+ name = names[n];
+ schema = this.schema.path(name);
+ hasKeys = true;
+
+ if (!schema) {
+ // if the path is not recognized, it's potentially embedded docs
+ // walk path atoms from right to left to find a matching path
+ var pieces = name.split('.')
+ , i = pieces.length;
+
+ while (i--) {
+ var path = pieces.slice(0, i).join('.')
+ , pathSchema = this.schema.path(path);
+
+ // loop until we find an array schema
+ if (pathSchema && pathSchema.caster) {
+ if (!paths[path]) {
+ paths[path] = { sub: {} };
+ }
+
+ paths[path].sub[pieces.slice(i).join('.')] = query.options.populate[name];
+ hasKeys || (hasKeys = true);
+ break;
+ }
+ }
+ } else {
+ paths[name] = query.options.populate[name];
+ hasKeys || (hasKeys = true);
+ }
+ }
+
+ return hasKeys && paths;
+};
+
+/**
+ * Populates an object
+ *
+ * @param {SchemaType} schema type for the oid
+ * @param {Object} object id or array of object ids
+ * @param {Object} object specifying query conditions, fields, and options
+ * @param {Function} callback
+ * @api private
+ */
+
+Model.prototype._populate = function populate (schema, oid, query, fn) {
+ if (!Array.isArray(oid)) {
+ var conditions = query.conditions || {};
+ conditions._id = oid;
+
+ return this
+ .model(schema.options.ref)
+ .findOne(conditions, query.fields, query.options, fn);
+ }
+
+ if (!oid.length) {
+ return fn(null, oid);
+ }
+
+ var model = this.model(schema.caster.options.ref)
+ , conditions = query && query.conditions || {};
+ conditions._id || (conditions._id = { $in: oid });
+
+ model.find(conditions, query.fields, query.options, function (err, docs) {
+ if (err) return fn(err);
+
+ // user specified sort order?
+ if (query.options && query.options.sort) {
+ return fn(null, docs);
+ }
+
+ // put back in original id order (using a hash reduces complexity from n*n to 2n)
+ var docHash = {};
+ docs.forEach(function (doc) {
+ docHash[doc._id] = doc;
+ });
+
+ var arr = [];
+ oid.forEach(function (id) {
+ if (id in docHash) arr.push(docHash[id]);
+ });
+
+ fn(null, arr);
+ });
+};
+
+/**
+ * Performs auto-population of relations.
+ *
+ * @param {Object} document returned by mongo
+ * @param {Query} query that originated the initialization
+ * @param {Function} callback
+ * @api private
+ */
+
+Model.prototype.init = function init (doc, query, fn) {
+ if ('function' == typeof query) {
+ fn = query;
+ query = null;
+ }
+
+ var populate = this._getPopulationKeys(query);
+
+ if (!populate) {
+ return Document.prototype.init.call(this, doc, fn);
+ }
+
+ // population from other models is necessary
+ var self = this;
+
+ init(doc, '', function (err) {
+ if (err) return fn(err);
+ Document.prototype.init.call(self, doc, fn);
+ });
+
+ return this;
+
+ function init (obj, prefix, fn) {
+ prefix = prefix || '';
+
+ var keys = Object.keys(obj)
+ , len = keys.length;
+
+ function next () {
+ if (--len < 0) return fn();
+
+ var i = keys[len]
+ , path = prefix + i
+ , schema = self.schema.path(path)
+ , total = 0
+ , poppath
+
+ if (!schema && obj[i] && 'Object' === obj[i].constructor.name) {
+ // assume nested object
+ return init(obj[i], path + '.', next);
+ }
+
+ if (!(obj[i] && schema && populate[path])) return next();
+
+ // this query object is re-used and passed around. we clone
+ // it to prevent query condition contamination between
+ // one populate call to the next.
+ poppath = utils.clone(populate[path]);
+
+ if (poppath.sub) {
+ obj[i].forEach(function (subobj) {
+ var pkeys = Object.keys(poppath.sub)
+ , pi = pkeys.length
+ , key
+
+ while (pi--) {
+ key = pkeys[pi];
+
+ if (subobj[key]) (function (key) {
+
+ total++;
+ self._populate(schema.schema.path(key), subobj[key], poppath.sub[key], done);
+ function done (err, doc) {
+ if (err) return error(err);
+ subobj[key] = doc;
+ --total || next();
+ }
+ })(key);
+ }
+ });
+
+ if (0 === total) return next();
+
+ } else {
+ self._populate(schema, obj[i], poppath, function (err, doc) {
+ if (err) return error(err);
+ obj[i] = doc;
+ next();
+ });
+ }
+ };
+
+ next();
+ };
+
+ function error (err) {
+ if (error.err) return;
+ fn(error.err = err);
+ }
+};
+
+function handleSave (promise, self) {
+ return tick(function handleSave (err, result) {
+ if (err) return promise.error(err);
+
+ self._storeShard();
+
+ var numAffected;
+ if (result) {
+ numAffected = result.length
+ ? result.length
+ : result;
+ } else {
+ numAffected = 0;
+ }
+
+ self.emit('save', self, numAffected);
+ promise.complete(self, numAffected);
+ promise = null;
+ self = null;
+ });
+}
+
+/**
+ * Saves this document.
+ *
+ * @see Model#registerHooks
+ * @param {Function} fn
+ * @api public
+ */
+
+Model.prototype.save = function save (fn) {
+ var promise = new Promise(fn)
+ , complete = handleSave(promise, this)
+ , options = {}
+
+ if (this.options.safe) {
+ options.safe = this.options.safe;
+ }
+
+ if (this.isNew) {
+ // send entire doc
+ this.collection.insert(this.toObject({ depopulate: 1 }), options, complete);
+ this._reset();
+ this.isNew = false;
+ this.emit('isNew', false);
+
+ } else {
+ var delta = this._delta();
+ this._reset();
+
+ if (delta) {
+ var where = this._where();
+ this.collection.update(where, delta, options, complete);
+ } else {
+ complete(null);
+ }
+
+ this.emit('isNew', false);
+ }
+};
+
+/**
+ * Produces a special query document of the modified properties.
+ * @api private
+ */
+
+Model.prototype._delta = function _delta () {
+ var dirty = this._dirty();
+
+ if (!dirty.length) return;
+
+ var self = this
+ , useSet = this.options['use$SetOnSave'];
+
+ return dirty.reduce(function (delta, data) {
+ var type = data.value
+ , schema = data.schema
+ , atomics
+ , val
+ , obj
+
+ if (type === undefined) {
+ if (!delta.$unset) delta.$unset = {};
+ delta.$unset[data.path] = 1;
+
+ } else if (type === null) {
+ if (!delta.$set) delta.$set = {};
+ delta.$set[data.path] = type;
+
+ } else if (type._path && type.doAtomics) {
+ // a MongooseArray or MongooseNumber
+ atomics = type._atomics;
+
+ var ops = Object.keys(atomics)
+ , i = ops.length
+ , op;
+
+ while (i--) {
+ op = ops[i]
+
+ if (op === '$pushAll' || op === '$pullAll') {
+ if (atomics[op].length === 1) {
+ val = atomics[op][0];
+ delete atomics[op];
+ op = op.replace('All', '');
+ atomics[op] = val;
+ }
+ }
+
+ val = atomics[op];
+ obj = delta[op] = delta[op] || {};
+
+ if (op === '$pull' || op === '$push') {
+ if ('Object' !== val.constructor.name) {
+ if (Array.isArray(val)) val = [val];
+ // TODO Should we place pull and push casting into the pull and push methods?
+ val = schema.cast(val)[0];
+ }
+ }
+
+ obj[data.path] = isMongooseObject(val)
+ ? val.toObject({ depopulate: 1 }) // MongooseArray
+ : Array.isArray(val)
+ ? val.map(function (mem) {
+ return isMongooseObject(mem)
+ ? mem.toObject({ depopulate: 1 })
+ : mem.valueOf
+ ? mem.valueOf()
+ : mem;
+ })
+ : val.valueOf
+ ? val.valueOf() // Numbers
+ : val;
+
+ if ('$addToSet' === op) {
+ if (val.length > 1) {
+ obj[data.path] = { $each: obj[data.path] };
+ } else {
+ obj[data.path] = obj[data.path][0];
+ }
+ }
+ }
+ } else {
+ if (type instanceof MongooseArray ||
+ type instanceof MongooseBuffer) {
+ type = type.toObject({ depopulate: 1 });
+ } else if (type._path) {
+ type = type.valueOf();
+ } else {
+ // nested object literal
+ type = utils.clone(type);
+ }
+
+ if (useSet) {
+ if (!('$set' in delta))
+ delta['$set'] = {};
+
+ delta['$set'][data.path] = type;
+ } else
+ delta[data.path] = type;
+ }
+
+ return delta;
+ }, {});
+}
+
+/**
+ * _where
+ *
+ * Returns a query object which applies shardkeys if
+ * they exist.
+ *
+ * @private
+ */
+
+Model.prototype._where = function _where () {
+ var where = {};
+
+ if (this._shardval) {
+ var paths = Object.keys(this._shardval)
+ , len = paths.length
+
+ for (var i = 0; i < len; ++i) {
+ where[paths[i]] = this._shardval[paths[i]];
+ }
+ }
+
+ var id = this._doc._id.valueOf // MongooseNumber
+ ? this._doc._id.valueOf()
+ : this._doc._id;
+
+ where._id = id;
+ return where;
+}
+
+/**
+ * Remove the document
+ *
+ * @param {Function} callback
+ * @api public
+ */
+
+Model.prototype.remove = function remove (fn) {
+ if (this._removing) return this;
+
+ var promise = this._removing = new Promise(fn)
+ , where = this._where()
+ , self = this;
+
+ this.collection.remove(where, tick(function (err) {
+ if (err) {
+ this._removing = null;
+ return promise.error(err);
+ }
+ promise.complete();
+ self.emit('remove', self);
+ }));
+
+ return this;
+};
+
+/**
+ * Register hooks override
+ *
+ * @api private
+ */
+
+Model.prototype._registerHooks = function registerHooks () {
+ Document.prototype._registerHooks.call(this);
+};
+
+/**
+ * Shortcut to access another model.
+ *
+ * @param {String} model name
+ * @api public
+ */
+
+Model.prototype.model = function model (name) {
+ return this.db.model(name);
+};
+
+/**
+ * Access the options defined in the schema
+ *
+ * @api private
+ */
+
+Model.prototype.__defineGetter__('options', function () {
+ return this.schema ? this.schema.options : {};
+});
+
+/**
+ * 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 () {
+ // build indexes
+ var self = this
+ , indexes = this.schema.indexes
+ , safe = this.schema.options.safe
+ , count = indexes.length;
+
+ indexes.forEach(function (index) {
+ var options = index[1];
+ options.safe = safe;
+ self.collection.ensureIndex(index[0], options, tick(function (err) {
+ if (err) return self.db.emit('error', err);
+ --count || self.emit('index');
+ }));
+ });
+
+ this.schema.emit('init', this);
+};
+
+/**
+ * Document schema
+ *
+ * @api public
+ */
+
+Model.schema;
+
+/**
+ * Database instance the model uses.
+ *
+ * @api public
+ */
+
+Model.db;
+
+/**
+ * Collection the model uses.
+ *
+ * @api public
+ */
+
+Model.collection;
+
+/**
+ * Define properties that access the prototype.
+ */
+
+['db', 'collection', 'schema', 'options', 'model'].forEach(function(prop){
+ Model.__defineGetter__(prop, function(){
+ return this.prototype[prop];
+ });
+});
+
+/**
+ * Module exports.
+ */
+
+module.exports = exports = Model;
+
+Model.remove = function remove (conditions, callback) {
+ if ('function' === typeof conditions) {
+ callback = conditions;
+ conditions = {};
+ }
+
+ var query = new Query(conditions).bind(this, 'remove');
+
+ if ('undefined' === typeof callback)
+ return query;
+
+ this._applyNamedScope(query);
+ return query.remove(callback);
+};
+
+/**
+ * Finds documents
+ *
+ * Examples:
+ * // retrieve only certain keys
+ * MyModel.find({ name: /john/i }, ['name', 'friends'], function () { })
+ *
+ * // pass options
+ * MyModel.find({ name: /john/i }, [], { skip: 10 } )
+ *
+ * @param {Object} conditions
+ * @param {Object/Function} (optional) fields to hydrate or callback
+ * @param {Function} callback
+ * @api public
+ */
+
+Model.find = function find (conditions, fields, options, callback) {
+ if ('function' == typeof conditions) {
+ callback = conditions;
+ conditions = {};
+ fields = null;
+ options = null;
+ } else if ('function' == typeof fields) {
+ callback = fields;
+ fields = null;
+ options = null;
+ } else if ('function' == typeof options) {
+ callback = options;
+ options = null;
+ }
+
+ var query = new Query(conditions, options);
+ query.bind(this, 'find');
+ query.select(fields);
+
+ if ('undefined' === typeof callback)
+ return query;
+
+ this._applyNamedScope(query);
+ return query.find(callback);
+};
+
+/**
+ * Merges the current named scope query into `query`.
+ *
+ * @param {Query} query
+ * @api private
+ */
+
+Model._applyNamedScope = function _applyNamedScope (query) {
+ var cQuery = this._cumulativeQuery;
+
+ if (cQuery) {
+ merge(query._conditions, cQuery._conditions);
+ if (query._fields && cQuery._fields)
+ merge(query._fields, cQuery._fields);
+ if (query.options && cQuery.options)
+ merge(query.options, cQuery.options);
+ delete this._cumulativeQuery;
+ }
+
+ return query;
+}
+
+/**
+ * Finds by id
+ *
+ * @param {ObjectId/Object} objectid, or a value that can be casted to it
+ * @api public
+ */
+
+Model.findById = function findById (id, fields, options, callback) {
+ return this.findOne({ _id: id }, fields, options, callback);
+};
+
+/**
+ * Finds one document
+ *
+ * @param {Object} conditions
+ * @param {Object/Function} (optional) fields to hydrate or callback
+ * @param {Function} callback
+ * @api public
+ */
+
+Model.findOne = function findOne (conditions, fields, options, callback) {
+ if ('function' == typeof options) {
+ // TODO Handle all 3 of the following scenarios
+ // Hint: Only some of these scenarios are possible if cQuery is present
+ // Scenario: findOne(conditions, fields, callback);
+ // Scenario: findOne(fields, options, callback);
+ // Scenario: findOne(conditions, options, callback);
+ callback = options;
+ options = null;
+ } else if ('function' == typeof fields) {
+ // TODO Handle all 2 of the following scenarios
+ // Scenario: findOne(conditions, callback)
+ // Scenario: findOne(fields, callback)
+ // Scenario: findOne(options, callback);
+ callback = fields;
+ fields = null;
+ options = null;
+ } else if ('function' == typeof conditions) {
+ callback = conditions;
+ conditions = {};
+ fields = null;
+ options = null;
+ }
+
+ var query = new Query(conditions, options).select(fields).bind(this, 'findOne');
+
+ if ('undefined' == typeof callback)
+ return query;
+
+ this._applyNamedScope(query);
+ return query.findOne(callback);
+};
+
+/**
+ * Counts documents
+ *
+ * @param {Object} conditions
+ * @param {Function} optional callback
+ * @api public
+ */
+
+Model.count = function count (conditions, callback) {
+ if ('function' === typeof conditions)
+ callback = conditions, conditions = {};
+
+ var query = new Query(conditions).bind(this, 'count');
+ if ('undefined' == typeof callback)
+ return query;
+
+ this._applyNamedScope(query);
+ return query.count(callback);
+};
+
+Model.distinct = function distinct (field, conditions, callback) {
+ var query = new Query(conditions).bind(this, 'distinct');
+ if ('undefined' == typeof callback) {
+ query._distinctArg = field;
+ return query;
+ }
+
+ this._applyNamedScope(query);
+ return query.distinct(field, callback);
+};
+
+/**
+ * `where` enables a very nice sugary api for doing your queries.
+ * For example, instead of writing:
+ * User.find({age: {$gte: 21, $lte: 65}}, callback);
+ * we can instead write more readably:
+ * User.where('age').gte(21).lte(65);
+ * Moreover, you can also chain a bunch of these together like:
+ * User
+ * .where('age').gte(21).lte(65)
+ * .where('name', /^b/i) // All names that begin where b or B
+ * .where('friends').slice(10);
+ * @param {String} path
+ * @param {Object} val (optional)
+ * @return {Query}
+ * @api public
+ */
+
+Model.where = function where (path, val) {
+ var q = new Query().bind(this, 'find');
+ return q.where.apply(q, arguments);
+};
+
+/**
+ * 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.
+ *
+ * @param {String|Function} js is a javascript string or anonymous function
+ * @return {Query}
+ * @api public
+ */
+
+Model.$where = function $where () {
+ var q = new Query().bind(this, 'find');
+ return q.$where.apply(q, arguments);
+};
+
+/**
+ * Shortcut for creating a new Document that is automatically saved
+ * to the db if valid.
+ *
+ * @param {Object} doc
+ * @param {Function} callback
+ * @api public
+ */
+
+Model.create = function create (doc, fn) {
+ if (1 === arguments.length) {
+ return 'function' === typeof doc && doc(null);
+ }
+
+ var self = this
+ , docs = [null]
+ , promise
+ , count
+ , args
+
+ if (Array.isArray(doc)) {
+ args = doc;
+ } else {
+ args = utils.args(arguments, 0, arguments.length - 1);
+ fn = arguments[arguments.length - 1];
+ }
+
+ if (0 === args.length) return fn(null);
+
+ promise = new Promise(fn);
+ count = args.length;
+
+ args.forEach(function (arg, i) {
+ var doc = new self(arg);
+ docs[i+1] = doc;
+ doc.save(function (err) {
+ if (err) return promise.error(err);
+ --count || fn.apply(null, docs);
+ });
+ });
+
+ // TODO
+ // utilize collection.insertAll for batch processing?
+};
+
+/**
+ * Updates documents.
+ *
+ * Examples:
+ *
+ * MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);
+ * MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
+ *
+ * Valid options:
+ *
+ * - safe (boolean) safe mode (defaults to value set in schema (true))
+ * - upsert (boolean) whether to create the doc if it doesn't match (false)
+ * - multi (boolean) whether multiple documents should be updated (false)
+ *
+ * @param {Object} conditions
+ * @param {Object} doc
+ * @param {Object} options
+ * @param {Function} callback
+ * @return {Query}
+ * @api public
+ */
+
+Model.update = function update (conditions, doc, options, callback) {
+ if (arguments.length < 4) {
+ if ('function' === typeof options) {
+ // Scenario: update(conditions, doc, callback)
+ callback = options;
+ options = null;
+ } else if ('function' === typeof doc) {
+ // Scenario: update(doc, callback);
+ callback = doc;
+ doc = conditions;
+ conditions = {};
+ options = null;
+ }
+ }
+
+ var query = new Query(conditions, options).bind(this, 'update', doc);
+
+ if ('undefined' == typeof callback)
+ return query;
+
+ this._applyNamedScope(query);
+ return query.update(doc, callback);
+};
+
+/**
+ * Compiler utility.
+ *
+ * @param {String} model name
+ * @param {Schema} schema object
+ * @param {String} collection name
+ * @param {Connection} connection to use
+ * @param {Mongoose} mongoose instance
+ * @api private
+ */
+
+Model.compile = function compile (name, schema, collectionName, connection, base) {
+ // generate new class
+ function model () {
+ Model.apply(this, arguments);
+ };
+
+ model.modelName = name;
+ model.__proto__ = Model;
+ model.prototype.__proto__ = Model.prototype;
+ model.prototype.base = base;
+ model.prototype.schema = schema;
+ model.prototype.db = connection;
+ model.prototype.collection = connection.collection(collectionName);
+
+ // apply methods
+ for (var i in schema.methods)
+ model.prototype[i] = schema.methods[i];
+
+ // apply statics
+ for (var i in schema.statics)
+ model[i] = schema.statics[i];
+
+ // apply named scopes
+ if (schema.namedScopes) schema.namedScopes.compile(model);
+
+ return model;
+};