From eebf2d661669fcc8796a666fcb8ce1977347ab6a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 1 Aug 2024 13:54:16 -0400 Subject: [PATCH] fix(document): apply virtuals to subdocuments if parent schema has `virtuals: true` for backwards compatibility Fix #14771 Re: #14394 Re: #14623 --- lib/document.js | 12 +++++++++--- test/document.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/document.js b/lib/document.js index f4bc5e3e53e..afc9e5ceacc 100644 --- a/lib/document.js +++ b/lib/document.js @@ -3801,7 +3801,7 @@ Document.prototype.$__handleReject = function handleReject(err) { }; /** - * Internal helper for toObject() and toJSON() that doesn't manipulate options + * Internal common logic for toObject() and toJSON() * * @return {Object} * @api private @@ -3834,14 +3834,17 @@ Document.prototype.$toObject = function(options, json) { } const depopulate = options._calledWithOptions.depopulate - ?? options._parentOptions?.depopulate ?? defaultOptions?.depopulate + ?? options.depopulate ?? false; // _isNested will only be true if this is not the top level document, we // should never depopulate the top-level document if (depopulate && options._isNested && this.$__.wasPopulated) { return clone(this.$__.wasPopulated.value || this._doc._id, options); } + if (depopulate) { + options.depopulate = true; + } // merge default options with input options. if (defaultOptions != null) { @@ -3855,7 +3858,9 @@ Document.prototype.$toObject = function(options, json) { options.json = json; options.minimize = _minimize; - options._parentOptions = options; + const parentOptions = options._parentOptions; + // Parent options should only bubble down for subdocuments, not populated docs + options._parentOptions = this.$isSubdocument ? options : null; options._skipSingleNestedGetters = false; // remember the root transform function @@ -3886,6 +3891,7 @@ Document.prototype.$toObject = function(options, json) { const virtuals = options._calledWithOptions.virtuals ?? defaultOptions.virtuals + ?? parentOptions?.virtuals ?? undefined; if (virtuals || (getters && virtuals !== false)) { diff --git a/test/document.test.js b/test/document.test.js index 5957556a867..f65479224d0 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -13750,6 +13750,38 @@ describe('document', function() { /Test error in post deleteOne hook/ ); }); + + it('applies virtuals to subschemas if top-level schema has virtuals: true (gh-14771)', function() { + const userLabSchema = new mongoose.Schema({ + capacityLevel: Number + }); + + userLabSchema.virtual('capacityLevelCeil').get(function() { + return Math.ceil(this.capacityLevel); + }); + + const labPlotSchema = new mongoose.Schema({ + plotId: Number, + lab: userLabSchema + }); + + const userSchema = new mongoose.Schema({ + username: String, + labPlots: [labPlotSchema] + }, { toObject: { virtuals: true }, toJSON: { virtuals: true } }); + + const User = db.model('User', userSchema); + + const doc = new User({ + username: 'test', + labPlots: [{ + plotId: 1, + lab: { capacityLevel: 3.14 } + }] + }); + assert.strictEqual(doc.toObject().labPlots[0].lab.capacityLevelCeil, 4); + assert.strictEqual(doc.toJSON().labPlots[0].lab.capacityLevelCeil, 4); + }); }); describe('Check if instance function that is supplied in schema option is available', function() {