Skip to content

Commit b4d34f4

Browse files
authored
Merge pull request #15415 from Automattic/vkarpov15/gh-15410
fix(model): make bulkSave() rely on document.validate() to validate docs and skip bulkWrite casting
2 parents b4c437a + 39c9b18 commit b4d34f4

File tree

2 files changed

+82
-41
lines changed

2 files changed

+82
-41
lines changed

lib/model.js

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3393,7 +3393,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
33933393
return bulkWriteResult;
33943394
}
33953395

3396-
const validations = ops.map(op => castBulkWrite(this, op, options));
3396+
const validations = options?._skipCastBulkWrite ? [] : ops.map(op => castBulkWrite(this, op, options));
33973397
const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore();
33983398
if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) {
33993399
options = { ...options, session: asyncLocalStorage.session };
@@ -3430,6 +3430,9 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
34303430
let validationErrors = [];
34313431
const results = [];
34323432
await new Promise((resolve) => {
3433+
if (validations.length === 0) {
3434+
return resolve();
3435+
}
34333436
for (let i = 0; i < validations.length; ++i) {
34343437
validations[i]((err) => {
34353438
if (err == null) {
@@ -3567,8 +3570,9 @@ Model.bulkSave = async function bulkSave(documents, options) {
35673570

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

3570-
const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps });
3571-
const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, { skipValidation: true, ...options }).then(
3573+
const writeOperations = this.buildBulkWriteOperations(documents, options);
3574+
const opts = { skipValidation: true, _skipCastBulkWrite: true, ...options };
3575+
const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, opts).then(
35723576
(res) => ({ bulkWriteResult: res, bulkWriteError: null }),
35733577
(err) => ({ bulkWriteResult: null, bulkWriteError: err })
35743578
);
@@ -3867,26 +3871,25 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
38673871
}
38683872

38693873
setDefaultOptions();
3870-
const discriminatorKey = this.schema.options.discriminatorKey;
38713874

3872-
const writeOperations = documents.reduce((accumulator, document, i) => {
3875+
const writeOperations = documents.map((document, i) => {
38733876
if (!options.skipValidation) {
38743877
if (!(document instanceof Document)) {
38753878
throw new Error(`documents.${i} was not a mongoose document, documents must be an array of mongoose documents (instanceof mongoose.Document).`);
38763879
}
3877-
const validationError = document.validateSync();
3878-
if (validationError) {
3879-
throw validationError;
3880+
if (options.validateBeforeSave == null || options.validateBeforeSave) {
3881+
const err = document.validateSync();
3882+
if (err != null) {
3883+
throw err;
3884+
}
38803885
}
38813886
}
38823887

38833888
const isANewDocument = document.isNew;
38843889
if (isANewDocument) {
38853890
const writeOperation = { insertOne: { document } };
38863891
utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps);
3887-
accumulator.push(writeOperation);
3888-
3889-
return accumulator;
3892+
return writeOperation;
38903893
}
38913894

38923895
const delta = document.$__delta();
@@ -3909,22 +3912,14 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
39093912
}
39103913
}
39113914

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

3926-
return accumulator;
3927-
}, []);
3921+
return null;
3922+
}).filter(op => op !== null);
39283923

39293924
return writeOperations;
39303925

test/model.test.js

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6808,15 +6808,10 @@ describe('Model', function() {
68086808
new User({ name: 'b' })
68096809
];
68106810

6811-
let err;
6812-
try {
6813-
User.buildBulkWriteOperations(users);
6814-
} catch (error) {
6815-
err = error;
6816-
}
6817-
6818-
6819-
assert.ok(err);
6811+
assert.throws(
6812+
() => User.buildBulkWriteOperations(users),
6813+
/name: Path `name` \(`a`\) is shorter than the minimum allowed length/
6814+
);
68206815
});
68216816

68226817
it('throws an error if documents is not an array', function() {
@@ -6828,9 +6823,7 @@ describe('Model', function() {
68286823

68296824

68306825
assert.throws(
6831-
function() {
6832-
User.buildBulkWriteOperations(null);
6833-
},
6826+
() => User.buildBulkWriteOperations(null),
68346827
/bulkSave expects an array of documents to be passed/
68356828
);
68366829
});
@@ -6841,14 +6834,11 @@ describe('Model', function() {
68416834

68426835
const User = db.model('User', userSchema);
68436836

6844-
68456837
assert.throws(
6846-
function() {
6847-
User.buildBulkWriteOperations([
6848-
new User({ name: 'Hafez' }),
6849-
{ name: 'I am not a document' }
6850-
]);
6851-
},
6838+
() => User.buildBulkWriteOperations([
6839+
new User({ name: 'Hafez' }),
6840+
{ name: 'I am not a document' }
6841+
]),
68526842
/documents\.1 was not a mongoose document/
68536843
);
68546844
});
@@ -7062,6 +7052,62 @@ describe('Model', function() {
70627052

70637053
});
70647054

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

0 commit comments

Comments
 (0)