Skip to content

Commit

Permalink
Merge pull request #14884 from Automattic/vkarpov15/gh-14763-2
Browse files Browse the repository at this point in the history
fix(model): throw MongooseBulkSaveIncompleteError if bulkSave() didn't completely succeed
  • Loading branch information
vkarpov15 authored Sep 19, 2024
2 parents d28e5a0 + 5716b04 commit 45bb194
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 7 deletions.
44 changes: 44 additions & 0 deletions lib/error/bulkSaveIncompleteError.js
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 4 additions & 4 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
);
}
Expand Down
32 changes: 29 additions & 3 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down

0 comments on commit 45bb194

Please sign in to comment.