Skip to content

Commit b962408

Browse files
authored
Merge pull request #15757 from Automattic/gh-15756
feat: add global option for `updatePipeline`
2 parents 47ae57f + b3a7429 commit b962408

File tree

7 files changed

+215
-15
lines changed

7 files changed

+215
-15
lines changed

docs/migrating_to_9.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ Set `updatePipeline: true` to enable update pipelines.
8585
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: true });
8686
```
8787

88+
You can also set `updatePipeline` globally to enable update pipelines for all update operations by default.
89+
90+
```javascript
91+
// Enable update pipelines globally
92+
mongoose.set('updatePipeline', true);
93+
94+
// Now update pipelines work without needing to specify the option on each query
95+
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }]);
96+
97+
// You can still override the global setting per query
98+
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: false }); // throws
99+
```
100+
88101
## Removed background option for indexes
89102

90103
[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/mongoose.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,18 +230,19 @@ Mongoose.prototype.setDriver = function setDriver(driver) {
230230
* - `cloneSchemas`: `false` by default. Set to `true` to `clone()` all schemas before compiling into a model.
231231
* - `debug`: If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arguments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`.
232232
* - `id`: If `true`, adds a `id` virtual to all schemas unless overwritten on a per-schema basis.
233-
* - `timestamps.createdAt.immutable`: `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.immutable) which means you can update the `createdAt`
234233
* - `maxTimeMS`: If set, attaches [maxTimeMS](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/) to every query
235234
* - `objectIdGetter`: `true` by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter.
236235
* - `overwriteModels`: Set to `true` to default to overwriting models with the same name when calling `mongoose.model()`, as opposed to throwing an `OverwriteModelError`.
237236
* - `returnOriginal`: If `false`, changes the default `returnOriginal` option to `findOneAndUpdate()`, `findByIdAndUpdate`, and `findOneAndReplace()` to false. This is equivalent to setting the `new` option to `true` for `findOneAndX()` calls by default. Read our [`findOneAndUpdate()` tutorial](https://mongoosejs.com/docs/tutorials/findoneandupdate.html) for more information.
238237
* - `runValidators`: `false` by default. Set to true to enable [update validators](https://mongoosejs.com/docs/validation.html#update-validators) for all validators by default.
239238
* - `sanitizeFilter`: `false` by default. Set to true to enable the [sanitization of the query filters](https://mongoosejs.com/docs/api/mongoose.html#Mongoose.prototype.sanitizeFilter()) against query selector injection attacks by wrapping any nested objects that have a property whose name starts with `$` in a `$eq`.
240239
* - `selectPopulatedPaths`: `true` by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one.
241-
* - `strict`: `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas.
242240
* - `strictQuery`: `false` by default. May be `false`, `true`, or `'throw'`. Sets the default [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas.
241+
* - `strict`: `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas.
242+
* - `timestamps.createdAt.immutable`: `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.immutable) which means you can update the `createdAt`
243243
* - `toJSON`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toJSON()), for determining how Mongoose documents get serialized by `JSON.stringify()`
244244
* - `toObject`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject())
245+
* - `updatePipeline`: `false` by default. If `true`, allows passing update pipelines (arrays) to update operations by default without explicitly setting `updatePipeline: true` in each query.
245246
*
246247
* @param {String|Object} key The name of the option or a object of multiple key-value pairs
247248
* @param {String|Function|Boolean} value The value of the option, unused if "key" is a object

lib/query.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3451,15 +3451,16 @@ Query.prototype.findOneAndUpdate = function(filter, update, options) {
34513451
delete options.fields;
34523452
}
34533453

3454-
const returnOriginal = this &&
3455-
this.model &&
3456-
this.model.base &&
3457-
this.model.base.options &&
3458-
this.model.base.options.returnOriginal;
3454+
const returnOriginal = this?.model?.base?.options?.returnOriginal;
34593455
if (options.new == null && options.returnDocument == null && options.returnOriginal == null && returnOriginal != null) {
34603456
options.returnOriginal = returnOriginal;
34613457
}
34623458

3459+
const updatePipeline = this?.model?.base?.options?.updatePipeline;
3460+
if (options.updatePipeline == null && updatePipeline != null) {
3461+
options.updatePipeline = updatePipeline;
3462+
}
3463+
34633464
this.setOptions(options);
34643465

