Skip to content

Commit d655898

Browse files
authored
Merge pull request #14197 from Automattic/IslandRhythms/gh-14164
fix(model): deep clone bulkWrite() updateOne arguments to avoid mutating documents in update
2 parents 913326d + 936d066 commit d655898

File tree

4 files changed

+54
-9
lines changed

4 files changed

+54
-9
lines changed

lib/helpers/model/castBulkWrite.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const applyTimestampsToChildren = require('../update/applyTimestampsToChildren')
66
const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate');
77
const cast = require('../../cast');
88
const castUpdate = require('../query/castUpdate');
9+
const clone = require('../clone');
910
const decorateUpdateWithVersionKey = require('../update/decorateUpdateWithVersionKey');
1011
const { inspect } = require('util');
1112
const setDefaultsOnInsert = require('../setDefaultsOnInsert');
@@ -64,30 +65,32 @@ module.exports = function castBulkWrite(originalModel, op, options) {
6465
const schema = model.schema;
6566
const strict = options.strict != null ? options.strict : model.schema.options.strict;
6667

68+
const update = clone(op['updateOne']['update']);
69+
6770
_addDiscriminatorToObject(schema, op['updateOne']['filter']);
6871

6972
if (model.schema.$timestamps != null && op['updateOne'].timestamps !== false) {
7073
const createdAt = model.schema.$timestamps.createdAt;
7174
const updatedAt = model.schema.$timestamps.updatedAt;
72-
applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], {});
75+
applyTimestampsToUpdate(now, createdAt, updatedAt, update, {});
7376
}
7477

7578
if (op['updateOne'].timestamps !== false) {
76-
applyTimestampsToChildren(now, op['updateOne']['update'], model.schema);
79+
applyTimestampsToChildren(now, update, model.schema);
7780
}
7881

7982
const shouldSetDefaultsOnInsert = op['updateOne'].setDefaultsOnInsert == null ?
8083
globalSetDefaultsOnInsert :
8184
op['updateOne'].setDefaultsOnInsert;
8285
if (shouldSetDefaultsOnInsert !== false) {
83-
setDefaultsOnInsert(op['updateOne']['filter'], model.schema, op['updateOne']['update'], {
86+
setDefaultsOnInsert(op['updateOne']['filter'], model.schema, update, {
8487
setDefaultsOnInsert: true,
8588
upsert: op['updateOne'].upsert
8689
});
8790
}
8891

8992
decorateUpdateWithVersionKey(
90-
op['updateOne']['update'],
93+
update,
9194
op['updateOne'],
9295
model.schema.options.versionKey
9396
);
@@ -96,8 +99,7 @@ module.exports = function castBulkWrite(originalModel, op, options) {
9699
strict: strict,
97100
upsert: op['updateOne'].upsert
98101
});
99-
100-
op['updateOne']['update'] = castUpdate(model.schema, op['updateOne']['update'], {
102+
op['updateOne']['update'] = castUpdate(model.schema, update, {
101103
strict: strict,
102104
upsert: op['updateOne'].upsert
103105
}, model, op['updateOne']['filter']);

lib/helpers/query/castUpdate.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const moveImmutableProperties = require('../update/moveImmutableProperties');
1313
const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
1414
const setDottedPath = require('../path/setDottedPath');
1515
const utils = require('../../utils');
16+
const { internalToObjectOptions } = require('../../options');
1617

1718
const mongodbUpdateOperators = new Set([
1819
'$currentDate',
@@ -99,7 +100,10 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
99100
const op = ops[i];
100101
val = ret[op];
101102
hasDollarKey = hasDollarKey || op.startsWith('$');
102-
103+
if (val != null && val.$__) {
104+
val = val.toObject(internalToObjectOptions);
105+
ret[op] = val;
106+
}
103107
if (val &&
104108
typeof val === 'object' &&
105109
!Buffer.isBuffer(val) &&

test/model.test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6392,6 +6392,47 @@ describe('Model', function() {
63926392
});
63936393
assert.deepEqual(timestampsOptions, [undefined, undefined]);
63946394
});
6395+
it('should not modify the object in the $set clause and not error when dealing with or without timestamps (gh-14164)', async function() {
6396+
const timeSchema = new Schema({
6397+
name: String,
6398+
properties: { type: Schema.Types.Mixed, default: {} }
6399+
}, { timestamps: true });
6400+
const timelessSchema = new Schema({
6401+
name: String,
6402+
properties: { type: Schema.Types.Mixed, default: {} }
6403+
});
6404+
6405+
const Time = db.model('Time', timeSchema);
6406+
const Timeless = db.model('Timeless', timelessSchema);
6407+
6408+
const timeDoc = await Time.create({
6409+
name: 'Time Test'
6410+
});
6411+
timeDoc.properties.color = 'Red';
6412+
const beforeSet = {};
6413+
Object.assign(beforeSet, timeDoc.toObject());
6414+
await Time.bulkWrite([{
6415+
updateOne: {
6416+
filter: { _id: timeDoc._id },
6417+
update: { $set: timeDoc }
6418+
}
6419+
}]);
6420+
assert.deepStrictEqual(beforeSet, timeDoc.toObject());
6421+
6422+
const timelessDoc = await Timeless.create({
6423+
name: 'Timeless Test'
6424+
});
6425+
timelessDoc.properties.color = 'Red';
6426+
const timelessObj = {};
6427+
Object.assign(timelessObj, timelessDoc.toObject());
6428+
await Timeless.bulkWrite([{
6429+
updateOne: {
6430+
filter: { _id: timelessDoc._id },
6431+
update: { $set: timelessDoc }
6432+
}
6433+
}]);
6434+
assert.deepStrictEqual(timelessObj, timelessDoc.toObject());
6435+
});
63956436
});
63966437

63976438
describe('bulkSave() (gh-9673)', function() {

test/schema.select.test.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@ describe('schema select option', function() {
1515

1616
before(function() {
1717
db = start();
18-
mongoose.set('debug', true);
1918
});
2019

2120
after(async function() {
2221
await db.close();
23-
mongoose.set('debug', false);
2422
});
2523

2624
beforeEach(() => db.deleteModel(/.*/));

0 commit comments

Comments
 (0)