/*! * Module dependencies. */ var NodeJSDocument = require('./document'); var EventEmitter = require('events').EventEmitter; var MongooseError = require('./error'); var Schema = require('./schema'); var ObjectId = require('./types/objectid'); var utils = require('./utils'); var ValidationError = MongooseError.ValidationError; var InternalCache = require('./internal'); var PromiseProvider = require('./promise_provider'); var VersionError = require('./error').VersionError; var Embedded; /** * Document constructor. * * @param {Object} obj the values to set * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data * @param {Boolean} [skipId] bool, should we auto create an ObjectId _id * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter * @event `init`: Emitted on a document after it has was retrieved from the db and fully hydrated by Mongoose. * @event `save`: Emitted when the document is successfully saved * @api private */ function Document(obj, schema, fields, skipId, skipInit) { if (!(this instanceof Document)) { return new Document(obj, schema, fields, skipId, skipInit); } if (utils.isObject(schema) && !schema.instanceOfSchema) { schema = new Schema(schema); } // When creating EmbeddedDocument, it already has the schema and he doesn't need the _id schema = this.schema || schema; // Generate ObjectId if it is missing, but it requires a scheme if (!this.schema && schema.options._id) { obj = obj || {}; if (obj._id === undefined) { obj._id = new ObjectId(); } } if (!schema) { throw new MongooseError.MissingSchemaError(); } this.$__setSchema(schema); this.$__ = new InternalCache; this.$__.emitter = new EventEmitter(); this.isNew = true; this.errors = undefined; if (typeof fields === 'boolean') { this.$__.strictMode = fields; fields = undefined; } else { this.$__.strictMode = this.schema.options && this.schema.options.strict; this.$__.selected = fields; } var required = this.schema.requiredPaths(); for (var i = 0; i < required.length; ++i) { this.$__.activePaths.require(required[i]); } this.$__.emitter.setMaxListeners(0); this._doc = this.$__buildDoc(obj, fields, skipId); if (!skipInit && obj) { this.init(obj); } this.$__registerHooksFromSchema(); // apply methods for (var m in schema.methods) { this[m] = schema.methods[m]; } // apply statics for (var s in schema.statics) { this[s] = schema.statics[s]; } } /*! * Inherit from the NodeJS document */ Document.prototype = Object.create(NodeJSDocument.prototype); Document.prototype.constructor = Document; /*! * Browser doc exposes the event emitter API */ Document.$emitter = new EventEmitter(); utils.each( ['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners', 'removeAllListeners', 'addListener'], function(emitterFn) { Document[emitterFn] = function() { return Document.$emitter[emitterFn].apply(Document.$emitter, arguments); }; }); /*! * Executes methods queued from the Schema definition * * @api private * @method $__registerHooksFromSchema * @deprecated * @memberOf Document */ Document.prototype.$__registerHooksFromSchema = function() { Embedded = Embedded || require('./types/embedded'); var Promise = PromiseProvider.get(); var _this = this; var q = _this.schema && _this.schema.callQueue; var toWrapEl; var len; var i; var j; var pointCut; var keys; if (!q.length) { return _this; } // we are only interested in 'pre' hooks, and group by point-cut var toWrap = { post: [] }; var pair; for (i = 0; i < q.length; ++i) { pair = q[i]; if (pair[0] !== 'pre' && pair[0] !== 'post' && pair[0] !== 'on') { _this[pair[0]].apply(_this, pair[1]); continue; } var args = [].slice.call(pair[1]); pointCut = pair[0] === 'on' ? 'post' : args[0]; if (!(pointCut in toWrap)) { toWrap[pointCut] = {post: [], pre: []}; } if (pair[0] === 'post') { toWrap[pointCut].post.push(args); } else if (pair[0] === 'on') { toWrap[pointCut].push(args); } else { toWrap[pointCut].pre.push(args); } } // 'post' hooks are simpler len = toWrap.post.length; toWrap.post.forEach(function(args) { _this.on.apply(_this, args); }); delete toWrap.post; // 'init' should be synchronous on subdocuments if (toWrap.init && _this instanceof Embedded) { if (toWrap.init.pre) { toWrap.init.pre.forEach(function(args) { _this.$pre.apply(_this, args); }); } if (toWrap.init.post) { toWrap.init.post.forEach(function(args) { _this.$post.apply(_this, args); }); } delete toWrap.init; } else if (toWrap.set) { // Set hooks also need to be sync re: gh-3479 if (toWrap.set.pre) { toWrap.set.pre.forEach(function(args) { _this.$pre.apply(_this, args); }); } if (toWrap.set.post) { toWrap.set.post.forEach(function(args) { _this.$post.apply(_this, args); }); } delete toWrap.set; } keys = Object.keys(toWrap); len = keys.length; for (i = 0; i < len; ++i) { pointCut = keys[i]; // this is so we can wrap everything into a promise; var newName = ('$__original_' + pointCut); if (!_this[pointCut]) { continue; } if (_this[pointCut].$isWrapped) { continue; } _this[newName] = _this[pointCut]; _this[pointCut] = (function(_newName) { return function wrappedPointCut() { var args = [].slice.call(arguments); var lastArg = args.pop(); var fn; var originalError = new Error(); var $results; if (lastArg && typeof lastArg !== 'function') { args.push(lastArg); } else { fn = lastArg; } var promise = new Promise.ES6(function(resolve, reject) { args.push(function(error) { if (error) { // gh-2633: since VersionError is very generic, take the // stack trace of the original save() function call rather // than the async trace if (error instanceof VersionError) { error.stack = originalError.stack; } _this.$__handleReject(error); reject(error); return; } // There may be multiple results and promise libs other than // mpromise don't support passing multiple values to `resolve()` $results = Array.prototype.slice.call(arguments, 1); resolve.apply(promise, $results); }); _this[_newName].apply(_this, args); }); if (fn) { if (_this.constructor.$wrapCallback) { fn = _this.constructor.$wrapCallback(fn); } return promise.then( function() { process.nextTick(function() { fn.apply(null, [null].concat($results)); }); }, function(error) { process.nextTick(function() { fn(error); }); }); } return promise; }; })(newName); _this[pointCut].$isWrapped = true; toWrapEl = toWrap[pointCut]; var _len = toWrapEl.pre.length; args; for (j = 0; j < _len; ++j) { args = toWrapEl.pre[j]; args[0] = newName; _this.$pre.apply(_this, args); } _len = toWrapEl.post.length; for (j = 0; j < _len; ++j) { args = toWrapEl.post[j]; args[0] = newName; _this.$post.apply(_this, args); } } return _this; }; /*! * Module exports. */ Document.ValidationError = ValidationError; module.exports = exports = Document;