lib/document.js
'use strict';
/*!
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter
, StorageError = require('./error')
, MixedSchema = require('./schema/mixed')
, ObjectId = require('./types/objectid')
, Schema = require('./schema')
, ValidatorError = require('./schematype').ValidatorError
, Deferred = require('./deferred')
, utils = require('./utils')
, clone = utils.clone
, ValidationError = StorageError.ValidationError
, InternalCache = require('./internal')
, deepEqual = utils.deepEqual
, DocumentArray
, SchemaArray
, Embedded;
/**
* The constructor of the document.
*
* @param {object} data - значения, которые нужно установить
* @param {string|undefined} [collectionName] - коллекция в которой будет находится документ
* @param {Schema} schema - схема по которой будет создан документ
* @param {object} [fields] - выбранные поля в документе (не реализовано)
* @param {Boolean} [init] - hydrate document - наполнить документ данными (используется в api-client)
* @constructor
*/
function Document ( data, collectionName, schema, fields, init ){
if ( !(this instanceof Document) ) {
return new Document( data, collectionName, schema, fields, init );
}
this.$__ = new InternalCache();
this.$__.emitter = new EventEmitter();
this.isNew = true;
// Create empty document with `init` flag.
// new TestDocument(true);
if ( 'boolean' === typeof data ){
init = data;
data = null;
}
if ( collectionName instanceof Schema ){
schema = collectionName;
collectionName = undefined;
}
if ( _.isObject( schema ) && !( schema instanceof Schema )) {
schema = new Schema( schema );
}
// Create empty document by scheme
if ( data instanceof Schema ){
schema = data;
data = null;
if ( schema.options._id ){
data = { _id: new ObjectId() };
}
} else {
// При создании EmbeddedDocument, в нём уже есть схема и ему не нужен _id
schema = this.schema || schema;
// Сгенерировать ObjectId, если он отсутствует, но его требует схема
if ( schema && !this.schema && schema.options._id ){
data = data || {};
if ( data._id === undefined ){
data._id = new ObjectId();
}
}
}
if ( !schema ){
throw new StorageError.MissingSchemaError();
}
// Create document with `init` flag.
// new TestDocument({ test: 'boom' }, true);
if ( 'boolean' === typeof collectionName ){
init = collectionName;
collectionName = undefined;
}
// Create document with `strict: true`
// collection.add({...}, true);
if ('boolean' === typeof fields) {
this.$__.strictMode = fields;
fields = undefined;
} else {
this.$__.strictMode = schema.options && schema.options.strict;
this.$__.selected = fields;
}
this.schema = schema;
if ( collectionName ){
this.collection = window.storage[ collectionName ];
this.collectionName = collectionName;
}
var required = schema.requiredPaths();
for (var i = 0; i < required.length; ++i) {
this.$__.activePaths.require( required[i] );
}
this.$__setSchema( schema );
this.$__.emitter.setMaxListeners(0);
this._doc = this.$__buildDoc( data, init );
if ( init ){
this.init( data );
} else if ( data ) {
this.set( data, undefined, true );
}
// m-gh-2439
// define getters for data.prop properties with non-strict schemas
if ( schema.options.strict === false && data ) {
var self = this
, keys = Object.keys( this._doc );
keys.forEach(function( key ) {
if (!(key in schema.tree)) {
defineProp( self, key, null, self );
}
});
}
// apply methods
for (var i in schema.methods) {
if (typeof schema.methods[i] === 'function') {
this[i] = schema.methods[i];
} else {
(function(_i) {
Object.defineProperty(this, _i, {
get: function() {
var h = {};
for (var k in schema.methods[_i]) {
h[k] = schema.methods[_i][k].bind(this);
}
return h;
}
});
})(i);
}
}
// apply statics
for (var i in schema.statics) {
// use defineProperty so that static props can't be overwritten
Object.defineProperty(this, i, {
value: schema.statics[i],
writable: false
});
}
}
/*!
* Document exposes the NodeJS event emitter API, so you can use
* `on`, `once`, etc.
*/
['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners',
'removeAllListeners', 'addListener'].forEach(function(emitterFn) {
Document.prototype[emitterFn] = function() {
return this.$__.emitter[emitterFn].apply(this.$__.emitter, arguments);
};
});
/**
* The documents schema.
*
* @api public
* @property schema
*/
Document.prototype.schema = null;
/**
* Boolean flag specifying if the document is new.
*
* @api public
* @property isNew
*/
Document.prototype.isNew = null;
/**
* The string version of this documents _id.
*
* ####Note:
*
* This getter exists on all documents by default. The getter can be disabled by setting the `id` [option](/docs/guide.html#id) of its `Schema` to false at construction time.
*
* new Schema({ name: String }, { id: false });
*
* @api public
* @see Schema options /docs/guide.html#options
* @property id
*/
Document.prototype.id = null;
/**
* Hash containing current validation errors.
*
* @api public
* @property errors
*/
Document.prototype.errors = null;
Document.prototype.adapterHooks = {
documentDefineProperty: _.noop,
documentSetInitialValue: _.noop,
documentGetValue: _.noop,
documentSetValue: _.noop
};
/**
* Builds the default doc structure
*
* @param {Object} obj
* @param {Boolean} [skipId]
* @return {Object}
* @api private
* @method $__buildDoc
* @memberOf Document
*/
Document.prototype.$__buildDoc = function ( obj, skipId ) {
var doc = {}
, self = this;
var paths = Object.keys( this.schema.paths )
, plen = paths.length
, ii = 0;
for ( ; ii < plen; ++ii ) {
var p = paths[ii];
if ( '_id' === p ) {
if ( skipId ) continue;
if ( obj && '_id' in obj ) continue;
}
var type = this.schema.paths[ p ];
var path = p.split('.');
var len = path.length;
var last = len - 1;
var doc_ = doc;
var i = 0;
for ( ; i < len; ++i ) {
var piece = path[ i ]
, defaultVal;
if ( i === last ) {
defaultVal = type.getDefault( self, true );
if ('undefined' !== typeof defaultVal ) {
doc_[ piece ] = defaultVal;
self.$__.activePaths.default( p );
}
} else {
doc_ = doc_[ piece ] || ( doc_[ piece ] = {} );
}
}
}
return doc;
};
/**
* Initializes the document without setters or marking anything modified.
*
* Called internally after a document is returned from server.
*
* @param {Object} data document returned by server
* @api private
*/
Document.prototype.init = function ( data ) {
this.isNew = false;
//todo: сдесь всё изменится, смотреть коммент метода this.populated
// handle docs with populated paths
/*!
if ( doc._id && opts && opts.populated && opts.populated.length ) {
var id = String( doc._id );
for (var i = 0; i < opts.populated.length; ++i) {
var item = opts.populated[ i ];
this.populated( item.path, item._docs[id], item );
}
}
*/
init( this, data, this._doc );
return this;
};
/*!
* Init helper.
*
* @param {Object} self document instance
* @param {Object} obj raw server doc
* @param {Object} doc object we are initializing
* @api 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 && _.isPlainObject( obj[ i ] ) &&
(!obj[i].constructor || 'Object' === utils.getFunctionName(obj[i].constructor))) {
// assume nested object
if (!doc[i]) doc[i] = {};
init(self, obj[i], doc[i], path + '.');
} else {
if (obj[i] === null) {
doc[i] = null;
} else if (obj[i] !== undefined) {
if (schema) {
try {
doc[i] = schema.cast(obj[i], self, true);
} catch (e) {
self.invalidate(e.path, new ValidatorError({
path: e.path,
message: e.message,
type: 'cast',
value: e.value
}));
}
} else {
doc[i] = obj[i];
}
self.adapterHooks.documentSetInitialValue.call( self, self, path, doc[i] );
}
// mark as hydrated
if (!self.isModified(path)) {
self.$__.activePaths.init(path);
}
}
}
}
/**
* Sets the value of a path, or many paths.
*
* ####Example:
*
* // path, value
* doc.set(path, value)
*
* // object
* doc.set({
* path : value
* , path2 : {
* path : value
* }
* })
*
* // only-the-fly cast to number
* doc.set(path, value, Number)
*
* // only-the-fly cast to string
* doc.set(path, value, String)
*
* // changing strict mode behavior
* doc.set(path, value, { strict: false });
*
* @param {String|Object} path path or object of key/vals to set
* @param {Mixed} val the value to set
* @param {Schema|String|Number|*} [type] optionally specify a type for "on-the-fly" attributes
* @param {Object} [options] optionally specify options that modify the behavior of the set
* @api public
*/
Document.prototype.set = function (path, val, type, options) {
if (type && 'Object' === utils.getFunctionName(type.constructor)) {
options = type;
type = undefined;
}
var merge = options && options.merge
, adhoc = type && true !== type
, constructing = true === type
, adhocs;
var strict = options && 'strict' in options
? options.strict
: this.$__.strictMode;
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 _temp = path;
path = val;
val = _temp;
} 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];
var pathName = prefix + key;
pathtype = this.schema.pathType(pathName);
if (null != path[key]
// need to know if plain object - no Buffer, ObjectId, ref, etc
&& _.isPlainObject(path[key])
&& ( !path[key].constructor || 'Object' === utils.getFunctionName(path[key].constructor) )
&& 'virtual' !== pathtype
&& 'real' !== pathtype
&& !(this.$__path(pathName) instanceof MixedSchema)
&& !(this.schema.paths[pathName] &&
this.schema.paths[pathName].options &&
this.schema.paths[pathName].options.ref)) {
this.set(path[key], pathName, constructing);
} else if (strict) {
if ('real' === pathtype || 'virtual' === pathtype) {
this.set(prefix + key, path[key], constructing);
} else if ('throw' === strict) {
throw new Error('Field `' + key + '` is not in schema.');
}
} else if (undefined !== path[key]) {
this.set(prefix + key, path[key], constructing);
}
}
return this;
}
}
// ensure _strict is honored for obj props
// docschema = new Schema({ path: { nest: 'string' }})
// doc.set('path', obj);
var pathType = this.schema.pathType(path);
if ('nested' === pathType && val && _.isPlainObject(val) &&
(!val.constructor || 'Object' === utils.getFunctionName(val.constructor))) {
if (!merge) this.setValue(path, null);
this.set(val, path, constructing);
return this;
}
var schema;
var parts = path.split('.');
var subpath;
if ('adhocOrUndefined' === pathType && strict) {
// check for roots that are Mixed types
var mixed;
for (var i = 0; i < parts.length; ++i) {
subpath = parts.slice(0, i+1).join('.');
schema = this.schema.path(subpath);
if (schema instanceof MixedSchema) {
// allow changes to sub paths of mixed types
mixed = true;
break;
}
}
if (!mixed) {
if ('throw' === strict) {
throw new Error('Field `' + path + '` is not in schema.');
}
return this;
}
} else if ('virtual' === pathType) {
schema = this.schema.virtualpath(path);
schema.applySetters(val, this);
return this;
} else {
schema = this.$__path(path);
}
var pathToMark;
// 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 {
for ( i = 0; i < parts.length; ++i ) {
subpath = parts.slice(0, i + 1).join('.');
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 this doc is being constructed we should not trigger getters
var priorVal = constructing
? undefined
: this.getValue(path);
if (!schema || undefined === val) {
this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal);
return this;
}
var shouldSet = true;
try {
// If the user is trying to set a ref path to a document with
// the correct model name, treat it as populated
var didPopulate = false;
if (schema.options &&
schema.options.ref &&
val instanceof Document &&
schema.options.ref === val.schema.name) {
//todo: need test (see https://github.com/Automattic/mongoose/pull/2387/files)
this.populated(path, val._id);
didPopulate = true;
}
val = schema.applySetters(val, this, false, priorVal);
if (!didPopulate && this.$__.populated) {
delete this.$__.populated[path];
}
this.$markValid(path);
} catch (e) {
var reason;
if (!(e instanceof StorageError.CastError)) {
reason = e;
}
this.invalidate(path, new StorageError.CastError(schema.instance, val, path, reason));
shouldSet = false;
}
if (shouldSet) {
this.$__set(pathToMark, path, constructing, parts, schema, val, priorVal);
}
return this;
};
/**
* Determine if we should mark this change as modified.
*
* @return {Boolean}
* @api private
* @method $__shouldModify
* @memberOf Document
*/
Document.prototype.$__shouldModify = function (
pathToMark, path, constructing, parts, schema, val, priorVal) {
if (this.isNew) return true;
if ( undefined === val && !this.isSelected(path) ) {
// when a path is not selected in a query, its initial
// value will be undefined.
return true;
}
if (undefined === val && path in this.$__.activePaths.states.default) {
// we're just unsetting the default value which was never saved
return false;
}
if (!utils.deepEqual(val, priorVal || this.get(path))) {
return true;
}
//тест не проходит из-за наличия лишнего поля в states.default (comments)
// На самом деле поле вроде и не лишнее
//console.info( path, path in this.$__.activePaths.states.default );
//console.log( this.$__.activePaths );
// Когда мы устанавливаем такое же значение как default
// Не понятно зачем мангуст его обновлял
/*!
if (!constructing &&
null != val &&
path in this.$__.activePaths.states.default &&
utils.deepEqual(val, schema.getDefault(this, constructing)) ) {
//console.log( pathToMark, this.$__.activePaths.states.modify );
// a path with a default was $unset on the server
// and the user is setting it to the same value again
return true;
}
*/
return false;
};
/**
* Handles the actual setting of the value and marking the path modified if appropriate.
*
* @api private
* @method $__set
* @memberOf Document
*/
Document.prototype.$__set = function ( pathToMark, path, constructing, parts, schema, val, priorVal ) {
var shouldModify = this.$__shouldModify.apply(this, arguments);
if (shouldModify) {
this.markModified(pathToMark, val);
}
var obj = this._doc
, i = 0
, l = parts.length;
for (; i < l; i++) {
var next = i + 1
, last = next === l;
if ( last ) {
obj[parts[i]] = val;
this.adapterHooks.documentSetValue.call( this, this, path, val );
} else {
if (obj[parts[i]] && 'Object' === utils.getFunctionName(obj[parts[i]].constructor)) {
obj = obj[parts[i]];
} else if (obj[parts[i]] && 'EmbeddedDocument' === utils.getFunctionName(obj[parts[i]].constructor) ) {
obj = obj[parts[i]];
} else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) {
obj = obj[parts[i]];
} else {
obj = obj[parts[i]] = {};
}
}
}
};
/**
* Gets a raw value from a path (no getters)
*
* @param {String} path
* @api private
*/
Document.prototype.getValue = function (path) {
return utils.getValue(path, this._doc);
};
/**
* Sets a raw value for a path (no casting, setters, transformations)
*
* @param {String} path
* @param {Object} value
* @api private
*/
Document.prototype.setValue = function (path, value) {
utils.setValue(path, value, this._doc);
return this;
};
/**
* Returns the value of a path.
*
* ####Example
*
* // path
* doc.get('age') // 47
*
* // dynamic casting to a string
* doc.get('age', String) // "47"
*
* @param {String} path
* @param {Schema|String|Number} [type] optionally specify a type for on-the-fly attributes
* @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 = undefined === obj || null === obj
? undefined
: obj[pieces[i]];
}
if (schema) {
obj = schema.applyGetters(obj, this);
}
this.adapterHooks.documentGetValue.call( this, this, path );
return obj;
};
/**
* Returns the schematype for the given `path`.
*
* @param {String} path
* @api private
* @method $__path
* @memberOf Document
*/
Document.prototype.$__path = function (path) {
var adhocs = this.$__.adhocPaths
, adhocType = adhocs && adhocs[path];
if (adhocType) {
return adhocType;
} else {
return this.schema.path(path);
}
};
/**
* Marks the path as having pending changes to write to the db.
*
* _Very helpful when using [Mixed](./schematypes.html#mixed) types._
*
* ####Example:
*
* doc.mixed.type = 'changed';
* doc.markModified('mixed.type');
* doc.save() // changes to mixed.type are now persisted
*
* @param {String} path the path to mark modified
* @api public
*/
Document.prototype.markModified = function (path) {
this.$__.activePaths.modify(path);
};
/**
* Returns the list of paths that have been modified.
*
* @return {Array}
* @api public
*/
Document.prototype.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('.'));
}, []));
}, []);
};
/**
* Returns true if this document was modified, else false.
*
* If `path` is given, checks if a path or any full path containing `path` as part of its path chain has been modified.
*
* ####Example
*
* doc.set('documents.0.title', 'changed');
* doc.isModified() // true
* doc.isModified('documents') // true
* doc.isModified('documents.0.title') // true
* doc.isDirectModified('documents') // false
*
* @param {String} [path] optional
* @return {Boolean}
* @api public
*/
Document.prototype.isModified = function (path) {
return path
? !!~this.modifiedPaths().indexOf(path)
: this.$__.activePaths.some('modify');
};
/**
* Returns true if `path` was directly set and modified, else false.
*
* ####Example
*
* doc.set('documents.0.title', 'changed');
* doc.isDirectModified('documents.0.title') // true
* doc.isDirectModified('documents') // false
*
* @param {String} path
* @return {Boolean}
* @api public
*/
Document.prototype.isDirectModified = function (path) {
return (path in this.$__.activePaths.states.modify);
};
/**
* Checks if `path` was initialized.
*
* @param {String} path
* @return {Boolean}
* @api public
*/
Document.prototype.isInit = function (path) {
return (path in this.$__.activePaths.states.init);
};
/**
* Checks if `path` was selected in the source query which initialized this document.
*
* ####Example
*
* Thing.findOne().select('name').exec(function (err, doc) {
* doc.isSelected('name') // true
* doc.isSelected('age') // false
* })
*
* @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;
if (1 === i && '_id' === paths[0]) {
// only _id was selected.
return 0 === this.$__.selected._id;
}
while (i--) {
cur = paths[i];
if ('_id' === cur) continue;
inclusive = !! this.$__.selected[cur];
break;
}
if (path in this.$__.selected) {
return inclusive;
}
i = paths.length;
var pathDot = path + '.';
while (i--) {
cur = paths[i];
if ('_id' === cur) continue;
if (0 === cur.indexOf(pathDot)) {
return inclusive;
}
if (0 === pathDot.indexOf(cur + '.')) {
return inclusive;
}
}
return ! inclusive;
}
return true;
};
/**
* Executes registered validation rules for this document.
*
* ####Note:
*
* This method is called `pre` save and if a validation rule is violated, [save](#model_Model-save) is aborted and the error is returned to your `callback`.
*
* ####Example:
*
* doc.validate(function (err) {
* if (err) handleError(err);
* else // validation passed
* });
*
* @param {Function} cb called after validation completes, passing an error if one occurred
* @api public
*/
Document.prototype.validate = function (cb) {
var self = this;
// only validate required fields when necessary
var paths = Object.keys(this.$__.activePaths.states.require).filter(function (path) {
if (!self.isSelected(path) && !self.isModified(path)) return false;
return true;
});
paths = paths.concat(Object.keys(this.$__.activePaths.states.init));
paths = paths.concat(Object.keys(this.$__.activePaths.states.modify));
paths = paths.concat(Object.keys(this.$__.activePaths.states.default));
if (0 === paths.length) {
complete();
return this;
}
var validating = {}
, total = 0;
// gh-661: if a whole array is modified, make sure to run validation on all
// the children as well
for (var i = 0; i < paths.length; ++i) {
var path = paths[i];
var val = self.getValue(path);
if (val instanceof Array && !Buffer.isBuffer(val) &&
!val.isStorageDocumentArray) {
var numElements = val.length;
for (var j = 0; j < numElements; ++j) {
paths.push(path + '.' + j);
}
}
}
paths.forEach(validatePath);
return this;
function validatePath (path) {
if (validating[path]) return;
validating[path] = true;
total++;
utils.setImmediate(function(){
var p = self.schema.path(path);
if (!p) return --total || complete();
var val = self.getValue(path);
p.doValidate(val, function (err) {
if (err) {
self.invalidate(
path
, err
, undefined
//, true // embedded docs
);
}
--total || complete();
}, self);
});
}
function complete () {
var err = self.$__.validationError;
self.$__.validationError = undefined;
cb && cb(err);
}
};
/**
* Executes registered validation rules (skipping asynchronous validators) for this document.
*
* ####Note:
*
* This method is useful if you need synchronous validation.
*
* ####Example:
*
* var err = doc.validateSync();
* if ( err ){
* handleError( err );
* } else {
* // validation passed
* }
*
* @return {StorageError|undefined} StorageError if there are errors during validation, or undefined if there is no error.
* @api public
*/
Document.prototype.validateSync = function () {
var self = this;
// only validate required fields when necessary
var paths = Object.keys(this.$__.activePaths.states.require).filter(function (path) {
if (!self.isSelected(path) && !self.isModified(path)) return false;
return true;
});
paths = paths.concat(Object.keys(this.$__.activePaths.states.init));
paths = paths.concat(Object.keys(this.$__.activePaths.states.modify));
paths = paths.concat(Object.keys(this.$__.activePaths.states.default));
var validating = {};
// gh-661: if a whole array is modified, make sure to run validation on all
// the children as well
for (var i = 0; i < paths.length; ++i) {
var path = paths[i];
var val = self.getValue(path);
if (val instanceof Array && !Buffer.isBuffer(val) &&
!val.isStorageDocumentArray) {
var numElements = val.length;
for (var j = 0; j < numElements; ++j) {
paths.push(path + '.' + j);
}
}
}
paths.forEach(function (path) {
if (validating[path]) return;
validating[path] = true;
var p = self.schema.path(path);
if (!p) return;
if (!self.$isValid(path)) {
return;
}
var val = self.getValue(path);
var err = p.doValidateSync(val, self);
if (err) {
self.invalidate(path, err, undefined);
}
});
var err = self.$__.validationError;
self.$__.validationError = undefined;
self.emit('validate', self);
if (err) {
for (var key in err.errors) {
// Make sure cast errors persist
if (err.errors[key] instanceof StorageError.CastError) {
self.invalidate(key, err.errors[key]);
}
}
}
return err;
};
/**
* Marks a path as invalid, causing validation to fail.
*
* The `errorMsg` argument will become the message of the `ValidationError`.
*
* The `value` argument (if passed) will be available through the `ValidationError.value` property.
*
* doc.invalidate('size', 'must be less than 20', 14);
* doc.validate(function (err) {
* console.log(err)
* // prints
* { message: 'Validation failed',
* name: 'ValidationError',
* errors:
* { size:
* { message: 'must be less than 20',
* name: 'ValidatorError',
* path: 'size',
* type: 'user defined',
* value: 14 } } }
* })
*
* @param {String} path the field to invalidate
* @param {String|StorageError|Error} errorMsg the error which states the reason `path` was invalid
* @param {Object|String|Number|any} value optional invalid value
* @api public
*/
Document.prototype.invalidate = function (path, errorMsg, value) {
if (!this.$__.validationError) {
this.$__.validationError = new ValidationError(this);
}
if (this.$__.validationError.errors[path]) return;
if (!errorMsg || 'string' === typeof errorMsg) {
errorMsg = new ValidatorError({
path: path,
message: errorMsg,
type: 'user defined',
value: value
});
}
if (this.$__.validationError == errorMsg) return;
this.$__.validationError.errors[path] = errorMsg;
};
/**
* Marks a path as valid, removing existing validation errors.
*
* @param {String} path the field to mark as valid
* @api private
* @method $markValid
* @receiver Document
*/
Document.prototype.$markValid = function(path) {
if (!this.$__.validationError || !this.$__.validationError.errors[path]) {
return;
}
delete this.$__.validationError.errors[path];
if (Object.keys(this.$__.validationError.errors).length === 0) {
this.$__.validationError = null;
}
};
/**
* Checks if a path is invalid
*
* @param {String} path the field to check
* @method $isValid
* @api private
* @receiver Document
*/
Document.prototype.$isValid = function(path) {
return !this.$__.validationError || !this.$__.validationError.errors[path];
};
/**
* Resets the internal modified state of this document.
*
* @api private
* @return {Document}
* @method $__reset
* @memberOf Document
*/
Document.prototype.$__reset = function reset () {
var self = this;
this.$__.activePaths
.map('init', 'modify', function (i) {
return self.getValue(i);
})
.filter(function (val) {
return val && val.isStorageDocumentArray && val.length;
})
.forEach(function (array) {
var i = array.length;
while (i--) {
var doc = array[i];
if (!doc) continue;
doc.$__reset();
}
});
// Clear 'modify'('dirty') cache
this.$__.activePaths.clear('modify');
this.$__.validationError = undefined;
this.errors = undefined;
//console.log( self.$__.activePaths.states.require );
//TODO: тут
this.schema.requiredPaths().forEach(function (path) {
self.$__.activePaths.require(path);
});
return this;
};
/**
* Returns this documents dirty paths / vals.
*
* @api private
* @method $__dirty
* @memberOf Document
*/
Document.prototype.$__dirty = function () {
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 = []
, lastPath
, top;
all.forEach(function( item ){
lastPath = item.path + '.';
minimal.push(item);
top = item;
});
top = lastPath = null;
return minimal;
};
/*!
* Compiles schemas.
* (установить геттеры/сеттеры на поля документа)
*/
function compile (self, tree, proto, prefix) {
var keys = Object.keys(tree)
, i = keys.length
, limb
, key;
while (i--) {
key = keys[i];
limb = tree[key];
defineProp(self
, key
, (('Object' === utils.getFunctionName(limb.constructor)
&& Object.keys(limb).length)
&& (!limb.type || limb.type.type)
? limb
: null)
, proto
, prefix
, keys);
}
}
// gets descriptors for all properties of `object`
// makes all properties non-enumerable to match previous behavior to #2211
function getOwnPropertyDescriptors(object) {
var result = {};
Object.getOwnPropertyNames(object).forEach(function(key) {
result[key] = Object.getOwnPropertyDescriptor(object, key);
result[key].enumerable = false;
});
return result;
}
/*!
* Defines the accessor named prop on the incoming prototype.
* там же, поля документа сделаем наблюдаемыми
*/
function defineProp (self, prop, subprops, prototype, prefix, keys) {
prefix = prefix || '';
var path = (prefix ? prefix + '.' : '') + prop;
if (subprops) {
Object.defineProperty(prototype, prop, {
enumerable: true
, configurable: true
, get: function () {
if (!this.$__.getters)
this.$__.getters = {};
if (!this.$__.getters[path]) {
var nested = Object.create(Object.getPrototypeOf(this), getOwnPropertyDescriptors(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);
};
nested.toJSON = nested.toObject;
nested.$__isNested = true;
compile( self, subprops, nested, path );
this.$__.getters[path] = nested;
}
return this.$__.getters[path];
}
, set: function (v) {
if (v instanceof Document) v = v.toObject();
return (this.$__.scope || this).set( path, v );
}
});
} else {
Object.defineProperty( prototype, prop, {
enumerable: true
, configurable: true
, get: function ( ) { return this.get.call(this.$__.scope || this, path); }
, set: function (v) { return this.set.call(this.$__.scope || this, path, v); }
});
}
self.adapterHooks.documentDefineProperty.call( self, self, prototype, prop, prefix, path );
//self.adapterHooks.documentDefineProperty.call( self, self, path, prototype );
}
/**
* Assigns/compiles `schema` into this documents prototype.
*
* @param {Schema} schema
* @api private
* @method $__setSchema
* @memberOf Document
*/
Document.prototype.$__setSchema = function ( schema ) {
this.schema = schema;
compile( this, schema.tree, this );
};
/**
* Get all subdocs (by bfs)
*
* @api private
* @method $__getAllSubdocs
* @memberOf Document
*/
Document.prototype.$__getAllSubdocs = function () {
DocumentArray || (DocumentArray = require('./types/documentarray'));
Embedded = Embedded || require('./types/embedded');
function docReducer(seed, path) {
var val = this[path];
if (val instanceof Embedded) seed.push(val);
if (val instanceof DocumentArray){
val.forEach(function _docReduce(doc) {
if (!doc || !doc._doc) return;
if (doc instanceof Embedded) seed.push(doc);
seed = Object.keys(doc._doc).reduce(docReducer.bind(doc._doc), seed);
});
}
return seed;
}
return Object.keys(this._doc).reduce(docReducer.bind(this), []);
};
/**
* Handle generic save stuff.
* to solve #1446 use use hierarchy instead of hooks
*
* @api private
* @method $__presaveValidate
* @memberOf Document
*/
Document.prototype.$__presaveValidate = function $__presaveValidate() {
// if any doc.set() calls failed
var docs = this.$__getArrayPathsToValidate();
var e2 = docs.map(function (doc) {
return doc.$__presaveValidate();
});
var e1 = [this.$__.saveError].concat(e2);
var err = e1.filter(function (x) {return x})[0];
this.$__.saveError = null;
return err;
};
/**
* Get active path that were changed and are arrays
*
* @api private
* @method $__getArrayPathsToValidate
* @memberOf Document
*/
Document.prototype.$__getArrayPathsToValidate = function () {
DocumentArray || (DocumentArray = require('./types/documentarray'));
// validate all document arrays.
return this.$__.activePaths
.map('init', 'modify', function (i) {
return this.getValue(i);
}.bind(this))
.filter(function (val) {
return val && val instanceof DocumentArray && val.length;
}).reduce(function(seed, array) {
return seed.concat(array);
}, [])
.filter(function (doc) {return doc});
};
/**
* Registers an error
*
* @param {Error} err
* @api private
* @method $__error
* @memberOf Document
*/
Document.prototype.$__error = function (err) {
this.$__.saveError = err;
return this;
};
/**
* Produces a special query document of the modified properties used in updates.
*
* @api private
* @method $__delta
* @memberOf Document
*/
Document.prototype.$__delta = function () {
var dirty = this.$__dirty();
var delta = {}
, len = dirty.length
, d = 0;
for (; d < len; ++d) {
var data = dirty[ d ];
var value = data.value;
value = utils.clone(value, { depopulate: 1 });
delta[ data.path ] = value;
}
return delta;
};
Document.prototype.$__handleSave = function(){
// Получаем ресурс коллекции, куда будем сохранять данные
var resource;
if ( this.collection ){
resource = this.collection.api;
}
var innerPromise = new Deferred();
if ( this.isNew ) {
// send entire doc
var toObjectOptions = {};
if (this.schema.options.toObject &&
this.schema.options.toObject.retainKeyOrder) {
toObjectOptions.retainKeyOrder = true;
}
toObjectOptions.depopulate = 1;
toObjectOptions._skipDepopulateTopLevel = true;
toObjectOptions.transform = false;
var obj = this.toObject(toObjectOptions);
if ( ( obj || {} ).hasOwnProperty('_id') === false ) {
// documents must have an _id else mongoose won't know
// what to update later if more changes are made. the user
// wouldn't know what _id was generated by mongodb either
// nor would the ObjectId generated my mongodb necessarily
// match the schema definition.
innerPromise.reject(new Error('document must have an _id before saving'));
return innerPromise;
}
// Без ресурса можно просто делать валидацию (подготовить данные к отправке), даже если нет коллекции
if ( !resource ){
innerPromise.resolve( obj );
} else {
resource.create( obj ).always( innerPromise.resolve );
}
this.$__reset();
this.isNew = false;
this.emit('isNew', false);
// Make it possible to retry the insert
this.$__.inserting = true;
} else {
// Make sure we don't treat it as a new object on error,
// since it already exists
this.$__.inserting = false;
var delta = this.$__delta();
if ( !_.isEmpty( delta ) ) {
this.$__reset();
// Без ресурса можно просто делать валидацию (подготовить данные к отправке), даже если нет коллекции
if ( !resource ){
innerPromise.resolve( delta );
} else {
resource( this.id ).update( delta ).always( innerPromise.resolve );
}
} else {
this.$__reset();
innerPromise.resolve( this );
}
this.emit('isNew', false);
}
return innerPromise;
};
/**
* @description Saves this document.
*
* Если апи-клиента нет и документ новый, то в колбэке будет plain object со всеми данными для сохранения на сервер.
* Если апи-клиента нет и документ старое, то в колбэке будет plain object только с изменёнными данными.
*
* Если апи-клиент есть и не важно новый документ или старый, в колбэке всегда будет ответ от rest-api-client
*
* // todo: доописать это дело
* Сейчас если есть ресурс (апи клиент), то:
* если документ новый, то после ответа создастся новый документ на основе ответа, и обовляется!!! (получше объяснить это) ссылка (id) внутри коллекции
* если документ старый, то после ответа ищется этот документ по id и делается set
*
*
* @example:
*
* product.sold = Date.now();
* product.save(function (err, product, numberAffected) {
* if (err) ..
* })
*
* @description The callback will receive three parameters, `err` if an error occurred, `product` which is the saved `product`, and `numberAffected` which will be 1 when the document was found and updated in the database, otherwise 0.
*
* The `fn` callback is optional. If no `fn` is passed and validation fails, the validation error will be emitted on the connection used to create this model.
* @example:
* var schema = new Schema(..);
* var Product = storage.createCollection('Product', schema );
* var doc = Product.add();
*
* // todo: реализовать это
* doc.on('error', handleError);
*
* @description As an extra measure of flow control, save will return a Promise (bound to `fn` if passed) so it could be chained, or hook to recive errors
* @example:
* product.save().done(function( product ){
* ...
* }).fail(function( err ){
* assert.ok( err )
* })
*
* @description retainKeyOrder - keep the key order of the doc save
* @example:
* var Checkin = new Schema({
* date: Date,
* location: {
* lat: Number,
* lng: Number
* }
* }, {
* toObject: {
* retainKeyOrder: true
* }
* });
* var Checkins = storage.createCollection('Product', schema );
* var doc = Checkins.add();
*
* doc.save().done(function( objToSave ){
* // in `objToSave` followed the correct order of the keys of doc
* });
*
* @param {function( object )} [done] optional callback, object - objToSave
* @return {Deferred} Deferred
* @api public
* @see middleware http://mongoosejs.com/docs/middleware.html
*/
Document.prototype.save = function ( done ) {
var self = this;
var finalPromise = new Deferred().done( done );
// Сохранять документ можно только если он находится в коллекции
if ( !this.collection ){
finalPromise.reject( arguments );
console.error('Document.save api handle is not implemented.');
return finalPromise;
}
// Check for preSave errors (точно знаю, что она проверяет ошибки в массивах (CastError))
var preSaveErr = self.$__presaveValidate();
if ( preSaveErr ) {
finalPromise.reject( preSaveErr );
return finalPromise;
}
// Validate
var p0 = new Deferred();
self.validate(function( err ){
if ( err ){
p0.reject( err );
finalPromise.reject( err );
} else {
p0.resolve();
}
});
// Сначала надо сохранить все поддокументы и сделать resolve!!!
// (тут псевдосохранение смотреть EmbeddedDocument.prototype.save )
// Call save hooks on subdocs
var subDocs = self.$__getAllSubdocs();
var whenCond = subDocs.map(function (d) {return d.save();});
whenCond.push( p0 );
// Так мы передаём массив promise условий
var p1 = Deferred.when.apply( Deferred, whenCond );
p1.fail(function ( err ) {
// If the initial insert fails provide a second chance.
// (If we did this all the time we would break updates)
if (self.$__.inserting) {
self.isNew = true;
self.emit('isNew', true);
}
finalPromise.reject( err );
});
// Handle save and results
p1.done(function(){
self.$__handleSave().done(function(){
self.emit('save', self);
//todo: надо проверять, нужно ли писать проверку на наличие ресурса, если он есть - отдавать self, если нет, отдавать как сейчас написано
// возможно и скорее всего, api и так отдаёт всё в правильном порядке (doc, meta, jqxhr)
finalPromise.resolve.apply( finalPromise, arguments );
}).fail(function(){
finalPromise.reject.apply( finalPromise, arguments );
});
});
return finalPromise;
};
/**
* Internal helper for toObject() and toJSON() that doesn't manipulate options
*
* @api private
* @method $toObject
* @memberOf Document
*/
Document.prototype.$toObject = function(options, json) {
var defaultOptions = { transform: true, json: json };
if (options && options.depopulate && !options._skipDepopulateTopLevel && this.$__.wasPopulated) {
// populated paths that we set to a document
return clone(this._id, options);
}
// If we're calling toObject on a populated doc, we may want to skip
// depopulated on the top level
if (options && options._skipDepopulateTopLevel) {
options._skipDepopulateTopLevel = false;
}
// When internally saving this document we always pass options,
// bypassing the custom schema options.
if (!(options && 'Object' == utils.getFunctionName(options.constructor)) ||
(options && options._useSchemaOptions)) {
if (json) {
options = this.schema.options.toJSON ?
clone(this.schema.options.toJSON) :
{};
options.json = true;
options._useSchemaOptions = true;
} else {
options = this.schema.options.toObject ?
clone(this.schema.options.toObject) :
{};
options.json = false;
options._useSchemaOptions = true;
}
}
for (var key in defaultOptions) {
if (options[key] === undefined) {
options[key] = defaultOptions[key];
}
}
('minimize' in options) || (options.minimize = this.schema.options.minimize);
// remember the root transform function
// to save it from being overwritten by sub-transform functions
var originalTransform = options.transform;
var ret = clone(this._doc, options) || {};
if (options.virtuals || options.getters && false !== options.virtuals) {
applyGetters(this, ret, 'virtuals', options);
}
if (options.getters) {
applyGetters(this, ret, 'paths', options);
// applyGetters for paths will add nested empty objects;
// if minimize is set, we need to remove them.
if (options.minimize) {
ret = minimize(ret) || {};
}
}
if (options.versionKey === false && this.schema.options.versionKey) {
delete ret[this.schema.options.versionKey];
}
var transform = options.transform;
// In the case where a subdocument has its own transform function, we need to
// check and see if the parent has a transform (options.transform) and if the
// child schema has a transform (this.schema.options.toObject) In this case,
// we need to adjust options.transform to be the child schema's transform and
// not the parent schema's
if (true === transform ||
(this.schema.options.toObject && transform)) {
var opts = options.json ? this.schema.options.toJSON : this.schema.options.toObject;
if (opts) {
transform = (typeof options.transform === 'function' ? options.transform : opts.transform);
}
} else {
options.transform = originalTransform;
}
if ('function' == typeof transform) {
var xformed = transform(this, ret, options);
if ('undefined' != typeof xformed) ret = xformed;
}
return ret;
};
/**
* Converts this document into a plain javascript object, ready for storage in MongoDB.
*
* Buffers are converted to instances of [mongodb.Binary](http://mongodb.github.com/node-mongodb-native/api-bson-generated/binary.html) for proper storage.
*
* ####Options:
*
* - `getters` apply all getters (path and virtual getters)
* - `virtuals` apply virtual getters (can override `getters` option)
* - `minimize` remove empty objects (defaults to true)
* - `transform` a transform function to apply to the resulting document before returning
*
* ####Getters/Virtuals
*
* 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 })
*
* To apply these options to every document of your schema by default, set your [schemas](#schema_Schema) `toObject` option to the same argument.
*
* schema.set('toObject', { virtuals: true })
*
* ####Transform
*
* We may need to perform a transformation of the resulting object based on some criteria, say to remove some sensitive information or return a custom object. In this case we set the optional `transform` function.
*
* Transform functions receive three arguments
*
* function (doc, ret, options) {}
*
* - `doc` The mongoose document which is being converted
* - `ret` The plain object representation which has been converted
* - `options` The options in use (either schema options or the options passed inline)
*
* ####Example
*
* // specify the transform schema option
* if (!schema.options.toObject) schema.options.toObject = {};
* schema.options.toObject.transform = function (doc, ret, options) {
* // remove the _id of every document before returning the result
* delete ret._id;
* }
*
* // without the transformation in the schema
* doc.toObject(); // { _id: 'anId', name: 'Wreck-it Ralph' }
*
* // with the transformation
* doc.toObject(); // { name: 'Wreck-it Ralph' }
*
* With transformations we can do a lot more than remove properties. We can even return completely new customized objects:
*
* if (!schema.options.toObject) schema.options.toObject = {};
* schema.options.toObject.transform = function (doc, ret, options) {
* return { movie: ret.name }
* }
*
* // without the transformation in the schema
* doc.toObject(); // { _id: 'anId', name: 'Wreck-it Ralph' }
*
* // with the transformation
* doc.toObject(); // { movie: 'Wreck-it Ralph' }
*
* _Note: if a transform function returns `undefined`, the return value will be ignored._
*
* Transformations may also be applied inline, overridding any transform set in the options:
*
* function xform (doc, ret, options) {
* return { inline: ret.name, custom: true }
* }
*
* // pass the transform as an inline option
* doc.toObject({ transform: xform }); // { inline: 'Wreck-it Ralph', custom: true }
*
* _Note: if you call `toObject` and pass any options, the transform declared in your schema options will __not__ be applied. To force its application pass `transform: true`_
*
* if (!schema.options.toObject) schema.options.toObject = {};
* schema.options.toObject.hide = '_id';
* schema.options.toObject.transform = function (doc, ret, options) {
* if (options.hide) {
* options.hide.split(' ').forEach(function (prop) {
* delete ret[prop];
* });
* }
* }
*
* var doc = new Doc({ _id: 'anId', secret: 47, name: 'Wreck-it Ralph' });
* doc.toObject(); // { secret: 47, name: 'Wreck-it Ralph' }
* doc.toObject({ hide: 'secret _id' }); // { _id: 'anId', secret: 47, name: 'Wreck-it Ralph' }
* doc.toObject({ hide: 'secret _id', transform: true }); // { name: 'Wreck-it Ralph' }
*
* Transforms are applied to the document _and each of its sub-documents_. To determine whether or not you are currently operating on a sub-document you might use the following guard:
*
* if ('function' == typeof doc.ownerDocument) {
* // working with a sub doc
* }
*
* Transforms, like all of these options, are also available for `toJSON`.
*
* See [schema options](/docs/guide.html#toObject) for some more details.
*
* _During save, no custom options are applied to the document before being sent to the database._
*
* retainKeyOrder - keep the key order of the doc save
*
* var Checkin = new Schema({ ... }, {
* toObject: {
* retainKeyOrder: true
* }
* });
*
* doc.toObject(); // object with correct order of the keys of doc
*
* // or inline
*
* doc.toObject({ retainKeyOrder: true });
*
* // or if use toJSON();
*
* var Checkin = new Schema({ ... }, {
* toJSON: {
* retainKeyOrder: true
* }
* });
*
* doc.toJSON(); // JSON string with correct order of the keys of doc
*
* // or inline
*
* doc.toJSON({ retainKeyOrder: true });
*
* @param {Object} [options]
* @return {Object} js object
* @api public
*/
Document.prototype.toObject = function (options) {
return this.$toObject(options);
};
/*!
* Minimizes an object, removing undefined values and empty objects
*
* @param {Object} object to minimize
* @return {Object}
*/
function minimize (obj) {
var keys = Object.keys(obj)
, i = keys.length
, hasKeys
, key
, val;
while (i--) {
key = keys[i];
val = obj[key];
if ( _.isPlainObject(val) ) {
obj[key] = minimize(val);
}
if (undefined === obj[key]) {
delete obj[key];
continue;
}
hasKeys = true;
}
return hasKeys
? obj
: undefined;
}
/*!
* Applies virtuals properties to `json`.
*
* @param {Document} self
* @param {Object} json
* @param {String} type either `virtuals` or `paths`
* @return {Object} `json`
*/
function applyGetters (self, json, type, options) {
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] = utils.clone(self.get(path), options);
} else {
branch = branch[part] || (branch[part] = {});
}
}
}
return json;
}
/**
* The return value of this method is used in calls to JSON.stringify(doc).
*
* This method accepts the same options as [Document#toObject](#document_Document-toObject). To apply the options to every document of your schema by default, set your [schemas](#schema_Schema) `toJSON` option to the same argument.
*
* schema.set('toJSON', { virtuals: true })
*
* See [schema options](/docs/guide.html#toJSON) for details.
*
* @param {Object} options
* @return {Object}
* @see Document#toObject #document_Document-toObject
* @api public
*/
Document.prototype.toJSON = function (options) {
return this.$toObject(options, true);
};
/**
* Returns true if the Document stores the same data as doc.
*
* Documents are considered equal when they have matching `_id`s, unless neither
* document has an `_id`, in which case this function falls back to using
* `deepEqual()`.
*
* @param {Document} doc a document to compare
* @return {Boolean}
* @api public
*/
Document.prototype.equals = function( doc ){
var tid = this.get('_id');
var docid = doc.get('_id');
if (!tid && !docid) {
return deepEqual(this, doc);
}
return tid && tid.equals
? (docid ? tid.equals(docid) : false)
: tid === docid;
};
/**
* Gets _id(s) used during population of the given `path`.
*
* ####Example:
*
* Model.findOne().populate('author').exec(function (err, doc) {
* console.log(doc.author.name) // Dr.Seuss
* console.log(doc.populated('author')) // '5144cf8050f071d979c118a7'
* })
*
* If the path was not populated, undefined is returned.
*
* @param {String} path
* @return {Array|ObjectId|Number|Buffer|String|undefined}
* @api public
*/
Document.prototype.populated = function( path, val, options ){
// val and options are internal
//TODO: доделать эту проверку, она должна опираться не на $__.populated, а на то, что наш объект имеет родителя
// и потом уже выставлять свойство populated == true
if (null == val) {
if (!this.$__.populated) return undefined;
var v = this.$__.populated[path];
if (v) return v.value;
return undefined;
}
// internal
if (true === val) {
if (!this.$__.populated) return undefined;
return this.$__.populated[path];
}
this.$__.populated || (this.$__.populated = {});
this.$__.populated[path] = { value: val, options: options };
return val;
};
/**
* Returns the full path to this document.
*
* @param {String} [path]
* @return {String}
* @api private
* @method $__fullPath
* @memberOf Document
*/
Document.prototype.$__fullPath = function (path) {
// overridden in SubDocuments
return path || '';
};
/**
* Удалить документ и вернуть коллекцию.
*
* @example
* document.remove();
*
* @see Collection.remove
* @returns {boolean}
*/
Document.prototype.remove = function(){
if ( this.collection ){
return this.collection.remove( this );
}
return delete this;
};
/**
* Очищает документ (выставляет значение по умолчанию или undefined)
*/
Document.prototype.empty = function(){
var doc = this
, self = this
, paths = Object.keys( this.schema.paths )
, plen = paths.length
, ii = 0;
for ( ; ii < plen; ++ii ) {
var p = paths[ii];
if ( '_id' == p ) continue;
var 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 ]
, defaultVal;
if ( i === last ) {
defaultVal = type.getDefault( self, true );
doc_[ piece ] = defaultVal || undefined;
self.$__.activePaths.default( p );
} else {
doc_ = doc_[ piece ] || ( doc_[ piece ] = {} );
}
}
}
};
/*!
* Module exports.
*/
Document.ValidationError = ValidationError;
module.exports = Document;