From 84e262e6650e9df57ca349a5037b53460951636e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 2 Oct 2022 16:40:56 -0400 Subject: [PATCH 1/2] fix(model): avoid saving applied defaults if path is deselected Fix #12414 --- lib/helpers/projection/isPathExcluded.js | 4 ++++ lib/model.js | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/helpers/projection/isPathExcluded.js b/lib/helpers/projection/isPathExcluded.js index e0424b52365..e8f126b22da 100644 --- a/lib/helpers/projection/isPathExcluded.js +++ b/lib/helpers/projection/isPathExcluded.js @@ -12,6 +12,10 @@ const isDefiningProjection = require('./isDefiningProjection'); */ module.exports = function isPathExcluded(projection, path) { + if (projection == null) { + return false; + } + if (path === '_id') { return projection._id === 0; } diff --git a/lib/model.js b/lib/model.js index 9abbeeb14f0..420e01eb486 100644 --- a/lib/model.js +++ b/lib/model.js @@ -51,11 +51,13 @@ const { getRelatedDBIndexes, getRelatedSchemaIndexes } = require('./helpers/indexes/getRelatedIndexes'); +const isPathExcluded = require('./helpers/projection/isPathExcluded'); const decorateDiscriminatorIndexOptions = require('./helpers/indexes/decorateDiscriminatorIndexOptions'); const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive'); const leanPopulateMap = require('./helpers/populate/leanPopulateMap'); const modifiedPaths = require('./helpers/update/modifiedPaths'); const parallelLimit = require('./helpers/parallelLimit'); +const parentPaths = require('./helpers/path/parentPaths'); const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline'); const pushNestedArrayPaths = require('./helpers/model/pushNestedArrayPaths'); const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField'); @@ -760,6 +762,19 @@ Model.prototype.$__delta = function() { } } + // If this path is set to default, and either this path or one of + // its parents is excluded, don't treat this path as dirty. + if (this.$isDefault(data.path) && this.$__.selected) { + if (data.path.indexOf('.') === -1 && isPathExcluded(this.$__.selected, data.path)) { + continue; + } + + const pathsToCheck = parentPaths(data.path); + if (pathsToCheck.find(path => isPathExcluded(this.$__.isSelected, path))) { + continue; + } + } + if (divergent.length) continue; if (value === undefined) { operand(this, where, delta, data, 1, '$unset'); @@ -798,6 +813,11 @@ Model.prototype.$__delta = function() { if (this.$__.version) { this.$__version(where, delta); } + + if (Object.keys(delta).length === 0) { + return [where, null]; + } + return [where, delta]; }; From d061da33066f1fc63f5be46af004415a89c271fd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 3 Oct 2022 11:57:48 -0400 Subject: [PATCH 2/2] test: add test coverage for #12414 --- test/document.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/document.test.js b/test/document.test.js index 84cafc22463..ca2aca278d9 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11881,6 +11881,24 @@ describe('document', function() { const addedDoc = await Model.findOne({ name: 'Test' }); assert.strictEqual(addedDoc.count, 1); }); + + it('avoids overwriting array if saving with no changes with array deselected (gh-12414)', async function() { + const schema = new mongoose.Schema({ + name: String, + tags: [String] + }); + const Test = db.model('Test', schema); + + const { _id } = await Test.create({ name: 'Mongoose', tags: ['mongodb'] }); + + const doc = await Test.findById(_id).select('name'); + assert.deepStrictEqual(doc.getChanges(), {}); + await doc.save(); + + const rawDoc = await Test.collection.findOne({ _id }); + assert.ok(rawDoc); + assert.deepStrictEqual(rawDoc.tags, ['mongodb']); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() {