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.
282 lines
6.9 KiB
282 lines
6.9 KiB
'use strict';
|
|
|
|
const Document = require('../document');
|
|
const immediate = require('../helpers/immediate');
|
|
const internalToObjectOptions = require('../options').internalToObjectOptions;
|
|
const promiseOrCallback = require('../helpers/promiseOrCallback');
|
|
|
|
const documentArrayParent = require('../helpers/symbols').documentArrayParent;
|
|
|
|
module.exports = Subdocument;
|
|
|
|
/**
|
|
* Subdocument constructor.
|
|
*
|
|
* @inherits Document
|
|
* @api private
|
|
*/
|
|
|
|
function Subdocument(value, fields, parent, skipId, options) {
|
|
this.$isSingleNested = true;
|
|
|
|
const hasPriorDoc = options != null && options.priorDoc;
|
|
let initedPaths = null;
|
|
if (hasPriorDoc) {
|
|
this._doc = Object.assign({}, options.priorDoc._doc);
|
|
delete this._doc[this.schema.options.discriminatorKey];
|
|
initedPaths = Object.keys(options.priorDoc._doc || {}).
|
|
filter(key => key !== this.schema.options.discriminatorKey);
|
|
}
|
|
if (parent != null) {
|
|
// If setting a nested path, should copy isNew from parent re: gh-7048
|
|
options = Object.assign({}, options, { isNew: parent.isNew });
|
|
}
|
|
Document.call(this, value, fields, skipId, options);
|
|
|
|
if (hasPriorDoc) {
|
|
for (const key of initedPaths) {
|
|
if (!this.$__.activePaths.states.modify[key] &&
|
|
!this.$__.activePaths.states.default[key] &&
|
|
!this.$__.$setCalled.has(key)) {
|
|
const schematype = this.schema.path(key);
|
|
const def = schematype == null ? void 0 : schematype.getDefault(this);
|
|
if (def === void 0) {
|
|
delete this._doc[key];
|
|
} else {
|
|
this._doc[key] = def;
|
|
this.$__.activePaths.default(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Subdocument.prototype = Object.create(Document.prototype);
|
|
|
|
Subdocument.prototype.toBSON = function() {
|
|
return this.toObject(internalToObjectOptions);
|
|
};
|
|
|
|
/**
|
|
* Used as a stub for middleware
|
|
*
|
|
* ####NOTE:
|
|
*
|
|
* _This is a no-op. Does not actually save the doc to the db._
|
|
*
|
|
* @param {Function} [fn]
|
|
* @return {Promise} resolved Promise
|
|
* @api private
|
|
*/
|
|
|
|
Subdocument.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
|
|
*/
|
|
|
|
Subdocument.prototype.$__save = function(fn) {
|
|
return immediate(() => fn(null, this));
|
|
};
|
|
|
|
Subdocument.prototype.$isValid = function(path) {
|
|
if (this.$parent && this.$basePath) {
|
|
return this.$parent.$isValid([this.$basePath, path].join('.'));
|
|
}
|
|
return Document.prototype.$isValid.call(this, path);
|
|
};
|
|
|
|
Subdocument.prototype.markModified = function(path) {
|
|
Document.prototype.markModified.call(this, path);
|
|
|
|
if (this.$parent && this.$basePath) {
|
|
if (this.$parent.isDirectModified(this.$basePath)) {
|
|
return;
|
|
}
|
|
this.$parent.markModified([this.$basePath, path].join('.'), this);
|
|
}
|
|
};
|
|
|
|
Subdocument.prototype.isModified = function(paths, modifiedPaths) {
|
|
if (this.$parent && this.$basePath) {
|
|
if (Array.isArray(paths) || typeof paths === 'string') {
|
|
paths = (Array.isArray(paths) ? paths : paths.split(' '));
|
|
paths = paths.map(p => [this.$basePath, p].join('.'));
|
|
}
|
|
|
|
return this.$parent.isModified(paths, modifiedPaths);
|
|
}
|
|
|
|
return Document.prototype.isModified.call(this, paths, modifiedPaths);
|
|
};
|
|
|
|
/**
|
|
* Marks a path as valid, removing existing validation errors.
|
|
*
|
|
* @param {String} path the field to mark as valid
|
|
* @api private
|
|
* @method $markValid
|
|
* @receiver Subdocument
|
|
*/
|
|
|
|
Subdocument.prototype.$markValid = function(path) {
|
|
Document.prototype.$markValid.call(this, path);
|
|
if (this.$parent && this.$basePath) {
|
|
this.$parent.$markValid([this.$basePath, path].join('.'));
|
|
}
|
|
};
|
|
|
|
/*!
|
|
* ignore
|
|
*/
|
|
|
|
Subdocument.prototype.invalidate = function(path, err, val) {
|
|
// Hack: array subdocuments' validationError is equal to the owner doc's,
|
|
// so validating an array subdoc gives the top-level doc back. Temporary
|
|
// workaround for #5208 so we don't have circular errors.
|
|
if (err !== this.ownerDocument().$__.validationError) {
|
|
Document.prototype.invalidate.call(this, path, err, val);
|
|
}
|
|
|
|
if (this.$parent && this.$basePath) {
|
|
this.$parent.invalidate([this.$basePath, path].join('.'), err, val);
|
|
} else if (err.kind === 'cast' || err.name === 'CastError') {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
/*!
|
|
* ignore
|
|
*/
|
|
|
|
Subdocument.prototype.$ignore = function(path) {
|
|
Document.prototype.$ignore.call(this, path);
|
|
if (this.$parent && this.$basePath) {
|
|
this.$parent.$ignore([this.$basePath, path].join('.'));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the top level document of this sub-document.
|
|
*
|
|
* @return {Document}
|
|
*/
|
|
|
|
Subdocument.prototype.ownerDocument = function() {
|
|
if (this.$__.ownerDocument) {
|
|
return this.$__.ownerDocument;
|
|
}
|
|
|
|
let parent = this.$parent;
|
|
if (!parent) {
|
|
return this;
|
|
}
|
|
|
|
while (parent.$parent || parent[documentArrayParent]) {
|
|
parent = parent.$parent || parent[documentArrayParent];
|
|
}
|
|
|
|
this.$__.ownerDocument = parent;
|
|
return this.$__.ownerDocument;
|
|
};
|
|
|
|
/**
|
|
* Returns this sub-documents parent document.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Subdocument.prototype.parent = function() {
|
|
return this.$parent;
|
|
};
|
|
|
|
/*!
|
|
* no-op for hooks
|
|
*/
|
|
|
|
Subdocument.prototype.$__remove = function(cb) {
|
|
return cb(null, this);
|
|
};
|
|
|
|
/**
|
|
* Null-out this subdoc
|
|
*
|
|
* @param {Object} [options]
|
|
* @param {Function} [callback] optional callback for compatibility with Document.prototype.remove
|
|
*/
|
|
|
|
Subdocument.prototype.remove = function(options, callback) {
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = null;
|
|
}
|
|
|
|
registerRemoveListener(this);
|
|
|
|
// If removing entire doc, no need to remove subdoc
|
|
if (!options || !options.noop) {
|
|
this.$parent.set(this.$basePath, null);
|
|
}
|
|
|
|
if (typeof callback === 'function') {
|
|
callback(null);
|
|
}
|
|
};
|
|
|
|
/*!
|
|
* ignore
|
|
*/
|
|
|
|
Subdocument.prototype.populate = function() {
|
|
throw new Error('Mongoose does not support calling populate() on nested ' +
|
|
'docs. Instead of `doc.nested.populate("path")`, use ' +
|
|
'`doc.populate("nested.path")`');
|
|
};
|
|
|
|
/*!
|
|
* Registers remove event listeners for triggering
|
|
* on subdocuments.
|
|
*
|
|
* @param {Subdocument} 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);
|
|
}
|
|
|