From 35d6c70c72e6516c16f96e8d79d7e8e9b56832b2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 25 Nov 2025 10:50:55 -0500 Subject: [PATCH] fix(bulkWrite): pass overwriteImmutable option to castUpdate fixes Fix #15781 Backport #15782 to 7.x --- lib/helpers/model/castBulkWrite.js | 14 +++++++++---- lib/helpers/query/castUpdate.js | 4 ++-- lib/helpers/query/handleImmutable.js | 6 +++++- test/model.updateOne.test.js | 30 ++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index 71d9150f848..56ee9548062 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -69,7 +69,9 @@ module.exports = function castBulkWrite(originalModel, op, options) { if (model.schema.$timestamps != null && op['updateOne'].timestamps !== false) { const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; - applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], {}); + applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], { + timestamps: op['updateOne'].timestamps + }); } if (op['updateOne'].timestamps !== false) { @@ -100,7 +102,8 @@ module.exports = function castBulkWrite(originalModel, op, options) { op['updateOne']['update'] = castUpdate(model.schema, op['updateOne']['update'], { strict: strict, overwrite: false, - upsert: op['updateOne'].upsert + upsert: op['updateOne'].upsert, + overwriteImmutable: op['updateOne'].overwriteImmutable }, model, op['updateOne']['filter']); } catch (error) { return callback(error, null); @@ -136,7 +139,9 @@ module.exports = function castBulkWrite(originalModel, op, options) { if (model.schema.$timestamps != null && op['updateMany'].timestamps !== false) { const createdAt = model.schema.$timestamps.createdAt; const updatedAt = model.schema.$timestamps.updatedAt; - applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateMany']['update'], {}); + applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateMany']['update'], { + timestamps: op['updateMany'].timestamps + }); } if (op['updateMany'].timestamps !== false) { applyTimestampsToChildren(now, op['updateMany']['update'], model.schema); @@ -158,7 +163,8 @@ module.exports = function castBulkWrite(originalModel, op, options) { op['updateMany']['update'] = castUpdate(model.schema, op['updateMany']['update'], { strict: strict, overwrite: false, - upsert: op['updateMany'].upsert + upsert: op['updateMany'].upsert, + overwriteImmutable: op['updateMany'].overwriteImmutable }, model, op['updateMany']['filter']); } catch (error) { return callback(error, null); diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js index 25fbb456ea1..2e097443c48 100644 --- a/lib/helpers/query/castUpdate.js +++ b/lib/helpers/query/castUpdate.js @@ -240,7 +240,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) { if (op !== '$setOnInsert' && !options.overwrite && - handleImmutable(schematype, strict, obj, key, prefix + key, context)) { + handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) { continue; } @@ -335,7 +335,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) { // You can use `$setOnInsert` with immutable keys if (op !== '$setOnInsert' && !options.overwrite && - handleImmutable(schematype, strict, obj, key, prefix + key, context)) { + handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) { continue; } diff --git a/lib/helpers/query/handleImmutable.js b/lib/helpers/query/handleImmutable.js index 22adb3c50de..dc26adbb3e6 100644 --- a/lib/helpers/query/handleImmutable.js +++ b/lib/helpers/query/handleImmutable.js @@ -2,7 +2,7 @@ const StrictModeError = require('../../error/strict'); -module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, ctx) { +module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, options, ctx) { if (schematype == null || !schematype.options || !schematype.options.immutable) { return false; } @@ -15,6 +15,10 @@ module.exports = function handleImmutable(schematype, strict, obj, key, fullPath return false; } + if (options && options.overwriteImmutable) { + return false; + } + if (strict === false) { return false; } diff --git a/test/model.updateOne.test.js b/test/model.updateOne.test.js index 8eab40a6a6b..5e5954c6c44 100644 --- a/test/model.updateOne.test.js +++ b/test/model.updateOne.test.js @@ -2707,6 +2707,36 @@ describe('model: updateOne: ', function() { assert.equal(doc.age, 20); }); + it('overwriting immutable createdAt with bulkWrite (gh-15781)', async function() { + const start = new Date().valueOf(); + const schema = Schema({ + createdAt: { + type: mongoose.Schema.Types.Date, + immutable: true + }, + name: String + }, { timestamps: true }); + + const Model = db.model('Test', schema); + + await Model.create({ name: 'gh-15781' }); + let doc = await Model.collection.findOne({ name: 'gh-15781' }); + assert.ok(doc.createdAt.valueOf() >= start); + + const createdAt = new Date('2011-06-01'); + assert.ok(createdAt.valueOf() < start.valueOf()); + await Model.bulkWrite([{ + updateOne: { + filter: { _id: doc._id }, + update: { name: 'gh-15781 update', createdAt }, + overwriteImmutable: true, + timestamps: false + } + }]); + doc = await Model.collection.findOne({ name: 'gh-15781 update' }); + assert.equal(doc.createdAt.valueOf(), createdAt.valueOf()); + }); + it('updates buffers with `runValidators` successfully (gh-8580)', async function() { const Test = db.model('Test', Schema({ data: { type: Buffer, required: true }