/** * Module dependencies. */ var EventEmitter = require('events').EventEmitter , MongooseError = require('./error') , MixedSchema = require('./schema/mixed') , Schema = require('./schema') , ValidatorError = require('./schematype').ValidatorError , utils = require('./utils') , clone = utils.clone , isMongooseObject = utils.isMongooseObject , inspect = require('util').inspect , StateMachine = require('./statemachine') , ActiveRoster = StateMachine.ctor('require', 'modify', 'init') , deepEqual = utils.deepEqual , hooks = require('hooks') , DocumentArray /** * Document constructor. * * @param {Object} values to set * @api private */ function Document (obj, fields) { // node <0.4.3 bug if (!this._events) this._events = {}; this.setMaxListeners(0); this._strictMode = this.schema.options && this.schema.options.strict; if ('boolean' === typeof fields) { this._strictMode = fields; fields = undefined; } else { this._selected = fields; } this._doc = this.buildDoc(fields); this._activePaths = new ActiveRoster(); var self = this; this.schema.requiredPaths.forEach(function (path) { self._activePaths.require(path); }); this._saveError = null; this._validationError = null; this.isNew = true; if (obj) this.set(obj, undefined, true); this._registerHooks(); this.doQueue(); this.errors = undefined; this._shardval = undefined; }; /** * Inherit from EventEmitter. */ Document.prototype.__proto__ = EventEmitter.prototype; /** * Base Mongoose instance for the model. Set by the Mongoose instance upon * pre-compilation. * * @api public */ Document.prototype.base; /** * Document schema as a nested structure. * * @api public */ Document.prototype.schema; /** * Whether the document is new. * * @api public */ Document.prototype.isNew; /** * Validation errors. * * @api public */ Document.prototype.errors; /** * Builds the default doc structure * * @api private */ Document.prototype.buildDoc = function (fields) { var doc = {} , self = this , exclude , keys , key , ki // determine if this doc is a result of a query with // excluded fields if (fields && 'Object' === fields.constructor.name) { keys = Object.keys(fields); ki = keys.length; while (ki--) { if ('_id' !== keys[ki]) { exclude = 0 === fields[keys[ki]]; break; } } } var paths = Object.keys(this.schema.paths) , plen = paths.length , ii = 0 for (; ii < plen; ++ii) { var p = paths[ii] , type = this.schema.paths[p] , path = p.split('.') , len = path.length , last = len-1 , doc_ = doc , i = 0 for (; i < len; ++i) { var piece = path[i] , def if (i === last) { if (fields) { if (exclude) { // apply defaults to all non-excluded fields if (p in fields) continue; def = type.getDefault(self); if ('undefined' !== typeof def) doc_[piece] = def; } else { // do nothing. only the fields specified in // the query will be populated } } else { def = type.getDefault(self); if ('undefined' !== typeof def) doc_[piece] = def; } } else { doc_ = doc_[piece] || (doc_[piece] = {}); } } }; return doc; }; /** * Inits (hydrates) the document without setters. * * Called internally after a document is returned * from mongodb. * * @param {Object} document returned by mongo * @param {Function} callback * @api private */ Document.prototype.init = function (doc, fn) { this.isNew = false; init(this, doc, this._doc); this._storeShard(); this.emit('init'); if (fn) fn(null); return this; }; /** * Init helper. * @param {Object} instance * @param {Object} obj - raw mongodb doc * @param {Object} doc - object we are initializing * @private */ function init (self, obj, doc, prefix) { prefix = prefix || ''; var keys = Object.keys(obj) , len = keys.length , schema , path , i; while (len--) { i = keys[len]; path = prefix + i; schema = self.schema.path(path); if (!schema && obj[i] && 'Object' === obj[i].constructor.name) { // assume nested object doc[i] = {}; init(self, obj[i], doc[i], path + '.'); } else { if (obj[i] === null) { doc[i] = null; } else if (obj[i] !== undefined) { if (schema) { self.try(function(){ doc[i] = schema.cast(obj[i], self, true); }); } else { doc[i] = obj[i]; } } // mark as hydrated self._activePaths.init(path); } } }; /** * _storeShard * * Stores the current values of the shard keys * for use later in the doc.save() where clause. * * Shard key values do not / are not allowed to change. * * @param {Object} document * @private */ Document.prototype._storeShard = function _storeShard () { var key = this.schema.options.shardkey; if (!(key && 'Object' == key.constructor.name)) return; var orig = this._shardval = {} , paths = Object.keys(key) , len = paths.length , val for (var i = 0; i < len; ++i) { val = this.getValue(paths[i]); if (isMongooseObject(val)) { orig[paths[i]] = val.toObject({ depopulate: true }) } else if (val.valueOf) { orig[paths[i]] = val.valueOf(); } else { orig[paths[i]] = val; } } } // Set up middleware support for (var k in hooks) { Document.prototype[k] = Document[k] = hooks[k]; } /** * Sets a path, or many paths * * Examples: * // path, value * doc.set(path, value) * * // object * doc.set({ * path : value * , path2 : { * path : value * } * } * * @param {String|Object} key path, or object * @param {Object} value, or undefined or a prefix if first parameter is an object * @param @optional {Schema|String|...} specify a type if this is an on-the-fly attribute * @api public */ Document.prototype.set = function (path, val, type) { var constructing = true === type , adhoc = type && true !== type , adhocs if (adhoc) { adhocs = this._adhocPaths || (this._adhocPaths = {}); adhocs[path] = Schema.interpretAsType(path, type); } if ('string' !== typeof path) { // new Document({ key: val }) if (null === path || undefined === path) { var _ = path; path = val; val = _; } else { var prefix = val ? val + '.' : ''; if (path instanceof Document) path = path._doc; var keys = Object.keys(path) , i = keys.length , pathtype , key while (i--) { key = keys[i]; if (null != path[key] && 'Object' === path[key].constructor.name && !(this._path(prefix + key) instanceof MixedSchema)) { this.set(path[key], prefix + key, constructing); } else if (this._strictMode) { pathtype = this.schema.pathType(prefix + key); if ('real' === pathtype || 'virtual' === pathtype) { this.set(prefix + key, path[key], constructing); } } else if (undefined !== path[key]) { this.set(prefix + key, path[key], constructing); } } return this; } } var schema; if ('virtual' === this.schema.pathType(path)) { schema = this.schema.virtualpath(path); schema.applySetters(val, this); return this; } else { schema = this._path(path); } var parts = path.split('.') , obj = this._doc , self = this , pathToMark , subpaths , subpath // When using the $set operator the path to the field must already exist. // Else mongodb throws: "LEFT_SUBFIELD only supports Object" if (parts.length <= 1) { pathToMark = path; } else { subpaths = parts.map(function (part, i) { return parts.slice(0, i).concat(part).join('.'); }); for (var i = 0, l = subpaths.length; i < l; i++) { subpath = subpaths[i]; if (this.isDirectModified(subpath) // earlier prefixes that are already // marked as dirty have precedence || this.get(subpath) === null) { pathToMark = subpath; break; } } if (!pathToMark) pathToMark = path; } if ((!schema || null === val || undefined === val) || this.try(function(){ // if this doc is being constructed we should not // trigger getters. var cur = constructing ? undefined : self.get(path); var casted = schema.cast(val, self, false, cur); val = schema.applySetters(casted, self); })) { if (this.isNew) { this.markModified(pathToMark); } else { var priorVal = this.get(path); if (!this.isDirectModified(pathToMark)) { if (undefined === val && !this.isSelected(path)) { // special case: // when a path is not selected in a query its initial // value will be undefined. this.markModified(pathToMark); } else if (!deepEqual(val, priorVal)) { this.markModified(pathToMark); } } } for (var i = 0, l = parts.length; i < l; i++) { var next = i + 1 , last = next === l; if (last) { obj[parts[i]] = val; } else { if (obj[parts[i]] && 'Object' === obj[parts[i]].constructor.name) { obj = obj[parts[i]]; } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) { obj = obj[parts[i]]; } else { obj = obj[parts[i]] = {}; } } } } return this; }; /** * Gets a raw value from a path (no getters) * * @param {String} path * @api private */ Document.prototype.getValue = function (path) { var parts = path.split('.') , obj = this._doc , part; for (var i = 0, l = parts.length; i < l-1; i++) { part = parts[i]; path = convertIfInt(path); obj = obj.getValue ? obj.getValue(part) // If we have an embedded array document member : obj[part]; if (!obj) return obj; } part = parts[l-1]; path = convertIfInt(path); return obj.getValue ? obj.getValue(part) // If we have an embedded array document member : obj[part]; }; function convertIfInt (string) { if (/^\d+$/.test(string)) { return parseInt(string, 10); } return string; } /** * Sets a raw value for a path (no casting, setters, transformations) * * @param {String} path * @param {Object} value * @api private */ Document.prototype.setValue = function (path, val) { var parts = path.split('.') , obj = this._doc; for (var i = 0, l = parts.length; i < l-1; i++) { obj = obj[parts[i]]; } obj[parts[l-1]] = val; return this; }; /** * Triggers casting on a specific path * * @todo - deprecate? not used anywhere * @param {String} path * @api public */ Document.prototype.doCast = function (path) { var schema = this.schema.path(path); if (schema) this.setValue(path, this.getValue(path)); }; /** * Gets a path * * @param {String} key path * @param @optional {Schema|String|...} specify a type if this is an on-the-fly attribute * @api public */ Document.prototype.get = function (path, type) { var adhocs; if (type) { adhocs = this._adhocPaths || (this._adhocPaths = {}); adhocs[path] = Schema.interpretAsType(path, type); } var schema = this._path(path) || this.schema.virtualpath(path) , pieces = path.split('.') , obj = this._doc; for (var i = 0, l = pieces.length; i < l; i++) { obj = null == obj ? null : obj[pieces[i]]; } if (schema) { obj = schema.applyGetters(obj, this); } return obj; }; /** * Finds the path in the ad hoc type schema list or * in the schema's list of type schemas * @param {String} path * @api private */ Document.prototype._path = function (path) { var adhocs = this._adhocPaths , adhocType = adhocs && adhocs[path]; if (adhocType) { return adhocType; } else { return this.schema.path(path); } }; /** * Commits a path, marking as modified if needed. Useful for mixed keys * * @api public */ Document.prototype.commit = Document.prototype.markModified = function (path) { this._activePaths.modify(path); }; /** * Captures an exception that will be bubbled to `save` * * @param {Function} function to execute * @param {Object} scope */ Document.prototype.try = function (fn, scope) { var res; try { fn.call(scope); res = true; } catch (e) { this.error(e); res = false; } return res; }; /** * modifiedPaths * * Returns the list of paths that have been modified. * * If we set `documents.0.title` to 'newTitle' * then `documents`, `documents.0`, and `documents.0.title` * are modified. * * @api public * @returns Boolean */ Document.prototype.__defineGetter__('modifiedPaths', function () { var directModifiedPaths = Object.keys(this._activePaths.states.modify); return directModifiedPaths.reduce(function (list, path) { var parts = path.split('.'); return list.concat(parts.reduce(function (chains, part, i) { return chains.concat(parts.slice(0, i).concat(part).join('.')); }, [])); }, []); }); /** * Checks if a path or any full path containing path as part of * its path chain has been directly modified. * * e.g., if we set `documents.0.title` to 'newTitle' * then we have directly modified `documents.0.title` * but not directly modified `documents` or `documents.0`. * Nonetheless, we still say `documents` and `documents.0` * are modified. They just are not considered direct modified. * The distinction is important because we need to distinguish * between what has been directly modified and what hasn't so * that we can determine the MINIMUM set of dirty data * that we want to send to MongoDB on a Document save. * * @param {String} path * @returns Boolean * @api public */ Document.prototype.isModified = function (path) { return !!~this.modifiedPaths.indexOf(path); }; /** * Checks if a path has been directly set and modified. False if * the path is only part of a larger path that was directly set. * * e.g., if we set `documents.0.title` to 'newTitle' * then we have directly modified `documents.0.title` * but not directly modified `documents` or `documents.0`. * Nonetheless, we still say `documents` and `documents.0` * are modified. They just are not considered direct modified. * The distinction is important because we need to distinguish * between what has been directly modified and what hasn't so * that we can determine the MINIMUM set of dirty data * that we want to send to MongoDB on a Document save. * * @param {String} path * @returns Boolean * @api public */ Document.prototype.isDirectModified = function (path) { return (path in this._activePaths.states.modify); }; /** * Checks if a certain path was initialized * * @param {String} path * @returns Boolean * @api public */ Document.prototype.isInit = function (path) { return (path in this._activePaths.states.init); }; /** * Checks if a path was selected. * @param {String} path * @return Boolean * @api public */ Document.prototype.isSelected = function isSelected (path) { if (this._selected) { if ('_id' === path) { return 0 !== this._selected._id; } var paths = Object.keys(this._selected) , i = paths.length , inclusive = false , cur while (i--) { cur = paths[i]; if ('_id' == cur) continue; inclusive = !! this._selected[cur]; break; } if (path in this._selected) { return inclusive; } i = paths.length; while (i--) { cur = paths[i]; if ('_id' == cur) continue; if (0 === cur.indexOf(path + '.')) { return inclusive; } if (0 === (path + '.').indexOf(cur)) { return inclusive; } } return ! inclusive; } return true; } /** * Validation middleware * * @param {Function} next * @api public */ Document.prototype.validate = function (next) { var total = 0 , self = this , validating = {} if (!this._activePaths.some('require', 'init', 'modify')) { return complete(); } function complete () { next(self._validationError); self._validationError = null; } this._activePaths.forEach('require', 'init', 'modify', function validatePath (path) { if (validating[path]) return; validating[path] = true; total++; process.nextTick(function(){ var p = self.schema.path(path); if (!p) return --total || complete(); p.doValidate(self.getValue(path), function (err) { if (err) self.invalidate(path, err); --total || complete(); }, self); }); }); return this; }; /** * Marks a path as invalid, causing a subsequent validation to fail. * * @param {String} path of the field to invalidate * @param {String/Error} error of the path. * @api public */ Document.prototype.invalidate = function (path, err) { if (!this._validationError) { this._validationError = new ValidationError(this); } if (!err || 'string' === typeof err) { err = new ValidatorError(path, err); } this._validationError.errors[path] = err; } /** * Resets the atomics and modified states of this document. * * @private * @return {this} */ Document.prototype._reset = function reset () { var self = this; DocumentArray || (DocumentArray = require('./types/documentarray')); this._activePaths .map('init', 'modify', function (i) { return self.getValue(i); }) .filter(function (val) { return (val && val instanceof DocumentArray && val.length); }) .forEach(function (array) { array.forEach(function (doc) { doc._reset(); }); }); // clear atomics this._dirty().forEach(function (dirt) { var type = dirt.value; if (type && type._path && type.doAtomics) { type._atomics = {}; } }); // Clear 'modify'('dirty') cache this._activePaths.clear('modify'); var self = this; this.schema.requiredPaths.forEach(function (path) { self._activePaths.require(path); }); return this; } /** * Returns the dirty paths / vals * * @api private */ Document.prototype._dirty = function _dirty () { var self = this; var all = this._activePaths.map('modify', function (path) { return { path: path , value: self.getValue(path) , schema: self._path(path) }; }); // Sort dirty paths in a flat hierarchy. all.sort(function (a, b) { return (a.path < b.path ? -1 : (a.path > b.path ? 1 : 0)); }); // Ignore "foo.a" if "foo" is dirty already. var minimal = [] , lastReference = null; all.forEach(function (item, i) { if (item.path.indexOf(lastReference) !== 0) { lastReference = item.path + '.'; minimal.push(item); } }); return minimal; } /** * Returns if the document has been modified * * @return {Boolean} * @api public */ Document.prototype.__defineGetter__('modified', function () { return this._activePaths.some('modify'); }); /** * Compiles schemas. * @api private */ function compile (tree, proto, prefix) { var keys = Object.keys(tree) , i = keys.length , limb , key; while (i--) { key = keys[i]; limb = tree[key]; define(key , (('Object' === limb.constructor.name && Object.keys(limb).length) && (!limb.type || limb.type.type) ? limb : null) , proto , prefix , keys); } }; /** * Defines the accessor named prop on the incoming prototype. * @api private */ function define (prop, subprops, prototype, prefix, keys) { var prefix = prefix || '' , path = (prefix ? prefix + '.' : '') + prop; if (subprops) { Object.defineProperty(prototype, prop, { enumerable: true , get: function () { if (!this.__getters) this.__getters = {}; if (!this.__getters[path]) { var nested = Object.create(this); // save scope for nested getters/setters if (!prefix) nested._scope = this; // shadow inherited getters from sub-objects so // thing.nested.nested.nested... doesn't occur (gh-366) var i = 0 , len = keys.length; for (; i < len; ++i) { // over-write the parents getter without triggering it Object.defineProperty(nested, keys[i], { enumerable: false // It doesn't show up. , writable: true // We can set it later. , configurable: true // We can Object.defineProperty again. , value: undefined // It shadows its parent. }); } nested.toObject = function () { return this.get(path); }; compile(subprops, nested, path); this.__getters[path] = nested; } return this.__getters[path]; } , set: function (v) { return this.set(v, path); } }); } else { Object.defineProperty(prototype, prop, { enumerable: true , get: function ( ) { return this.get.call(this._scope || this, path); } , set: function (v) { return this.set.call(this._scope || this, path, v); } }); } }; /** * We override the schema setter to compile accessors * * @api private */ Document.prototype.__defineSetter__('schema', function (schema) { compile(schema.tree, this); this._schema = schema; }); /** * We override the schema getter to return the internal reference * * @api private */ Document.prototype.__defineGetter__('schema', function () { return this._schema; }); /** * Register default hooks * * @api private */ Document.prototype._registerHooks = function _registerHooks () { if (!this.save) return; DocumentArray || (DocumentArray = require('./types/documentarray')); this.pre('save', function (next) { // we keep the error semaphore to make sure we don't // call `save` unnecessarily (we only need 1 error) var subdocs = 0 , error = false , self = this; var arrays = this._activePaths .map('init', 'modify', function (i) { return self.getValue(i); }) .filter(function (val) { return (val && val instanceof DocumentArray && val.length); }); if (!arrays.length) return next(); arrays.forEach(function (array) { subdocs += array.length; array.forEach(function (value) { if (!error) value.save(function (err) { if (!error) { if (err) { error = true; next(err); } else --subdocs || next(); } }); }); }); }, function (err) { this.db.emit('error', err); }).pre('save', function checkForExistingErrors (next) { if (this._saveError) { next(this._saveError); this._saveError = null; } else { next(); } }).pre('save', function validation (next) { return this.validate(next); }); }; /** * Registers an error * * @TODO underscore this method * @param {Error} error * @api private */ Document.prototype.error = function (err) { this._saveError = err; return this; }; /** * Executes methods queued from the Schema definition * * @TODO underscore this method * @api private */ Document.prototype.doQueue = function () { if (this.schema && this.schema.callQueue) for (var i = 0, l = this.schema.callQueue.length; i < l; i++) { this[this.schema.callQueue[i][0]].apply(this, this.schema.callQueue[i][1]); } return this; }; /** * Gets the document * * Available options: * * - getters: apply all getters (path and virtual getters) * - virtuals: apply virtual getters (can override `getters` option) * * Example of only applying path getters: * * doc.toObject({ getters: true, virtuals: false }) * * Example of only applying virtual getters: * * doc.toObject({ virtuals: true }) * * Example of applying both path and virtual getters: * * doc.toObject({ getters: true }) * * @return {Object} plain object * @api public */ Document.prototype.toObject = function (options) { options || (options = {}); options.minimize = true; var ret = clone(this._doc, options); if (options.virtuals || options.getters && false !== options.virtuals) { applyGetters(this, ret, 'virtuals'); } if (options.getters) { applyGetters(this, ret, 'paths'); } return ret; }; /** * Applies virtuals properties to `json`. * * @param {Document} self * @param {Object} json * @param {String} either `virtuals` or `paths` * @return json * @private */ function applyGetters (self, json, type) { var schema = self.schema , paths = Object.keys(schema[type]) , i = paths.length , path while (i--) { path = paths[i]; var parts = path.split('.') , plen = parts.length , last = plen - 1 , branch = json , part for (var ii = 0; ii < plen; ++ii) { part = parts[ii]; if (ii === last) { branch[part] = self.get(path); } else { branch = branch[part] || (branch[part] = {}); } } } return json; } /** * JSON.stringify helper. * * Implicitly called when a document is passed * to JSON.stringify() * * @return {Object} * @api public */ Document.prototype.toJSON = function (options) { if ('undefined' === typeof options) options = {}; options.json = true; return this.toObject(options); }; /** * Helper for console.log * * @api public */ Document.prototype.toString = Document.prototype.inspect = function (options) { return inspect(this.toObject(options)); }; /** * Returns true if the Document stores the same data as doc. * @param {Document} doc to compare to * @return {Boolean} * @api public */ Document.prototype.equals = function (doc) { return this.get('_id') === doc.get('_id'); }; /** * Module exports. */ module.exports = Document; /** * Document Validation Error */ function ValidationError (instance) { MongooseError.call(this, "Validation failed"); Error.captureStackTrace(this, arguments.callee); this.name = 'ValidationError'; this.errors = instance.errors = {}; }; ValidationError.prototype.toString = function () { return this.name + ': ' + Object.keys(this.errors).map(function (key) { return String(this.errors[key]); }, this).join(', '); }; /** * Inherits from MongooseError. */ ValidationError.prototype.__proto__ = MongooseError.prototype; Document.ValidationError = ValidationError; /** * Document Error * * @param text */ function DocumentError () { MongooseError.call(this, msg); Error.captureStackTrace(this, arguments.callee); this.name = 'DocumentError'; }; /** * Inherits from MongooseError. */ DocumentError.prototype.__proto__ = MongooseError.prototype; exports.Error = DocumentError;