diff --git a/lib/model.js b/lib/model.js index c86cd0ac7e5..c6d5b28c083 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2079,6 +2079,11 @@ var INSERT_MANY_CONVERT_OPTIONS = { * because it only sends one operation to the server, rather than one for each * document. * + * Mongoose always validates each document **before** sending `insertMany` + * to MongoDB. So if one document has a validation error, no documents will + * be saved, unless you set + * [the `ordered` option to false](https://docs.mongodb.com/manual/reference/method/db.collection.insertMany/#error-handling). + * * This function does **not** trigger save middleware. * * This function triggers the following middleware: @@ -2091,6 +2096,8 @@ var INSERT_MANY_CONVERT_OPTIONS = { * * @param {Array|Object|*} doc(s) * @param {Object} [options] see the [mongodb driver options](http://mongodb.github.io/node-mongodb-native/2.2/api/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 `false`, will return the [raw result from the MongoDB driver](http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~insertWriteOpCallback) with a `mongoose` property that contains `validationErrors` if this is an unordered `insertMany`. * @param {Function} [callback] callback * @return {Promise} * @api public @@ -2105,16 +2112,18 @@ Model.insertMany = function(arr, options, callback) { if (callback) { callback = this.$wrapCallback(callback); } - var limit = options && options.limit; - if (typeof limit !== 'number') { - limit = 1000; - } + callback = callback || utils.noop; + options = options || {}; + var limit = get(options, 'limit', 1000); + var rawResult = get(options, 'rawResult', false); + var ordered = get(options, 'ordered', true); if (!Array.isArray(arr)) { arr = [arr]; } var toExecute = []; + var validationErrors = []; arr.forEach(function(doc) { toExecute.push(function(callback) { doc = new _this(doc); @@ -2123,7 +2132,8 @@ Model.insertMany = function(arr, options, callback) { // Option `ordered` signals that insert should be continued after reaching // a failing insert. Therefore we delegate "null", meaning the validation // failed. It's up to the next function to filter out all failed models - if (options != null && typeof options === 'object' && options['ordered'] === false) { + if (ordered === false) { + validationErrors.push(error); return callback(null, null); } return callback(error); @@ -2144,7 +2154,7 @@ Model.insertMany = function(arr, options, callback) { }); // Quickly escape while there aren't any valid docAttributes if (docAttributes.length < 1) { - callback && callback(null, []); + callback(null, []); return; } var docObjects = docAttributes.map(function(doc) { @@ -2157,7 +2167,7 @@ Model.insertMany = function(arr, options, callback) { return doc.toObject(INSERT_MANY_CONVERT_OPTIONS); }); - _this.collection.insertMany(docObjects, options, function(error) { + _this.collection.insertMany(docObjects, options, function(error, res) { if (error) { callback && callback(error); return; @@ -2167,7 +2177,17 @@ Model.insertMany = function(arr, options, callback) { docAttributes[i].emit('isNew', false); docAttributes[i].constructor.emit('isNew', false); } - callback && callback(null, docAttributes); + if (rawResult) { + if (ordered === false) { + // Decorate with mongoose validation errors in case of unordered, + // because then still do `insertMany()` + res.mongoose = { + validationErrors: validationErrors + }; + } + return callback(null, res); + } + callback(null, docAttributes); }); }); }; diff --git a/lib/utils.js b/lib/utils.js index 71b79fde719..8e3b719f297 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -894,3 +894,9 @@ exports.each = function(arr, fn) { fn(arr[i]); } }; + +/*! + * ignore + */ + +exports.noop = function() {}; diff --git a/test/model.test.js b/test/model.test.js index ebe2ffe8d6d..a27155a1761 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5398,6 +5398,28 @@ describe('Model', function() { }); }); + it('insertMany() multi validation error with ordered false (gh-5337)', function(done) { + var schema = new Schema({ + name: { type: String, required: true } + }); + var Movie = db.model('gh5337', schema); + + var arr = [ + { foo: 'The Phantom Menace' }, + { name: 'Star Wars' }, + { name: 'The Empire Strikes Back' }, + { foobar: 'The Force Awakens' } + ]; + var opts = { ordered: false, rawResult: true }; + Movie.insertMany(arr, opts, function(error, res) { + assert.ifError(error); + assert.equal(res.mongoose.validationErrors.length, 2); + assert.equal(res.mongoose.validationErrors[0].name, 'ValidationError'); + assert.equal(res.mongoose.validationErrors[1].name, 'ValidationError'); + done(); + }); + }); + it('insertMany() depopulate (gh-4590)', function(done) { var personSchema = new Schema({ name: String