From 5716b040d2a17b551762467f566cae4888dc8077 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Sep 2024 14:41:04 -0400 Subject: [PATCH] fix(model): throw MongooseBulkSaveIncompleteError if bulkSave() didn't completely succeed Fix #14763 --- lib/error/bulkSaveIncompleteError.js | 44 ++++++++++++++++++++++++++++ lib/model.js | 8 ++--- test/model.test.js | 32 ++++++++++++++++++-- 3 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 lib/error/bulkSaveIncompleteError.js diff --git a/lib/error/bulkSaveIncompleteError.js b/lib/error/bulkSaveIncompleteError.js new file mode 100644 index 0000000000..c4b88e5d7b --- /dev/null +++ b/lib/error/bulkSaveIncompleteError.js @@ -0,0 +1,44 @@ +/*! + * Module dependencies. + */ + +'use strict'; + +const MongooseError = require('./mongooseError'); + + +/** + * If the underwriting `bulkWrite()` for `bulkSave()` succeeded, but wasn't able to update or + * insert all documents, we throw this error. + * + * @api private + */ + +class MongooseBulkSaveIncompleteError extends MongooseError { + constructor(modelName, documents, bulkWriteResult) { + const matchedCount = bulkWriteResult?.matchedCount ?? 0; + const insertedCount = bulkWriteResult?.insertedCount ?? 0; + let preview = documents.map(doc => doc._id).join(', '); + if (preview.length > 100) { + preview = preview.slice(0, 100) + '...'; + } + + const numDocumentsNotUpdated = documents.length - matchedCount - insertedCount; + super(`${modelName}.bulkSave() was not able to update ${numDocumentsNotUpdated} of the given documents due to incorrect version or optimistic concurrency, document ids: ${preview}`); + + this.modelName = modelName; + this.documents = documents; + this.bulkWriteResult = bulkWriteResult; + this.numDocumentsNotUpdated = numDocumentsNotUpdated; + } +} + +Object.defineProperty(MongooseBulkSaveIncompleteError.prototype, 'name', { + value: 'MongooseBulkSaveIncompleteError' +}); + +/*! + * exports + */ + +module.exports = MongooseBulkSaveIncompleteError; diff --git a/lib/model.js b/lib/model.js index 0a1091ab2b..6d9458b5d7 100644 --- a/lib/model.js +++ b/lib/model.js @@ -64,6 +64,7 @@ const STATES = require('./connectionState'); const util = require('util'); const utils = require('./utils'); const minimize = require('./helpers/minimize'); +const MongooseBulkSaveIncompleteError = require('./error/bulkSaveIncompleteError'); const modelCollectionSymbol = Symbol('mongoose#Model#collection'); const modelDbSymbol = Symbol('mongoose#Model#db'); @@ -3418,11 +3419,10 @@ Model.bulkSave = async function bulkSave(documents, options) { const matchedCount = bulkWriteResult?.matchedCount ?? 0; const insertedCount = bulkWriteResult?.insertedCount ?? 0; - if (writeOperations.length > 0 && matchedCount + insertedCount === 0 && !bulkWriteError) { - throw new DocumentNotFoundError( - writeOperations.filter(op => op.updateOne).map(op => op.updateOne.filter), + if (writeOperations.length > 0 && matchedCount + insertedCount < writeOperations.length && !bulkWriteError) { + throw new MongooseBulkSaveIncompleteError( this.modelName, - writeOperations.length, + documents, bulkWriteResult ); } diff --git a/test/model.test.js b/test/model.test.js index 8e94a87669..1e531c097b 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6994,9 +6994,35 @@ describe('Model', function() { foo.bar = 2; const err = await TestModel.bulkSave([foo]).then(() => null, err => err); - assert.equal(err.name, 'DocumentNotFoundError'); - assert.equal(err.numAffected, 1); - assert.ok(Array.isArray(err.filter)); + assert.equal(err.name, 'MongooseBulkSaveIncompleteError'); + assert.equal(err.numDocumentsNotUpdated, 1); + }); + it('should error if not all documents were inserted or updated (gh-14763)', async function() { + const fooSchema = new mongoose.Schema({ + bar: { type: Number } + }, { optimisticConcurrency: true }); + const TestModel = db.model('Test', fooSchema); + + const errorDoc = await TestModel.create({ bar: 0 }); + const okDoc = await TestModel.create({ bar: 0 }); + + // update 1 + errorDoc.bar = 1; + await errorDoc.save(); + + // parallel update + const errorDocCopy = await TestModel.findById(errorDoc._id); + errorDocCopy.bar = 99; + await errorDocCopy.save(); + + errorDoc.bar = 2; + okDoc.bar = 2; + const err = await TestModel.bulkSave([errorDoc, okDoc]).then(() => null, err => err); + assert.equal(err.name, 'MongooseBulkSaveIncompleteError'); + assert.equal(err.numDocumentsNotUpdated, 1); + + const updatedOkDoc = await TestModel.findById(okDoc._id); + assert.equal(updatedOkDoc.bar, 2); }); it('should error if there is a validation error', async function() { const fooSchema = new mongoose.Schema({