From e6c9dd3eb7c5ec8f104f9b62eca85bd6e5887dda Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 14 Oct 2024 14:17:14 -0400 Subject: [PATCH] fix(document): recursively clear modified subpaths when setting deeply nested subdoc to null Fix #14952 --- lib/helpers/document/cleanModifiedSubpaths.js | 16 +++- test/document.test.js | 78 +++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/lib/helpers/document/cleanModifiedSubpaths.js b/lib/helpers/document/cleanModifiedSubpaths.js index 43c225e4fd2..c12b5e2eea5 100644 --- a/lib/helpers/document/cleanModifiedSubpaths.js +++ b/lib/helpers/document/cleanModifiedSubpaths.js @@ -25,11 +25,21 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) { ++deleted; if (doc.$isSubdocument) { - const owner = doc.ownerDocument(); - const fullPath = doc.$__fullPath(modifiedPath); - owner.$__.activePaths.clearPath(fullPath); + cleanParent(doc, modifiedPath); } } } return deleted; }; + +function cleanParent(doc, path, seen = new Set()) { + if (seen.has(doc)) { + throw new Error('Infinite subdocument loop: subdoc with _id ' + doc._id + ' is a parent of itself'); + } + const parent = doc.$parent(); + const newPath = doc.$__pathRelativeToParent(void 0, false) + '.' + path; + parent.$__.activePaths.clearPath(newPath); + if (parent.$isSubdocument) { + cleanParent(parent, newPath, seen); + } +} diff --git a/test/document.test.js b/test/document.test.js index f3869b8e58c..f866b618d4b 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -13978,6 +13978,84 @@ describe('document', function() { assert.ok(Buffer.isBuffer(reloaded.pdfSettings.fileContent)); assert.strictEqual(reloaded.pdfSettings.fileContent.toString('utf8'), 'hello'); }); + + it('clears modified subpaths when setting deeply nested subdoc to null (gh-14952)', async function() { + const currentMilestoneSchema = new Schema( + { + id: { type: String, required: true } + }, + { + _id: false + } + ); + + const milestoneSchema = new Schema( + { + current: { + type: currentMilestoneSchema, + required: true + } + }, + { + _id: false + } + ); + + const campaignSchema = new Schema( + { + milestones: { + type: milestoneSchema, + required: false + } + }, + { + _id: false + } + ); + const questSchema = new Schema( + { + campaign: { type: campaignSchema, required: false } + }, + { + _id: false + } + ); + + const parentSchema = new Schema({ + quests: [questSchema] + }); + + const ParentModel = db.model('Parent', parentSchema); + const doc = new ParentModel({ + quests: [ + { + campaign: { + milestones: { + current: { + id: 'milestone1' + } + } + } + } + ] + }); + + await doc.save(); + + // Set the nested schema to null + doc.quests[0].campaign.milestones.current = { + id: 'milestone1' + }; + doc.quests[0].campaign.milestones.current = { + id: '' + }; + + doc.quests[0].campaign.milestones = null; + await doc.save(); + + const fromDb = await ParentModel.findById(doc._id).orFail(); + assert.strictEqual(fromDb.quests[0].campaign.milestones, null); + }); }); describe('Check if instance function that is supplied in schema option is available', function() {