Skip to content

Commit 06c8245

Browse files
authored
Merge pull request #15586 from Automattic/vkarpov15/gh-14424
BREAKING CHANGE: disallow update pipelines by default, require updatePipeline option
2 parents 5da0a9c + ffb1f41 commit 06c8245

File tree

5 files changed

+69
-31
lines changed

5 files changed

+69
-31
lines changed

docs/migrating_to_9.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ schema.pre('save', function(next, arg) {
6868

6969
In Mongoose 9, `next(null, 'new arg')` doesn't overwrite the args to the next middleware.
7070

71+
## Update pipelines disallowed by default
72+
73+
As of MongoDB 4.2, you can pass an array of pipeline stages to `updateOne()`, `updateMany()`, and `findOneAndUpdate()` to modify the document in multiple stages.
74+
Mongoose does not cast update pipelines at all, so for Mongoose 9 we've made using update pipelines throw an error by default.
75+
76+
```javascript
77+
// Throws in Mongoose 9. Works in Mongoose 8
78+
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }]);
79+
```
80+
81+
Set `updatePipeline: true` to enable update pipelines.
82+
83+
```javascript
84+
// Works in Mongoose 9
85+
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: true });
86+
```
87+
7188
## Removed background option for indexes
7289

7390
[MongoDB no longer supports the `background` option for indexes as of MongoDB 4.2](https://www.mongodb.com/docs/manual/core/index-creation/#index-operations). Mongoose 9 will no longer set the background option by default and Mongoose 9 no longer supports setting the `background` option on `Schema.prototype.index()`.

lib/query.js

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,6 +1748,10 @@ Query.prototype.setOptions = function(options, overwrite) {
17481748
this._mongooseOptions.overwriteImmutable = options.overwriteImmutable;
17491749
delete options.overwriteImmutable;
17501750
}
1751+
if ('updatePipeline' in options) {
1752+
this._mongooseOptions.updatePipeline = options.updatePipeline;
1753+
delete options.updatePipeline;
1754+
}
17511755
if ('sanitizeProjection' in options) {
17521756
if (options.sanitizeProjection && !this._mongooseOptions.sanitizeProjection) {
17531757
sanitizeProjection(this._fields);
@@ -3385,7 +3389,7 @@ function prepareDiscriminatorCriteria(query) {
33853389
* @memberOf Query
33863390
* @instance
33873391
* @param {Object|Query} [filter]
3388-
* @param {Object} [doc]
3392+
* @param {Object} [update]
33893393
* @param {Object} [options]
33903394
* @param {Boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) rather than just the document
33913395
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
@@ -3407,9 +3411,9 @@ function prepareDiscriminatorCriteria(query) {
34073411
* @api public
34083412
*/
34093413

3410-
Query.prototype.findOneAndUpdate = function(filter, doc, options) {
3414+
Query.prototype.findOneAndUpdate = function(filter, update, options) {
34113415
if (typeof filter === 'function' ||
3412-
typeof doc === 'function' ||
3416+
typeof update === 'function' ||
34133417
typeof options === 'function' ||
34143418
typeof arguments[3] === 'function') {
34153419
throw new MongooseError('Query.prototype.findOneAndUpdate() no longer accepts a callback');
@@ -3423,7 +3427,7 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options) {
34233427
options = undefined;
34243428
break;
34253429
case 1:
3426-
doc = filter;
3430+
update = filter;
34273431
filter = options = undefined;
34283432
break;
34293433
}
@@ -3436,11 +3440,6 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options) {
34363440
);
34373441
}
34383442

3439-
// apply doc
3440-
if (doc) {
3441-
this._mergeUpdate(doc);
3442-
}
3443-
34443443
options = options ? clone(options) : {};
34453444

34463445
if (options.projection) {
@@ -3463,6 +3462,11 @@ Query.prototype.findOneAndUpdate = function(filter, doc, options) {
34633462

34643463
this.setOptions(options);
34653464

3465+
// apply doc
3466+
if (update) {
3467+
this._mergeUpdate(update);
3468+
}
3469+
34663470
return this;
34673471
};
34683472

@@ -3997,39 +4001,43 @@ function _completeManyLean(schema, docs, path, opts) {
39974001
* Override mquery.prototype._mergeUpdate to handle mongoose objects in
39984002
* updates.
39994003
*
4000-
* @param {Object} doc
4004+
* @param {Object} update
40014005
* @method _mergeUpdate
40024006
* @memberOf Query
40034007
* @instance
40044008
* @api private
40054009
*/
40064010

4007-
Query.prototype._mergeUpdate = function(doc) {
4011+
Query.prototype._mergeUpdate = function(update) {
4012+
const updatePipeline = this._mongooseOptions.updatePipeline;
4013+
if (!updatePipeline && Array.isArray(update)) {
4014+
throw new MongooseError('Cannot pass an array to query updates unless the `updatePipeline` option is set.');
4015+
}
40084016
if (!this._update) {
4009-
this._update = Array.isArray(doc) ? [] : {};
4017+
this._update = Array.isArray(update) ? [] : {};
40104018
}
40114019

4012-
if (doc == null || (typeof doc === 'object' && Object.keys(doc).length === 0)) {
4020+
if (update == null || (typeof update === 'object' && Object.keys(update).length === 0)) {
40134021
return;
40144022
}
40154023

4016-
if (doc instanceof Query) {
4024+
if (update instanceof Query) {
40174025
if (Array.isArray(this._update)) {
4018-
throw new Error('Cannot mix array and object updates');
4026+
throw new MongooseError('Cannot mix array and object updates');
40194027
}
4020-
if (doc._update) {
4021-
utils.mergeClone(this._update, doc._update);
4028+
if (update._update) {
4029+
utils.mergeClone(this._update, update._update);
40224030
}
4023-
} else if (Array.isArray(doc)) {
4031+
} else if (Array.isArray(update)) {
40244032
if (!Array.isArray(this._update)) {
4025-
throw new Error('Cannot mix array and object updates');
4033+
throw new MongooseError('Cannot mix array and object updates');
40264034
}
4027-
this._update = this._update.concat(doc);
4035+
this._update = this._update.concat(update);
40284036
} else {
40294037
if (Array.isArray(this._update)) {
4030-
throw new Error('Cannot mix array and object updates');
4038+
throw new MongooseError('Cannot mix array and object updates');
40314039
}
4032-
utils.mergeClone(this._update, doc);
4040+
utils.mergeClone(this._update, update);
40334041
}
40344042
};
40354043

test/model.updateOne.test.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2766,10 +2766,16 @@ describe('model: updateOne: ', function() {
27662766
const Model = db.model('Test', schema);
27672767

27682768
await Model.create({ oldProp: 'test' });
2769+
2770+
assert.throws(
2771+
() => Model.updateOne({}, [{ $set: { newProp: 'test2' } }]),
2772+
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
2773+
);
2774+
27692775
await Model.updateOne({}, [
27702776
{ $set: { newProp: 'test2' } },
27712777
{ $unset: ['oldProp'] }
2772-
]);
2778+
], { updatePipeline: true });
27732779
let doc = await Model.findOne();
27742780
assert.equal(doc.newProp, 'test2');
27752781
assert.strictEqual(doc.oldProp, void 0);
@@ -2778,7 +2784,7 @@ describe('model: updateOne: ', function() {
27782784
await Model.updateOne({}, [
27792785
{ $addFields: { oldProp: 'test3' } },
27802786
{ $project: { newProp: 0 } }
2781-
]);
2787+
], { updatePipeline: true });
27822788
doc = await Model.findOne();
27832789
assert.equal(doc.oldProp, 'test3');
27842790
assert.strictEqual(doc.newProp, void 0);
@@ -2792,7 +2798,7 @@ describe('model: updateOne: ', function() {
27922798
await Model.updateOne({}, [
27932799
{ $set: { newProp: 'test2' } },
27942800
{ $unset: 'oldProp' }
2795-
]);
2801+
], { updatePipeline: true });
27962802
const doc = await Model.findOne();
27972803
assert.equal(doc.newProp, 'test2');
27982804
assert.strictEqual(doc.oldProp, void 0);
@@ -2805,8 +2811,11 @@ describe('model: updateOne: ', function() {
28052811
const updatedAt = cat.updatedAt;
28062812

28072813
await new Promise(resolve => setTimeout(resolve), 50);
2808-
const updated = await Cat.findOneAndUpdate({ _id: cat._id },
2809-
[{ $set: { name: 'Raikou' } }], { new: true });
2814+
const updated = await Cat.findOneAndUpdate(
2815+
{ _id: cat._id },
2816+
[{ $set: { name: 'Raikou' } }],
2817+
{ new: true, updatePipeline: true }
2818+
);
28102819
assert.ok(updated.updatedAt.getTime() > updatedAt.getTime());
28112820
});
28122821
});

test/timestamps.test.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,9 +1034,7 @@ describe('timestamps', function() {
10341034
sub: { subName: 'John' }
10351035
});
10361036
await doc.save();
1037-
await Test.updateMany({}, [{ $set: { updateCounter: 1 } }]);
1038-
// oddly enough, the null property is not accessible. Doing check.null doesn't return anything even though
1039-
// if you were to console.log() the output of a findOne you would be able to see it. This is the workaround.
1037+
await Test.updateMany({}, [{ $set: { updateCounter: 1 } }], { updatePipeline: true });
10401038
const test = await Test.countDocuments({ null: { $exists: true } });
10411039
assert.equal(test, 0);
10421040
// now we need to make sure that the solution didn't prevent the updateCounter addition

types/query.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ declare module 'mongoose' {
4343
| 'setDefaultsOnInsert'
4444
| 'strict'
4545
| 'strictQuery'
46-
| 'translateAliases';
46+
| 'translateAliases'
47+
| 'updatePipeline';
4748

4849
type MongooseBaseQueryOptions<DocType = unknown> = Pick<QueryOptions<DocType>, MongooseBaseQueryOptionKeys>;
4950

@@ -219,6 +220,11 @@ declare module 'mongoose' {
219220
translateAliases?: boolean;
220221
upsert?: boolean;
221222
useBigInt64?: boolean;
223+
/**
224+
* Set to true to allow passing in an update pipeline instead of an update document.
225+
* Mongoose disallows update pipelines by default because Mongoose does not cast update pipelines.
226+
*/
227+
updatePipeline?: boolean;
222228
writeConcern?: mongodb.WriteConcern;
223229

224230
[other: string]: any;

0 commit comments

Comments
 (0)