From 1111f98fab0dbc4e17b0d933d3cb4a6472d1e0cc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 Mar 2023 10:47:07 -0400 Subject: [PATCH 1/2] fix(model): add `results` property to unordered `insertMany()` to make it easy to identify exactly which documents were inserted Fix #12791 --- lib/model.js | 18 +++++++++++++++++- test/model.test.js | 10 ++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 8021a4f622e..7596817bc1a 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3427,6 +3427,7 @@ Model.$__insertMany = function(arr, options, callback) { const validationErrors = []; const validationErrorsToOriginalOrder = new Map(); + const results = ordered ? null : new Array(arr.length); const toExecute = arr.map((doc, index) => callback => { if (!(doc instanceof _this)) { @@ -3454,6 +3455,7 @@ Model.$__insertMany = function(arr, options, callback) { if (ordered === false) { validationErrors.push(error); validationErrorsToOriginalOrder.set(error, index); + results[index] = error; return callback(null, null); } return callback(error); @@ -3536,6 +3538,19 @@ Model.$__insertMany = function(arr, options, callback) { ...error.writeErrors[i], index: validDocIndexToOriginalIndex.get(error.writeErrors[i].index) }; + if (!ordered) { + results[validDocIndexToOriginalIndex.get(error.writeErrors[i].index)] = error.writeErrors[i]; + } + } + + if (!ordered) { + for (let i = 0; i < results.length; ++i) { + if (results[i] === void 0) { + results[i] = docAttributes[i]; + } + } + + error.results = results; } let firstErroredIndex = -1; @@ -3563,7 +3578,8 @@ Model.$__insertMany = function(arr, options, callback) { if (rawResult && ordered === false) { error.mongoose = { - validationErrors: validationErrors + validationErrors: validationErrors, + results: results }; } diff --git a/test/model.test.js b/test/model.test.js index e474e0f1850..745ef793baf 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4570,6 +4570,11 @@ describe('Model', function() { const error = await Movie.insertMany(arr, { ordered: false }).then(() => null, err => err); assert.equal(error.message.indexOf('E11000'), 0); + assert.equal(error.results.length, 3); + assert.equal(error.results[0].name, 'Star Wars'); + assert.ok(error.results[1].err); + assert.ok(error.results[1].err.errmsg.includes('E11000')); + assert.equal(error.results[2].name, 'The Empire Strikes Back'); const docs = await Movie.find({}).sort({ name: 1 }).exec(); assert.equal(docs.length, 2); @@ -4677,6 +4682,11 @@ describe('Model', function() { assert.equal(err.insertedDocs[0].code, 'test'); assert.equal(err.insertedDocs[1].code, 'HARD'); + assert.equal(err.results.length, 3); + assert.ok(err.results[0].err.errmsg.includes('E11000')); + assert.equal(err.results[1].code, 'test'); + assert.equal(err.results[2].code, 'HARD'); + await Question.deleteMany({}); await Question.create({ code: 'MEDIUM', text: '123' }); await Question.create({ code: 'HARD', text: '123' }); From 27091c6ca4831d56aa861b247cf6f66c3889b617 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 Mar 2023 10:58:37 -0400 Subject: [PATCH 2/2] fix: add types and docs re: code review comments --- lib/model.js | 18 +++++++++++++----- test/model.test.js | 13 +++++++++++++ test/types/models.test.ts | 7 +++++-- types/models.d.ts | 9 +++++++++ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/lib/model.js b/lib/model.js index 7596817bc1a..f26c18babbb 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3371,7 +3371,7 @@ Model.startSession = function() { * @param {Array|Object|*} doc(s) * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany) * @param {Boolean} [options.ordered=true] if true, will fail fast on the first error encountered. If false, will insert all the documents it can and report errors later. An `insertMany()` with `ordered = false` is called an "unordered" `insertMany()`. - * @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` if this is an unordered `insertMany`. + * @param {Boolean} [options.rawResult=false] if false, the returned promise resolves to the documents that passed mongoose document validation. If `true`, will return the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/InsertManyResult.html) with a `mongoose` property that contains `validationErrors` and `results` if this is an unordered `insertMany`. * @param {Boolean} [options.lean=false] if `true`, skips hydrating and validating the documents. This option is useful if you need the extra performance, but Mongoose won't validate the documents before inserting. * @param {Number} [options.limit=null] this limits the number of documents being processed (validation/casting) by mongoose in parallel, this does **NOT** send the documents in batches to MongoDB. Use this option if you're processing a large number of documents and your app is running out of memory. * @param {String|Object|Array} [options.populate=null] populates the result documents. This option is a no-op if `rawResult` is set. @@ -3534,19 +3534,20 @@ Model.$__insertMany = function(arr, options, callback) { const erroredIndexes = new Set((error && error.writeErrors || []).map(err => err.index)); for (let i = 0; i < error.writeErrors.length; ++i) { + const originalIndex = validDocIndexToOriginalIndex.get(error.writeErrors[i].index); error.writeErrors[i] = { ...error.writeErrors[i], - index: validDocIndexToOriginalIndex.get(error.writeErrors[i].index) + index: originalIndex }; if (!ordered) { - results[validDocIndexToOriginalIndex.get(error.writeErrors[i].index)] = error.writeErrors[i]; + results[originalIndex] = error.writeErrors[i]; } } if (!ordered) { for (let i = 0; i < results.length; ++i) { if (results[i] === void 0) { - results[i] = docAttributes[i]; + results[i] = docs[i]; } } @@ -3594,10 +3595,17 @@ Model.$__insertMany = function(arr, options, callback) { if (rawResult) { if (ordered === false) { + for (let i = 0; i < results.length; ++i) { + if (results[i] === void 0) { + results[i] = docs[i]; + } + } + // Decorate with mongoose validation errors in case of unordered, // because then still do `insertMany()` res.mongoose = { - validationErrors: validationErrors + validationErrors: validationErrors, + results: results }; } return callback(null, res); diff --git a/test/model.test.js b/test/model.test.js index 745ef793baf..7e1b903073e 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4857,6 +4857,12 @@ describe('Model', function() { assert.ok(!res.mongoose.validationErrors[0].errors['year']); assert.ok(res.mongoose.validationErrors[1].errors['year']); assert.ok(!res.mongoose.validationErrors[1].errors['name']); + + assert.equal(res.mongoose.results.length, 3); + assert.ok(res.mongoose.results[0].errors['name']); + assert.ok(res.mongoose.results[1].errors['year']); + assert.ok(res.mongoose.results[2].$__); + assert.equal(res.mongoose.results[2].name, 'The Empire Strikes Back'); }); it('insertMany() validation error with ordered false and rawResult for mixed write and validation error (gh-12791)', async function() { @@ -4886,6 +4892,13 @@ describe('Model', function() { assert.ok(!err.mongoose.validationErrors[0].errors['year']); assert.ok(err.mongoose.validationErrors[1].errors['year']); assert.ok(!err.mongoose.validationErrors[1].errors['name']); + + assert.equal(err.mongoose.results.length, 4); + assert.ok(err.mongoose.results[0].errors['name']); + assert.ok(err.mongoose.results[1].errors['year']); + assert.ok(err.mongoose.results[2].$__); + assert.equal(err.mongoose.results[2].name, 'The Empire Strikes Back'); + assert.ok(err.mongoose.results[3].err); }); it('insertMany() populate option (gh-9720)', async function() { diff --git a/test/types/models.test.ts b/test/types/models.test.ts index da7738f6b50..964c5dd2ad1 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -87,11 +87,11 @@ async function insertManyTest() { foo: string; } - const TestSchema = new Schema({ + const TestSchema = new Schema({ foo: { type: String, required: true } }); - const Test = connection.model('Test', TestSchema); + const Test = connection.model('Test', TestSchema); Test.insertMany([{ foo: 'bar' }]).then(async res => { res.length; @@ -99,6 +99,9 @@ async function insertManyTest() { const res = await Test.insertMany([{ foo: 'bar' }], { rawResult: true }); expectType(res.insertedIds[0]); + + const res2 = await Test.insertMany([{ foo: 'bar' }], { ordered: false, rawResult: true }); + expectAssignable>(res2.mongoose.results[0]); } function schemaStaticsWithoutGenerics() { diff --git a/types/models.d.ts b/types/models.d.ts index 5a557d21d36..bcfe680e532 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -288,6 +288,15 @@ declare module 'mongoose' { insertMany(doc: DocContents, callback: Callback, Require_id>, TMethodsAndOverrides, TVirtuals>>>): void; insertMany(docs: Array, options: InsertManyOptions & { lean: true; }): Promise, Require_id>>>; + insertMany( + doc: DocContents, + options: InsertManyOptions & { ordered: false; rawResult: true; } + ): Promise & { + mongoose: { + validationErrors: Error[]; + results: Array, Require_id>, TMethodsAndOverrides, TVirtuals>> + } + }>; insertMany(docs: Array, options: InsertManyOptions & { rawResult: true; }): Promise>; insertMany(docs: Array): Promise, Require_id>, TMethodsAndOverrides, TVirtuals>>>; insertMany(doc: DocContents, options: InsertManyOptions & { lean: true; }): Promise, Require_id>>>;