Skip to content

Commit b107906

Browse files
committed
fix(model): make bulkSave() rely on document.validate() to validate docs and skip bulkWrite casting
Fix #15410
1 parent 2dda096 commit b107906

File tree

2 files changed

+87
-43
lines changed

2 files changed

+87
-43
lines changed

lib/model.js

+15-23
Original file line numberDiff line numberDiff line change
@@ -3394,7 +3394,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
33943394
return bulkWriteResult;
33953395
}
33963396

3397-
const validations = ops.map(op => castBulkWrite(this, op, options));
3397+
const validations = options?._skipCastBulkWrite ? [] : ops.map(op => castBulkWrite(this, op, options));
33983398
const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore();
33993399
if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) {
34003400
options = { ...options, session: asyncLocalStorage.session };
@@ -3431,6 +3431,9 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
34313431
let validationErrors = [];
34323432
const results = [];
34333433
await new Promise((resolve) => {
3434+
if (validations.length === 0) {
3435+
return resolve();
3436+
}
34343437
for (let i = 0; i < validations.length; ++i) {
34353438
validations[i]((err) => {
34363439
if (err == null) {
@@ -3568,8 +3571,9 @@ Model.bulkSave = async function bulkSave(documents, options) {
35683571

35693572
await Promise.all(documents.map(doc => buildPreSavePromise(doc, options)));
35703573

3571-
const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps });
3572-
const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, { skipValidation: true, ...options }).then(
3574+
const writeOperations = await this.buildBulkWriteOperations(documents, options);
3575+
const opts = { skipValidation: true, _skipCastBulkWrite: true, ...options };
3576+
const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, opts).then(
35733577
(res) => ({ bulkWriteResult: res, bulkWriteError: null }),
35743578
(err) => ({ bulkWriteResult: null, bulkWriteError: err })
35753579
);
@@ -3862,32 +3866,28 @@ Model.castObject = function castObject(obj, options) {
38623866
* @api private
38633867
*/
38643868

3865-
Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, options) {
3869+
Model.buildBulkWriteOperations = async function buildBulkWriteOperations(documents, options) {
38663870
if (!Array.isArray(documents)) {
38673871
throw new Error(`bulkSave expects an array of documents to be passed, received \`${documents}\` instead`);
38683872
}
38693873

38703874
setDefaultOptions();
3871-
const discriminatorKey = this.schema.options.discriminatorKey;
38723875

3873-
const writeOperations = documents.reduce((accumulator, document, i) => {
3876+
const writeOperations = await Promise.all(documents.map(async(document, i) => {
38743877
if (!options.skipValidation) {
38753878
if (!(document instanceof Document)) {
38763879
throw new Error(`documents.${i} was not a mongoose document, documents must be an array of mongoose documents (instanceof mongoose.Document).`);
38773880
}
3878-
const validationError = document.validateSync();
3879-
if (validationError) {
3880-
throw validationError;
3881+
if (options.validateBeforeSave == null || options.validateBeforeSave) {
3882+
await document.validate();
38813883
}
38823884
}
38833885

38843886
const isANewDocument = document.isNew;
38853887
if (isANewDocument) {
38863888
const writeOperation = { insertOne: { document } };
38873889
utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps);
3888-
accumulator.push(writeOperation);
3889-
3890-
return accumulator;
3890+
return writeOperation;
38913891
}
38923892

38933893
const delta = document.$__delta();
@@ -3910,22 +3910,14 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
39103910
}
39113911
}
39123912

3913-
// Set the discriminator key, so bulk write casting knows which
3914-
// schema to use re: gh-13907
3915-
if (document[discriminatorKey] != null && !(discriminatorKey in where)) {
3916-
where[discriminatorKey] = document[discriminatorKey];
3917-
}
3918-
39193913
document.$__version(where, delta);
39203914
const writeOperation = { updateOne: { filter: where, update: changes } };
39213915
utils.injectTimestampsOption(writeOperation.updateOne, options.timestamps);
3922-
accumulator.push(writeOperation);
3923-
3924-
return accumulator;
3916+
return writeOperation;
39253917
}
39263918

3927-
return accumulator;
3928-
}, []);
3919+
return null;
3920+
})).then(results => results.filter(op => op !== null));
39293921

39303922
return writeOperations;
39313923

test/model.test.js

+72-20
Original file line numberDiff line numberDiff line change
@@ -6780,7 +6780,7 @@ describe('Model', function() {
67806780
await users[2].save();
67816781
users[2].name = 'I am the updated third name';
67826782

6783-
const writeOperations = User.buildBulkWriteOperations(users);
6783+
const writeOperations = await User.buildBulkWriteOperations(users);
67846784

67856785
const desiredWriteOperations = [
67866786
{ insertOne: { document: users[0] } },
@@ -6795,7 +6795,7 @@ describe('Model', function() {
67956795

67966796
});
67976797

6798-
it('throws an error when one document is invalid', () => {
6798+
it('throws an error when one document is invalid', async() => {
67996799
const userSchema = new Schema({
68006800
name: { type: String, minLength: 5 }
68016801
});
@@ -6810,12 +6810,11 @@ describe('Model', function() {
68106810

68116811
let err;
68126812
try {
6813-
User.buildBulkWriteOperations(users);
6813+
await User.buildBulkWriteOperations(users);
68146814
} catch (error) {
68156815
err = error;
68166816
}
68176817

6818-
68196818
assert.ok(err);
68206819
});
68216820

@@ -6827,10 +6826,8 @@ describe('Model', function() {
68276826
const User = db.model('User', userSchema);
68286827

68296828

6830-
assert.throws(
6831-
function() {
6832-
User.buildBulkWriteOperations(null);
6833-
},
6829+
assert.rejects(
6830+
User.buildBulkWriteOperations(null),
68346831
/bulkSave expects an array of documents to be passed/
68356832
);
68366833
});
@@ -6842,17 +6839,15 @@ describe('Model', function() {
68426839
const User = db.model('User', userSchema);
68436840

68446841

6845-
assert.throws(
6846-
function() {
6847-
User.buildBulkWriteOperations([
6848-
new User({ name: 'Hafez' }),
6849-
{ name: 'I am not a document' }
6850-
]);
6851-
},
6842+
assert.rejects(
6843+
User.buildBulkWriteOperations([
6844+
new User({ name: 'Hafez' }),
6845+
{ name: 'I am not a document' }
6846+
]),
68526847
/documents\.1 was not a mongoose document/
68536848
);
68546849
});
6855-
it('skips validation when given `skipValidation` true', () => {
6850+
it('skips validation when given `skipValidation` true', async() => {
68566851
const userSchema = new Schema({
68576852
name: { type: String, minLength: 5 }
68586853
});
@@ -6865,7 +6860,7 @@ describe('Model', function() {
68656860
new User({ name: 'b' })
68666861
];
68676862

6868-
const writeOperations = User.buildBulkWriteOperations(users, { skipValidation: true });
6863+
const writeOperations = await User.buildBulkWriteOperations(users, { skipValidation: true });
68696864

68706865
assert.equal(writeOperations.length, 3);
68716866
});
@@ -6904,7 +6899,7 @@ describe('Model', function() {
69046899
userToUpdate.name = 'John Doe';
69056900

69066901
// Act
6907-
const writeOperations = User.buildBulkWriteOperations([newUser, userToUpdate], { timestamps: false, skipValidation: true });
6902+
const writeOperations = await User.buildBulkWriteOperations([newUser, userToUpdate], { timestamps: false, skipValidation: true });
69086903

69096904
// Assert
69106905
const timestampsOptions = writeOperations.map(writeOperationContainer => {
@@ -6926,7 +6921,7 @@ describe('Model', function() {
69266921
userToUpdate.name = 'John Doe';
69276922

69286923
// Act
6929-
const writeOperations = User.buildBulkWriteOperations([newUser, userToUpdate], { timestamps: true, skipValidation: true });
6924+
const writeOperations = await User.buildBulkWriteOperations([newUser, userToUpdate], { timestamps: true, skipValidation: true });
69306925

69316926
// Assert
69326927
const timestampsOptions = writeOperations.map(writeOperationContainer => {
@@ -6948,7 +6943,7 @@ describe('Model', function() {
69486943
userToUpdate.name = 'John Doe';
69496944

69506945
// Act
6951-
const writeOperations = User.buildBulkWriteOperations([newUser, userToUpdate], { skipValidation: true });
6946+
const writeOperations = await User.buildBulkWriteOperations([newUser, userToUpdate], { skipValidation: true });
69526947

69536948
// Assert
69546949
const timestampsOptions = writeOperations.map(writeOperationContainer => {
@@ -7062,6 +7057,63 @@ describe('Model', function() {
70627057

70637058
});
70647059

7060+
it('saves documents with embedded discriminators (gh-15410)', async function() {
7061+
const requirementSchema = new Schema({
7062+
kind: { type: String, required: true },
7063+
quantity: Number,
7064+
notes: String
7065+
}, { _id: false, discriminatorKey: 'kind' });
7066+
7067+
const componentRequirementSchema = new Schema({
7068+
componentTest: 'ObjectId'
7069+
}, { _id: false });
7070+
7071+
const toolRequirementSchema = new Schema({
7072+
toolTest: 'ObjectId'
7073+
}, { _id: false });
7074+
7075+
const subRowSchema = new Schema({
7076+
requirements: [requirementSchema]
7077+
}, { _id: false });
7078+
7079+
const rowSchema = new Schema({
7080+
rows: [subRowSchema]
7081+
}, { _id: false });
7082+
7083+
const orderSchema = new Schema({
7084+
code: String,
7085+
rows: [rowSchema]
7086+
}, { timestamps: true });
7087+
7088+
const Requirement = requirementSchema;
7089+
Requirement.discriminators = {};
7090+
Requirement.discriminators['ComponentRequirement'] = componentRequirementSchema;
7091+
Requirement.discriminators['ToolRequirement'] = toolRequirementSchema;
7092+
7093+
subRowSchema.path('requirements').discriminator('ComponentRequirement', componentRequirementSchema);
7094+
subRowSchema.path('requirements').discriminator('ToolRequirement', toolRequirementSchema);
7095+
7096+
const Order = db.model('Order', orderSchema);
7097+
7098+
const order = await Order.create({
7099+
code: 'test-2',
7100+
rows: [{
7101+
rows: [{
7102+
requirements: [
7103+
{ kind: 'ComponentRequirement', quantity: 1 },
7104+
{ kind: 'ToolRequirement', quantity: 1 }
7105+
]
7106+
}]
7107+
}]
7108+
});
7109+
7110+
const newObjectId = new mongoose.Types.ObjectId();
7111+
order.rows[0].rows[0].requirements[1].set({ toolTest: newObjectId.toString() });
7112+
await Order.bulkSave([order]);
7113+
const reread = await Order.findById(order._id).lean();
7114+
assert.strictEqual(reread.rows[0].rows[0].requirements[1].toolTest?.toHexString(), newObjectId.toHexString());
7115+
});
7116+
70657117
it('insertMany should throw an error if there were operations that failed validation, ' +
70667118
'but all operations that passed validation succeeded (gh-14572) (gh-13256)', async function() {
70677119
const userSchema = new Schema({

0 commit comments

Comments
 (0)