You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
10 KiB
445 lines
10 KiB
/* eslint no-func-assign: 1 */
|
|
|
|
/*!
|
|
* Module dependencies.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const Document = require('../document_provider')();
|
|
const EventEmitter = require('events').EventEmitter;
|
|
const ValidationError = require('../error/validation');
|
|
const immediate = require('../helpers/immediate');
|
|
const internalToObjectOptions = require('../options').internalToObjectOptions;
|
|
const get = require('../helpers/get');
|
|
const promiseOrCallback = require('../helpers/promiseOrCallback');
|
|
const util = require('util');
|
|
|
|
const documentArrayParent = require('../helpers/symbols').documentArrayParent;
|
|
const validatorErrorSymbol = require('../helpers/symbols').validatorErrorSymbol;
|
|
|
|
/**
|
|
* EmbeddedDocument constructor.
|
|
*
|
|
* @param {Object} obj js object returned from the db
|
|
* @param {MongooseDocumentArray} parentArr the parent array of this document
|
|
* @param {Boolean} skipId
|
|
* @inherits Document
|
|
* @api private
|
|
*/
|
|
|
|
function EmbeddedDocument(obj, parentArr, skipId, fields, index) {
|
|
if (parentArr != null && parentArr.isMongooseDocumentArray) {
|
|
this.__parentArray = parentArr;
|
|
this[documentArrayParent] = parentArr.$parent();
|
|
} else {
|
|
this.__parentArray = undefined;
|
|
this[documentArrayParent] = undefined;
|
|
}
|
|
this.$setIndex(index);
|
|
this.$isDocumentArrayElement = true;
|
|
|
|
Document.call(this, obj, fields, skipId);
|
|
|
|
const _this = this;
|
|
this.on('isNew', function(val) {
|
|
_this.isNew = val;
|
|
});
|
|
|
|
_this.on('save', function() {
|
|
_this.constructor.emit('save', _this);
|
|
});
|
|
}
|
|
|
|
/*!
|
|
* Inherit from Document
|
|
*/
|
|
EmbeddedDocument.prototype = Object.create(Document.prototype);
|
|
EmbeddedDocument.prototype.constructor = EmbeddedDocument;
|
|
|
|
for (const i in EventEmitter.prototype) {
|
|
EmbeddedDocument[i] = EventEmitter.prototype[i];
|
|
}
|
|
|
|
EmbeddedDocument.prototype.toBSON = function() {
|
|
return this.toObject(internalToObjectOptions);
|
|
};
|
|
|
|
/*!
|
|
* ignore
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.$setIndex = function(index) {
|
|
this.__index = index;
|
|
|
|
if (get(this, '$__.validationError', null) != null) {
|
|
const keys = Object.keys(this.$__.validationError.errors);
|
|
for (const key of keys) {
|
|
this.invalidate(key, this.$__.validationError.errors[key]);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Marks the embedded doc modified.
|
|
*
|
|
* ####Example:
|
|
*
|
|
* var doc = blogpost.comments.id(hexstring);
|
|
* doc.mixed.type = 'changed';
|
|
* doc.markModified('mixed.type');
|
|
*
|
|
* @param {String} path the path which changed
|
|
* @api public
|
|
* @receiver EmbeddedDocument
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.markModified = function(path) {
|
|
this.$__.activePaths.modify(path);
|
|
if (!this.__parentArray) {
|
|
return;
|
|
}
|
|
|
|
if (this.isNew) {
|
|
// Mark the WHOLE parent array as modified
|
|
// if this is a new document (i.e., we are initializing
|
|
// a document),
|
|
this.__parentArray._markModified();
|
|
} else {
|
|
this.__parentArray._markModified(this, path);
|
|
}
|
|
};
|
|
|
|
/*!
|
|
* ignore
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.populate = function() {
|
|
throw new Error('Mongoose does not support calling populate() on nested ' +
|
|
'docs. Instead of `doc.arr[0].populate("path")`, use ' +
|
|
'`doc.populate("arr.0.path")`');
|
|
};
|
|
|
|
/**
|
|
* Used as a stub for [hooks.js](https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3)
|
|
*
|
|
* ####NOTE:
|
|
*
|
|
* _This is a no-op. Does not actually save the doc to the db._
|
|
*
|
|
* @param {Function} [fn]
|
|
* @return {Promise} resolved Promise
|
|
* @api private
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.save = function(options, fn) {
|
|
if (typeof options === 'function') {
|
|
fn = options;
|
|
options = {};
|
|
}
|
|
options = options || {};
|
|
|
|
if (!options.suppressWarning) {
|
|
console.warn('mongoose: calling `save()` on a subdoc does **not** save ' +
|
|
'the document to MongoDB, it only runs save middleware. ' +
|
|
'Use `subdoc.save({ suppressWarning: true })` to hide this warning ' +
|
|
'if you\'re sure this behavior is right for your app.');
|
|
}
|
|
|
|
return promiseOrCallback(fn, cb => {
|
|
this.$__save(cb);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Used as a stub for middleware
|
|
*
|
|
* ####NOTE:
|
|
*
|
|
* _This is a no-op. Does not actually save the doc to the db._
|
|
*
|
|
* @param {Function} [fn]
|
|
* @method $__save
|
|
* @api private
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.$__save = function(fn) {
|
|
return immediate(() => fn(null, this));
|
|
};
|
|
|
|
/*!
|
|
* Registers remove event listeners for triggering
|
|
* on subdocuments.
|
|
*
|
|
* @param {EmbeddedDocument} sub
|
|
* @api private
|
|
*/
|
|
|
|
function registerRemoveListener(sub) {
|
|
let owner = sub.ownerDocument();
|
|
|
|
function emitRemove() {
|
|
owner.removeListener('save', emitRemove);
|
|
owner.removeListener('remove', emitRemove);
|
|
sub.emit('remove', sub);
|
|
sub.constructor.emit('remove', sub);
|
|
owner = sub = null;
|
|
}
|
|
|
|
owner.on('save', emitRemove);
|
|
owner.on('remove', emitRemove);
|
|
}
|
|
|
|
/*!
|
|
* no-op for hooks
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.$__remove = function(cb) {
|
|
return cb(null, this);
|
|
};
|
|
|
|
/**
|
|
* Removes the subdocument from its parent array.
|
|
*
|
|
* @param {Object} [options]
|
|
* @param {Function} [fn]
|
|
* @api public
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.remove = function(options, fn) {
|
|
if (typeof options === 'function' && !fn) {
|
|
fn = options;
|
|
options = undefined;
|
|
}
|
|
if (!this.__parentArray || (options && options.noop)) {
|
|
fn && fn(null);
|
|
return this;
|
|
}
|
|
|
|
let _id;
|
|
if (!this.willRemove) {
|
|
_id = this._doc._id;
|
|
if (!_id) {
|
|
throw new Error('For your own good, Mongoose does not know ' +
|
|
'how to remove an EmbeddedDocument that has no _id');
|
|
}
|
|
this.__parentArray.pull({ _id: _id });
|
|
this.willRemove = true;
|
|
registerRemoveListener(this);
|
|
}
|
|
|
|
if (fn) {
|
|
fn(null);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Override #update method of parent documents.
|
|
* @api private
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.update = function() {
|
|
throw new Error('The #update method is not available on EmbeddedDocuments');
|
|
};
|
|
|
|
/**
|
|
* Helper for console.log
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.inspect = function() {
|
|
return this.toObject({
|
|
transform: false,
|
|
virtuals: false,
|
|
flattenDecimals: false
|
|
});
|
|
};
|
|
|
|
if (util.inspect.custom) {
|
|
/*!
|
|
* Avoid Node deprecation warning DEP0079
|
|
*/
|
|
|
|
EmbeddedDocument.prototype[util.inspect.custom] = EmbeddedDocument.prototype.inspect;
|
|
}
|
|
|
|
/**
|
|
* Marks a path as invalid, causing validation to fail.
|
|
*
|
|
* @param {String} path the field to invalidate
|
|
* @param {String|Error} err error which states the reason `path` was invalid
|
|
* @return {Boolean}
|
|
* @api public
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.invalidate = function(path, err, val) {
|
|
Document.prototype.invalidate.call(this, path, err, val);
|
|
|
|
if (!this[documentArrayParent] || this.__index == null) {
|
|
if (err[validatorErrorSymbol] || err instanceof ValidationError) {
|
|
return true;
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
const index = this.__index;
|
|
const parentPath = this.__parentArray.$path();
|
|
const fullPath = [parentPath, index, path].join('.');
|
|
this[documentArrayParent].invalidate(fullPath, err, val);
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Marks a path as valid, removing existing validation errors.
|
|
*
|
|
* @param {String} path the field to mark as valid
|
|
* @api private
|
|
* @method $markValid
|
|
* @receiver EmbeddedDocument
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.$markValid = function(path) {
|
|
if (!this[documentArrayParent]) {
|
|
return;
|
|
}
|
|
|
|
const index = this.__index;
|
|
if (typeof index !== 'undefined') {
|
|
const parentPath = this.__parentArray.$path();
|
|
const fullPath = [parentPath, index, path].join('.');
|
|
this[documentArrayParent].$markValid(fullPath);
|
|
}
|
|
};
|
|
|
|
/*!
|
|
* ignore
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.$ignore = function(path) {
|
|
Document.prototype.$ignore.call(this, path);
|
|
|
|
if (!this[documentArrayParent]) {
|
|
return;
|
|
}
|
|
|
|
const index = this.__index;
|
|
if (typeof index !== 'undefined') {
|
|
const parentPath = this.__parentArray.$path();
|
|
const fullPath = [parentPath, index, path].join('.');
|
|
this[documentArrayParent].$ignore(fullPath);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Checks if a path is invalid
|
|
*
|
|
* @param {String} path the field to check
|
|
* @api private
|
|
* @method $isValid
|
|
* @receiver EmbeddedDocument
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.$isValid = function(path) {
|
|
const index = this.__index;
|
|
if (typeof index !== 'undefined' && this[documentArrayParent]) {
|
|
return !this[documentArrayParent].$__.validationError ||
|
|
!this[documentArrayParent].$__.validationError.errors[this.$__fullPath(path)];
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Returns the top level document of this sub-document.
|
|
*
|
|
* @return {Document}
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.ownerDocument = function() {
|
|
if (this.$__.ownerDocument) {
|
|
return this.$__.ownerDocument;
|
|
}
|
|
|
|
let parent = this[documentArrayParent];
|
|
if (!parent) {
|
|
return this;
|
|
}
|
|
|
|
while (parent[documentArrayParent] || parent.$parent) {
|
|
parent = parent[documentArrayParent] || parent.$parent;
|
|
}
|
|
|
|
this.$__.ownerDocument = parent;
|
|
return this.$__.ownerDocument;
|
|
};
|
|
|
|
/**
|
|
* Returns the full path to this document. If optional `path` is passed, it is appended to the full path.
|
|
*
|
|
* @param {String} [path]
|
|
* @return {String}
|
|
* @api private
|
|
* @method $__fullPath
|
|
* @memberOf EmbeddedDocument
|
|
* @instance
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.$__fullPath = function(path) {
|
|
if (!this.$__.fullPath) {
|
|
let parent = this; // eslint-disable-line consistent-this
|
|
if (!parent[documentArrayParent]) {
|
|
return path;
|
|
}
|
|
|
|
const paths = [];
|
|
while (parent[documentArrayParent] || parent.$parent) {
|
|
if (parent[documentArrayParent]) {
|
|
paths.unshift(parent.__parentArray.$path());
|
|
} else {
|
|
paths.unshift(parent.$basePath);
|
|
}
|
|
parent = parent[documentArrayParent] || parent.$parent;
|
|
}
|
|
|
|
this.$__.fullPath = paths.join('.');
|
|
|
|
if (!this.$__.ownerDocument) {
|
|
// optimization
|
|
this.$__.ownerDocument = parent;
|
|
}
|
|
}
|
|
|
|
return path
|
|
? this.$__.fullPath + '.' + path
|
|
: this.$__.fullPath;
|
|
};
|
|
|
|
/**
|
|
* Returns this sub-documents parent document.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.parent = function() {
|
|
return this[documentArrayParent];
|
|
};
|
|
|
|
/**
|
|
* Returns this sub-documents parent array.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
EmbeddedDocument.prototype.parentArray = function() {
|
|
return this.__parentArray;
|
|
};
|
|
|
|
/*!
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = EmbeddedDocument;
|
|
|