diff options
Diffstat (limited to 'node_modules/mongoose/lib/model.js')
| -rw-r--r-- | node_modules/mongoose/lib/model.js | 917 |
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; +}; |
