mongoose
Version:
Mongoose MongoDB ORM
844 lines (693 loc) • 18.6 kB
JavaScript
/**
* 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
, inspect = require('util').inspect
, StateMachine = require('./statemachine')
, ActiveRoster = StateMachine.ctor('require', 'modify', 'init')
, deepEqual = utils.deepEqual
, hooks = require('hooks');
/**
* Document constructor.
*
* @param {Object} values to set
* @api private
*/
function Document (obj) {
this._doc = this.buildDoc();
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);
this.registerHooks();
this.doQueue();
this.errors = 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 () {
var doc = {}
, self = this;
this.schema.eachPath(function (i, type) {
var path = i.split('.')
, len = path.length;
path.reduce( function (ref, piece, i) {
var _default;
if (i === len-1) {
_default = type.getDefault(self);
if ('undefined' !== typeof _default)
ref[piece] = _default;
} else
return ref[piece] || (ref[piece] = {});
}, doc);
});
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) {
var self = this;
this.isNew = false;
function init (obj, doc, prefix) {
prefix = prefix || '';
var keys = Object.keys(obj)
, len = keys.length
, i;
while (len--) {
i = keys[len];
var path = prefix + i
, schema = self.schema.path(path);
if (!schema && obj[i] && obj[i].constructor == Object) {
// assume nested object
doc[i] = {};
init(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);
}
}
};
init(doc, self._doc);
this.emit('init');
if (fn)
fn(null);
return this;
};
// 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 adhocs;
if (type) {
adhocs = this._adhocPaths || (this._adhocPaths = {});
adhocs[path] = Schema.interpretAsType(path, type);
}
if ('string' !== typeof path) {
if (null === path || undefined === path)
return this.set(val, path);
var prefix = val
? val + '.'
: '';
if (path instanceof Document) {
path = path._doc;
}
var keys = Object.keys(path);
var i = keys.length;
var key;
while (i--) {
key = keys[i];
if (!(this._path(prefix + key) instanceof MixedSchema)
&& undefined !== path[key]
&& null !== path[key]
&& Object == path[key].constructor) {
this.set(path[key], prefix + key);
} else if (undefined !== path[key]) {
this.set(prefix + key, path[key]);
}
}
return this;
}
var schema = this._path(path)
, parts = path.split('.')
, obj = this._doc
, self = this;
if (this.schema.pathType(path) === 'virtual') {
schema = this.schema.virtualpath(path);
schema.applySetters(val, this);
return this;
}
// When using the $set operator the path to the field must already exist.
// Else mongodb throws: "LEFT_SUBFIELD only supports Object"
var pathToMark
, subpaths
, subpath;
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;
}
var markedModified = this.isDirectModified(pathToMark);
if ((!schema || null === val || undefined === val) ||
this.try(function(){
val = schema.applySetters(schema.cast(val, self), self);
})) {
var priorVal = this.get(path);
if (!markedModified && !deepEqual(val, priorVal)) {
this._activePaths.modify(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 {
// TODO - does this handle obj[parts[i]] === 0 ?
if (obj[parts[i]] && obj[parts[i]].constructor === Object) {
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
*
* @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
* @param {Object} obj
* @api private
*/
Document.prototype._path = function (path, obj) {
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;
};
/**
* 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
*/
Document.prototype.isModified = function (path) {
var directModifiedPaths = Object.keys(this._activePaths.states.modify);
var allPossibleChains = 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('.'));
}, []));
}, []);
return !!~allPossibleChains.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
*/
Document.prototype.isDirectModified = function (path) {
return (path in this._activePaths.states.modify);
};
/**
* Checks if a certain path was initialized
*
* @param {String} path
*/
Document.prototype.isInit = function (path) {
return (path in this._activePaths.states.init);
};
/**
* Validation middleware
*
* @param {Function} next
* @api private
*/
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;
}
/**
* 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
, ((limb.constructor == Object
&& 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 () {
var self = this;
if (!this.save) return;
this.pre('save', function checkForExistingErrors (next) {
if (self._saveError){
next(self._saveError);
self._saveError = null;
} else {
next();
}
}, function (err) {
this.db.emit('error', err);
}).pre('save', function validation (next) {
return self.validate.call(self, next);
});
};
/**
* Registers an error
* TODO: handle multiple
*
* @param {Error} error
* @api private
*/
Document.prototype.error = function (err) {
this._saveError = err;
return this;
};
/**
* Executes methods queued from the Schema definition
*
* @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
*
* @todo Should we apply getters?
* @return {Object} plain object
* @api public
*/
Document.prototype.toObject = function (ref) {
if (ref) return this._doc;
return clone(this._doc, true);
};
/**
* Returns a JSON string for the document
*
* @return {String} JSON representation
* @api public
*/
Document.prototype.toJSON = function () {
return this.toObject();
};
/**
* Helper for console.log
*
* @api public
*/
Document.prototype.inspect = function () {
return inspect(this.toObject(true));
};
/**
* 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;