Skip to content

Commit

Permalink
fix(query): add overwriteImmutable option to allow updating immutab…
Browse files Browse the repository at this point in the history
…le properties without disabling strict mode

Fix #8619
  • Loading branch information
vkarpov15 committed Nov 1, 2024
1 parent 10a3aed commit 5062842
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 4 deletions.
17 changes: 17 additions & 0 deletions docs/timestamps.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,20 @@ await User.findOneAndUpdate({}, { $setOnInsert: { updatedAt: new Date() } }, {
timestamps: { createdAt: true, updatedAt: false }
});
```

## Updating Timestamps

If you need to disable Mongoose's timestamps and update a document's timestamps to a different value using `updateOne()` or `findOneAndUpdate()`, you need to do the following:

1. Set the `timestamps` option to `false` to prevent Mongoose from setting `updatedAt`.
2. Set `overwriteImmutable` to `false` to allow overwriting `createdAt`, which is an immutable property by default.

```javascript
const createdAt = new Date('2011-06-01');
// Update a document's `createdAt` to a custom value. Normally Mongoose would prevent doing this because `createdAt`
// is immutable.
await Model.updateOne({ _id: doc._id }, { createdAt }, { overwriteImmutable: true, timestamps: false });

doc = await Model.collection.findOne({ _id: doc._id });
doc.createdAt.valueOf() === createdAt.valueOf(); // true
```
4 changes: 2 additions & 2 deletions lib/helpers/query/castUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
}

if (op !== '$setOnInsert' &&
handleImmutable(schematype, strict, obj, key, prefix + key, context)) {
handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) {
continue;
}

Expand Down Expand Up @@ -353,7 +353,7 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {

// You can use `$setOnInsert` with immutable keys
if (op !== '$setOnInsert' &&
handleImmutable(schematype, strict, obj, key, prefix + key, context)) {
handleImmutable(schematype, strict, obj, key, prefix + key, options, context)) {
continue;
}

Expand Down
5 changes: 4 additions & 1 deletion lib/helpers/query/handleImmutable.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const StrictModeError = require('../../error/strict');

module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, ctx) {
module.exports = function handleImmutable(schematype, strict, obj, key, fullPath, options, ctx) {
if (schematype == null || !schematype.options || !schematype.options.immutable) {
return false;
}
Expand All @@ -15,6 +15,9 @@ module.exports = function handleImmutable(schematype, strict, obj, key, fullPath
return false;
}

if (options && options.overwriteImmutable) {
return false;
}
if (strict === false) {
return false;
}
Expand Down
11 changes: 10 additions & 1 deletion lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,7 @@ Query.prototype.getOptions = function() {
* - [writeConcern](https://www.mongodb.com/docs/manual/reference/method/db.collection.update/)
* - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): If `timestamps` is set in the schema, set this option to `false` to skip timestamps for that particular update. Has no effect if `timestamps` is not enabled in the schema options.
* - overwriteDiscriminatorKey: allow setting the discriminator key in the update. Will use the correct discriminator schema if the update changes the discriminator key.
* - overwriteImmutable: allow overwriting properties that are set to `immutable` in the schema. Defaults to false.
*
* The following options are only for `find()`, `findOne()`, `findById()`, `findOneAndUpdate()`, `findOneAndReplace()`, `findOneAndDelete()`, and `findByIdAndUpdate()`:
*
Expand Down Expand Up @@ -1697,6 +1698,10 @@ Query.prototype.setOptions = function(options, overwrite) {
this._mongooseOptions.overwriteDiscriminatorKey = options.overwriteDiscriminatorKey;
delete options.overwriteDiscriminatorKey;
}
if ('overwriteImmutable' in options) {
this._mongooseOptions.overwriteImmutable = options.overwriteImmutable;
delete options.overwriteImmutable;
}
if ('sanitizeProjection' in options) {
if (options.sanitizeProjection && !this._mongooseOptions.sanitizeProjection) {
sanitizeProjection(this._fields);
Expand Down Expand Up @@ -3318,6 +3323,7 @@ function prepareDiscriminatorCriteria(query) {
* @param {Boolean} [options.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`.
* @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
* @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert), set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
* @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html
* @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/
* @see ModifyResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html
Expand Down Expand Up @@ -4016,6 +4022,7 @@ Query.prototype._replaceOne = async function _replaceOne() {
* @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
* @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
* @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert), set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
* @param {Function} [callback] params are (error, writeOpResult)
* @return {Query} this
* @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
Expand Down Expand Up @@ -4086,6 +4093,7 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) {
* @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
* @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
* @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
* @param {Boolean} [options.overwriteImmutable=false] Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert), set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
* @param {Function} [callback] params are (error, writeOpResult)
* @return {Query} this
* @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update()
Expand Down Expand Up @@ -4707,7 +4715,8 @@ Query.prototype._castUpdate = function _castUpdate(obj) {
strict: this._mongooseOptions.strict,
upsert: upsert,
arrayFilters: this.options.arrayFilters,
overwriteDiscriminatorKey: this._mongooseOptions.overwriteDiscriminatorKey
overwriteDiscriminatorKey: this._mongooseOptions.overwriteDiscriminatorKey,
overwriteImmutable: this._mongooseOptions.overwriteImmutable
}, this, this._conditions);
};

Expand Down
23 changes: 23 additions & 0 deletions test/model.updateOne.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2526,6 +2526,29 @@ describe('model: updateOne: ', function() {
assert.ok(doc.createdAt.valueOf() >= start);
});

it('overwriting immutable createdAt (gh-8619)', async function() {
const start = new Date().valueOf();
const schema = Schema({
createdAt: {
type: mongoose.Schema.Types.Date,
immutable: true
},
name: String
}, { timestamps: true });

const Model = db.model('Test', schema);

await Model.create({ name: 'gh-8619' });
let doc = await Model.collection.findOne({ name: 'gh-8619' });
assert.ok(doc.createdAt.valueOf() >= start);

const createdAt = new Date('2011-06-01');
assert.ok(createdAt.valueOf() < start.valueOf());
await Model.updateOne({ _id: doc._id }, { name: 'gh-8619 update', createdAt }, { overwriteImmutable: true, timestamps: false });
doc = await Model.collection.findOne({ name: 'gh-8619 update' });
assert.equal(doc.createdAt.valueOf(), createdAt.valueOf());
});

it('conditional immutable (gh-8001)', async function() {
const schema = Schema({
test: {
Expand Down
6 changes: 6 additions & 0 deletions types/query.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ declare module 'mongoose' {
| 'context'
| 'multipleCastError'
| 'overwriteDiscriminatorKey'
| 'overwriteImmutable'
| 'populate'
| 'runValidators'
| 'sanitizeProjection'
Expand Down Expand Up @@ -154,6 +155,11 @@ declare module 'mongoose' {
new?: boolean;

overwriteDiscriminatorKey?: boolean;
/**
* Mongoose removes updated immutable properties from `update` by default (excluding $setOnInsert).
* Set `overwriteImmutable` to `true` to allow updating immutable properties using other update operators.
*/
overwriteImmutable?: boolean;
projection?: ProjectionType<DocType>;
/**
* if true, returns the full ModifyResult rather than just the document
Expand Down

0 comments on commit 5062842

Please sign in to comment.