Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/migrating_to_9.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ Set `updatePipeline: true` to enable update pipelines.
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: true });
```

You can also set `updatePipeline` globally to enable update pipelines for all update operations by default.

```javascript
// Enable update pipelines globally
mongoose.set('updatePipeline', true);

// Now update pipelines work without needing to specify the option on each query
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }]);

// You can still override the global setting per query
await Model.updateOne({}, [{ $set: { newProp: 'test2' } }], { updatePipeline: false }); // throws
```

## Removed background option for indexes

[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()`.
Expand Down
5 changes: 3 additions & 2 deletions lib/mongoose.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,18 +230,19 @@ Mongoose.prototype.setDriver = function setDriver(driver) {
* - `cloneSchemas`: `false` by default. Set to `true` to `clone()` all schemas before compiling into a model.
* - `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(', ')})`.
* - `id`: If `true`, adds a `id` virtual to all schemas unless overwritten on a per-schema basis.
* - `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`
* - `maxTimeMS`: If set, attaches [maxTimeMS](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/) to every query
* - `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.
* - `overwriteModels`: Set to `true` to default to overwriting models with the same name when calling `mongoose.model()`, as opposed to throwing an `OverwriteModelError`.
* - `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.
* - `runValidators`: `false` by default. Set to true to enable [update validators](https://mongoosejs.com/docs/validation.html#update-validators) for all validators by default.
* - `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`.
* - `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.
* - `strict`: `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas.
* - `strictQuery`: `false` by default. May be `false`, `true`, or `'throw'`. Sets the default [strictQuery](https://mongoosejs.com/docs/guide.html#strictQuery) mode for schemas.
* - `strict`: `true` by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas.
* - `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`
* - `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()`
* - `toObject`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject())
* - `updatePipeline`: `false` by default. If `true`, allows passing update pipelines (arrays) to update operations by default without explicitly setting `updatePipeline: true` in each query.
*
* @param {String|Object} key The name of the option or a object of multiple key-value pairs
* @param {String|Function|Boolean} value The value of the option, unused if "key" is a object
Expand Down
24 changes: 14 additions & 10 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -3451,15 +3451,16 @@ Query.prototype.findOneAndUpdate = function(filter, update, options) {
delete options.fields;
}

const returnOriginal = this &&
this.model &&
this.model.base &&
this.model.base.options &&
this.model.base.options.returnOriginal;
const returnOriginal = this?.model?.base?.options?.returnOriginal;
if (options.new == null && options.returnDocument == null && options.returnOriginal == null && returnOriginal != null) {
options.returnOriginal = returnOriginal;
}

const updatePipeline = this?.model?.base?.options?.updatePipeline;
if (options.updatePipeline == null && updatePipeline != null) {
options.updatePipeline = updatePipeline;
}

this.setOptions(options);

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

options = options || {};

const returnOriginal = this &&
this.model &&
this.model.base &&
this.model.base.options &&
this.model.base.options.returnOriginal;
const returnOriginal = this?.model?.base?.options?.returnOriginal;
if (options.new == null && options.returnDocument == null && options.returnOriginal == null && returnOriginal != null) {
options.returnOriginal = returnOriginal;
}

this.setOptions(options);

return this;
Expand Down Expand Up @@ -4407,6 +4405,12 @@ function _update(query, op, filter, doc, options, callback) {
query.merge(filter);
}

const updatePipeline = query?.model?.base?.options?.updatePipeline;
if (updatePipeline != null && (options == null || options.updatePipeline == null)) {
options = options || {};
options.updatePipeline = updatePipeline;
}

if (utils.isObject(options)) {
query.setOptions(options);
}
Expand Down
5 changes: 3 additions & 2 deletions lib/validOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const VALID_OPTIONS = Object.freeze([
'debug',
'forceRepopulate',
'id',
'timestamps.createdAt.immutable',
'maxTimeMS',
'objectIdGetter',
'overwriteModels',
Expand All @@ -32,10 +31,12 @@ const VALID_OPTIONS = Object.freeze([
'strict',
'strictPopulate',
'strictQuery',
'timestamps.createdAt.immutable',
'toJSON',
'toObject',
'transactionAsyncLocalStorage',
'translateAliases'
'translateAliases',
'updatePipeline'
]);

module.exports = VALID_OPTIONS;
166 changes: 166 additions & 0 deletions test/model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6741,6 +6741,172 @@ describe('Model', function() {
});
});

describe('`updatePipeline` global option (gh-15756)', function() {
// Arrange
const originalValue = mongoose.get('updatePipeline');

afterEach(() => {
mongoose.set('updatePipeline', originalValue);
});

describe('allows update pipelines when global `updatePipeline` is `true`', function() {
it('works with updateOne', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: true });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act
await User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }]);
const user = await User.findById(createdUser._id);

// Assert
assert.equal(user.counter, 1);
});