34653466
// apply doc
@@ -3715,14 +3716,11 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options) {
37153716

37163717
options = options || {};
37173718

3718-
const returnOriginal = this &&
3719-
this.model &&
3720-
this.model.base &&
3721-
this.model.base.options &&
3722-
this.model.base.options.returnOriginal;
3719+
const returnOriginal = this?.model?.base?.options?.returnOriginal;
37233720
if (options.new == null && options.returnDocument == null && options.returnOriginal == null && returnOriginal != null) {
37243721
options.returnOriginal = returnOriginal;
37253722
}
3723+
37263724
this.setOptions(options);
37273725

37283726
return this;
@@ -4407,6 +4405,12 @@ function _update(query, op, filter, doc, options, callback) {
44074405
query.merge(filter);
44084406
}
44094407

4408+
const updatePipeline = query?.model?.base?.options?.updatePipeline;
4409+
if (updatePipeline != null && (options == null || options.updatePipeline == null)) {
4410+
options = options || {};
4411+
options.updatePipeline = updatePipeline;
4412+
}
4413+
44104414
if (utils.isObject(options)) {
44114415
query.setOptions(options);
44124416
}

lib/validOptions.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const VALID_OPTIONS = Object.freeze([
1919
'debug',
2020
'forceRepopulate',
2121
'id',
22-
'timestamps.createdAt.immutable',
2322
'maxTimeMS',
2423
'objectIdGetter',
2524
'overwriteModels',
@@ -32,10 +31,12 @@ const VALID_OPTIONS = Object.freeze([
3231
'strict',
3332
'strictPopulate',
3433
'strictQuery',
34+
'timestamps.createdAt.immutable',
3535
'toJSON',
3636
'toObject',
3737
'transactionAsyncLocalStorage',
38-
'translateAliases'
38+
'translateAliases',
39+
'updatePipeline'
3940
]);
4041

4142
module.exports = VALID_OPTIONS;

test/model.test.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6741,6 +6741,172 @@ describe('Model', function() {
67416741
});
67426742
});
67436743

6744+
describe('`updatePipeline` global option (gh-15756)', function() {
6745+
// Arrange
6746+
const originalValue = mongoose.get('updatePipeline');
6747+
6748+
afterEach(() => {
6749+
mongoose.set('updatePipeline', originalValue);
6750+
});
6751+
6752+
describe('allows update pipelines when global `updatePipeline` is `true`', function() {
6753+
it('works with updateOne', async function() {
6754+
// Arrange
6755+
const { User } = createTestContext({ globalUpdatePipeline: true });
6756+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6757+
6758+
// Act
6759+
await User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }]);
6760+
const user = await User.findById(createdUser._id);
6761+
6762+
// Assert
6763+
assert.equal(user.counter, 1);
6764+
});
6765+
6766+
it('works with updateMany', async function() {
6767+
// Arrange
6768+
const { User } = createTestContext({ globalUpdatePipeline: true });
6769+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6770+
6771+
// Act
6772+
await User.updateMany({ _id: createdUser._id }, [{ $set: { counter: 2 } }]);
6773+
const user = await User.findById(createdUser._id);
6774+
6775+
// Assert
6776+
assert.equal(user.counter, 2);
6777+
});
6778+
6779+
it('works with findOneAndUpdate', async function() {
6780+
// Arrange
6781+
const { User } = createTestContext({ globalUpdatePipeline: true });
6782+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6783+
6784+
// Act
6785+
const user = await User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 3, name: 'Hafez3' } }], { new: true });
6786+
6787+
// Assert
6788+
assert.equal(user.counter, 3);
6789+
assert.equal(user.name, 'Hafez3');
6790+
});
6791+
6792+
it('works with findByIdAndUpdate', async function() {
6793+
// Arrange
6794+
const { User } = createTestContext({ globalUpdatePipeline: true });
6795+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6796+
6797+
// Act
6798+
const user = await User.findByIdAndUpdate(createdUser._id, [{ $set: { counter: 4, name: 'Hafez4' } }], { new: true });
6799+
6800+
// Assert
6801+
assert.equal(user.counter, 4);
6802+
assert.equal(user.name, 'Hafez4');
6803+
});
6804+
});
6805+
6806+
describe('explicit `updatePipeline` option overrides global setting', function() {
6807+
it('explicit false overrides global true for updateOne', async function() {
6808+
// Arrange
6809+
const { User } = createTestContext({ globalUpdatePipeline: true });
6810+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6811+
6812+
// Act & Assert
6813+
assert.throws(
6814+
() => User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: false }),
6815+
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
6816+
);
6817+
});
6818+
6819+
it('explicit false overrides global true for findOneAndUpdate', async function() {
6820+
// Arrange
6821+
const { User } = createTestContext({ globalUpdatePipeline: true });
6822+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6823+
6824+
// Act & Assert
6825+
assert.throws(
6826+
() => User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: false }),
6827+
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
6828+
);
6829+
});
6830+
});
6831+
6832+
describe('throws error when global `updatePipeline` is false and no explicit option', function() {
6833+
it('updateOne should throw error', async function() {
6834+
// Arrange
6835+
const { User } = createTestContext({ globalUpdatePipeline: false });
6836+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6837+
6838+
// Act & Assert
6839+
assert.throws(
6840+
() => User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }]),
6841+
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
6842+
);
6843+
});
6844+
6845+
it('updateMany should throw error', async function() {
6846+
// Arrange
6847+
const { User } = createTestContext({ globalUpdatePipeline: false });
6848+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6849+
6850+
// Act & Assert
6851+
assert.throws(
6852+
() => User.updateMany({ _id: createdUser._id }, [{ $set: { counter: 1 } }]),
6853+
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
6854+
);
6855+
});
6856+
6857+
it('findOneAndUpdate should throw error', async function() {
6858+
// Arrange
6859+
const { User } = createTestContext({ globalUpdatePipeline: false });
6860+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6861+
6862+
// Act & Assert
6863+
assert.throws(
6864+
() => User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 1 } }]),
6865+
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
6866+
);
6867+
});
6868+
});
6869+
6870+
describe('explicit `updatePipeline: true` overrides global `updatePipeline: false`', function() {
6871+
it('works with updateOne', async function() {
6872+
// Arrange
6873+
const { User } = createTestContext({ globalUpdatePipeline: false });
6874+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6875+
6876+
// Act
6877+
await User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: true });
6878+
const user = await User.findById(createdUser._id);
6879+
6880+
// Assert
6881+
assert.equal(user.counter, 1);
6882+
});
6883+
6884+
it('works with findOneAndUpdate', async function() {
6885+
// Arrange
6886+
const { User } = createTestContext({ globalUpdatePipeline: false });
6887+
const createdUser = await User.create({ name: 'Hafez', counter: 0 });
6888+
6889+
// Act
6890+
const user = await User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 2, name: 'Hafez2' } }], { updatePipeline: true, new: true });
6891+
6892+
// Assert
6893+
assert.equal(user.counter, 2);
6894+
assert.equal(user.name, 'Hafez2');
6895+
});
6896+
});
6897+
6898+
function createTestContext({ globalUpdatePipeline }) {
6899+
mongoose.set('updatePipeline', globalUpdatePipeline);
6900+
const userSchema = new Schema({
6901+
name: { type: String },
6902+
counter: { type: Number, default: 0 }
6903+
});
6904+
6905+
const User = db.model('User', userSchema);
6906+
return { User };
6907+
}
6908+
});
6909+
67446910
describe('buildBulkWriteOperations() (gh-9673)', () => {
67456911
it('builds write operations', async() => {
67466912

test/types/base.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ function gh10139() {
5656
mongoose.set('timestamps.createdAt.immutable', false);
5757
}
5858

59+
function gh15756() {
60+
mongoose.set('updatePipeline', false);
61+
mongoose.set('updatePipeline', true);
62+
}
63+
5964
function gh12100() {
6065
mongoose.syncIndexes({ continueOnError: true, sparse: true });
6166
mongoose.syncIndexes({ continueOnError: false, sparse: true });
@@ -64,7 +69,8 @@ function gh12100() {
6469
function setAsObject() {
6570
mongoose.set({
6671
debug: true,
67-
autoIndex: false
72+
autoIndex: false,
73+
updatePipeline: true
6874
});
6975

7076
expectError(mongoose.set({ invalid: true }));

types/mongooseoptions.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,5 +215,14 @@ declare module 'mongoose' {
215215
* to their database property names. Defaults to false.
216216
*/
217217
translateAliases?: boolean;
218+
219+
/**
220+
* If `true`, allows passing update pipelines (arrays) to update operations by default
221+
* without explicitly setting `updatePipeline: true` in each query. This is the global
222+
* default for the `updatePipeline` query option.
223+
*
224+
* @default false
225+
*/
226+
updatePipeline?: boolean;
218227
}
219228
}

0 commit comments

Comments
 (0)