From ca3a738021a2eb061af0622882135cf5c97f4cd0 Mon Sep 17 00:00:00 2001 From: Suliman Abdulrazzaq Date: Fri, 25 Sep 2026 14:17:42 +0300 Subject: [PATCH] fix(query): replace {MODEL} in cast error messages from update and bulkWrite() casting --- lib/helpers/model/castBulkWrite.js | 23 ++++++++++++++++----- lib/helpers/query/castUpdate.js | 8 ++++++++ test/schema.test.js | 33 ++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js index bf0dd491434..d13a5b96cfa 100644 --- a/lib/helpers/model/castBulkWrite.js +++ b/lib/helpers/model/castBulkWrite.js @@ -149,7 +149,7 @@ module.exports.castUpdateOne = function castUpdateOne(originalModel, updateOne, model.schema.options.versionKey ); - updateOne['filter'] = cast(model.schema, updateOne['filter'], { + updateOne['filter'] = _castFilter(model, updateOne['filter'], { strict: strict, upsert: updateOne.upsert }); @@ -210,7 +210,7 @@ module.exports.castUpdateMany = function castUpdateMany(originalModel, updateMan model.schema.options.versionKey ); - updateMany['filter'] = cast(model.schema, updateMany['filter'], { + updateMany['filter'] = _castFilter(model, updateMany['filter'], { strict: strict, upsert: updateMany.upsert }); @@ -230,7 +230,7 @@ module.exports.castReplaceOne = async function castReplaceOne(originalModel, rep const strict = options.strict ?? model.schema.options.strict; _addDiscriminatorToObject(schema, replaceOne['filter']); - replaceOne['filter'] = cast(model.schema, replaceOne['filter'], { + replaceOne['filter'] = _castFilter(model, replaceOne['filter'], { strict: strict, upsert: replaceOne.upsert }); @@ -264,7 +264,7 @@ module.exports.castDeleteOne = function castDeleteOne(originalModel, deleteOne) _addDiscriminatorToObject(schema, deleteOne['filter']); - deleteOne['filter'] = cast(model.schema, deleteOne['filter']); + deleteOne['filter'] = _castFilter(model, deleteOne['filter']); }; module.exports.castDeleteMany = function castDeleteMany(originalModel, deleteMany) { @@ -273,7 +273,7 @@ module.exports.castDeleteMany = function castDeleteMany(originalModel, deleteMan _addDiscriminatorToObject(schema, deleteMany['filter']); - deleteMany['filter'] = cast(model.schema, deleteMany['filter']); + deleteMany['filter'] = _castFilter(model, deleteMany['filter']); }; module.exports.cast = { @@ -285,6 +285,19 @@ module.exports.cast = { deleteMany: module.exports.castDeleteMany }; +function _castFilter(model, filter, options) { + try { + return cast(model.schema, filter, options); + } catch (err) { + // Set the model on cast errors so `{MODEL}` gets replaced, like it is for + // query filter casting (gh-8300) + if (typeof err.setModel === 'function') { + err.setModel(model); + } + throw err; + } +} + function _addDiscriminatorToObject(schema, obj) { if (schema == null) { return; diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js index 6d6e6c3f902..511be60049f 100644 --- a/lib/helpers/query/castUpdate.js +++ b/lib/helpers/query/castUpdate.js @@ -499,6 +499,14 @@ function walkUpdatePath(schema, obj, op, options, context, filter, prefix) { */ function _appendError(error, query, key, aggregatedError) { + // Set the model on cast errors so `{MODEL}` gets replaced, like it is for + // query filter casting and document validation (gh-8300). `query` is a + // Query, or the model itself when casting a `bulkWrite()` operation. + const model = query?.modelName != null ? query : query?.model; + if (model != null && typeof error.setModel === 'function') { + error.setModel(model); + } + if (typeof query !== 'object' || !query.options.multipleCastError) { throw error; } diff --git a/test/schema.test.js b/test/schema.test.js index b9251763f50..30729c399d1 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -2827,6 +2827,39 @@ describe('schema', function() { '"twenty" is not a valid number for model gh8300_castObject' ); }); + + it('replaces {MODEL} with model name in update and bulkWrite() cast errors', async function() { + const schema = Schema({ + age: { + type: Number, + cast: '{VALUE} is not a valid number for model {MODEL}' + } + }); + const Test = db.model('gh8300_update', schema); + const message = '"twenty" is not a valid number for model gh8300_update'; + + const isCastError = err => err.name === 'CastError' && err.message === message; + + await assert.rejects(() => Test.updateOne({}, { age: 'twenty' }), isCastError); + await assert.rejects(() => Test.updateMany({}, { $set: { age: 'twenty' } }), isCastError); + await assert.rejects(() => Test.findOneAndUpdate({}, { age: 'twenty' }), isCastError); + + await assert.rejects( + () => Test.updateOne({}, { age: 'twenty' }, { multipleCastError: true }), + err => err.name === 'ValidationError' && + err.errors['age'].message === message && + err.message === 'Validation failed: age: ' + message + ); + + await assert.rejects( + () => Test.bulkWrite([{ updateOne: { filter: {}, update: { age: 'twenty' } } }]), + isCastError + ); + await assert.rejects( + () => Test.bulkWrite([{ deleteOne: { filter: { age: 'twenty' } } }]), + isCastError + ); + }); }); it('copies `.add()`-ed paths when calling `.add()` with a schema argument (gh-8429)', function() {