it('works with updateMany', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: true });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act
await User.updateMany({ _id: createdUser._id }, [{ $set: { counter: 2 } }]);
const user = await User.findById(createdUser._id);

// Assert
assert.equal(user.counter, 2);
});

it('works with findOneAndUpdate', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: true });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act
const user = await User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 3, name: 'Hafez3' } }], { new: true });

// Assert
assert.equal(user.counter, 3);
assert.equal(user.name, 'Hafez3');
});

it('works with findByIdAndUpdate', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: true });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act
const user = await User.findByIdAndUpdate(createdUser._id, [{ $set: { counter: 4, name: 'Hafez4' } }], { new: true });

// Assert
assert.equal(user.counter, 4);
assert.equal(user.name, 'Hafez4');
});
});

describe('explicit `updatePipeline` option overrides global setting', function() {
it('explicit false overrides global true for updateOne', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: true });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act & Assert
assert.throws(
() => User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: false }),
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
);
});

it('explicit false overrides global true for findOneAndUpdate', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: true });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act & Assert
assert.throws(
() => User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: false }),
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
);
});
});

describe('throws error when global `updatePipeline` is false and no explicit option', function() {
it('updateOne should throw error', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: false });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act & Assert
assert.throws(
() => User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }]),
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
);
});

it('updateMany should throw error', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: false });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act & Assert
assert.throws(
() => User.updateMany({ _id: createdUser._id }, [{ $set: { counter: 1 } }]),
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
);
});

it('findOneAndUpdate should throw error', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: false });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act & Assert
assert.throws(
() => User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 1 } }]),
/Cannot pass an array to query updates unless the `updatePipeline` option is set/
);
});
});

describe('explicit `updatePipeline: true` overrides global `updatePipeline: false`', function() {
it('works with updateOne', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: false });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act
await User.updateOne({ _id: createdUser._id }, [{ $set: { counter: 1 } }], { updatePipeline: true });
const user = await User.findById(createdUser._id);

// Assert
assert.equal(user.counter, 1);
});

it('works with findOneAndUpdate', async function() {
// Arrange
const { User } = createTestContext({ globalUpdatePipeline: false });
const createdUser = await User.create({ name: 'Hafez', counter: 0 });

// Act
const user = await User.findOneAndUpdate({ _id: createdUser._id }, [{ $set: { counter: 2, name: 'Hafez2' } }], { updatePipeline: true, new: true });

// Assert
assert.equal(user.counter, 2);
assert.equal(user.name, 'Hafez2');
});
});

function createTestContext({ globalUpdatePipeline }) {
mongoose.set('updatePipeline', globalUpdatePipeline);
const userSchema = new Schema({
name: { type: String },
counter: { type: Number, default: 0 }
});

const User = db.model('User', userSchema);
return { User };
}
});

describe('buildBulkWriteOperations() (gh-9673)', () => {
it('builds write operations', async() => {

Expand Down
8 changes: 7 additions & 1 deletion test/types/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ function gh10139() {
mongoose.set('timestamps.createdAt.immutable', false);
}

function gh15756() {
mongoose.set('updatePipeline', false);
mongoose.set('updatePipeline', true);
}

function gh12100() {
mongoose.syncIndexes({ continueOnError: true, sparse: true });
mongoose.syncIndexes({ continueOnError: false, sparse: true });
Expand All @@ -64,7 +69,8 @@ function gh12100() {
function setAsObject() {
mongoose.set({
debug: true,
autoIndex: false
autoIndex: false,
updatePipeline: true
});

expectError(mongoose.set({ invalid: true }));
Expand Down
9 changes: 9 additions & 0 deletions types/mongooseoptions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,5 +215,14 @@ declare module 'mongoose' {
* to their database property names. Defaults to false.
*/
translateAliases?: boolean;

/**
* If `true`, allows passing update pipelines (arrays) to update operations by default
* without explicitly setting `updatePipeline: true` in each query. This is the global
* default for the `updatePipeline` query option.
*
* @default false
*/
updatePipeline?: boolean;
}
}