From 2481079bde95017b3b48321e493857009be9245c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Oct 2022 21:41:29 -0400 Subject: [PATCH] feat(document): add `$timestamps()` method to set timestamps for `save()`, `bulkSave()`, and `insertMany()` Fix #12117 --- lib/document.js | 42 +++++++++++++++++++++++++++++++++++++++++ lib/model.js | 13 ++++++++++++- lib/schema.js | 1 + test/model.test.js | 39 ++++++++++++++++++++++++++++++++++++++ test/timestamps.test.js | 14 ++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) diff --git a/lib/document.js b/lib/document.js index 5b9d3fcc5c7..6debc4014e6 100644 --- a/lib/document.js +++ b/lib/document.js @@ -948,6 +948,48 @@ Document.prototype.$session = function $session(session) { return session; }; +/** + * Getter/setter around whether this document will apply timestamps by + * default when using `save()` and `bulkSave()`. + * + * #### Example: + * + * const TestModel = mongoose.model('Test', new Schema({ name: String }, { timestamps: true })); + * const doc = new TestModel({ name: 'John Smith' }); + * + * doc.$timestamps(); // true + * + * doc.$timestamps(false); + * await doc.save(); // Does **not** apply timestamps + * + * @param {Boolean} [value] overwrite the current session + * @return {Document} this + * @method $timestamps + * @api public + * @memberOf Document + */ + +Document.prototype.$timestamps = function $timestamps(value) { + if (arguments.length === 0) { + if (this.$__.timestamps != null) { + return this.$__.timestamps; + } + + if (this.$__schema) { + return this.$__schema.options.timestamps; + } + + return undefined; + } + + const currentValue = this.$timestamps(); + if (value !== currentValue) { + this.$__.timestamps = value; + } + + return this; +}; + /** * Overwrite all values in this document with the values of `obj`, except * for immutable properties. Behaves similarly to `set()`, except for it diff --git a/lib/model.js b/lib/model.js index 9c3bb658b04..5fbda039dc3 100644 --- a/lib/model.js +++ b/lib/model.js @@ -511,6 +511,9 @@ Model.prototype.save = function(options, fn) { if (options.hasOwnProperty('session')) { this.$session(options.session); } + if (this.$__.timestamps != null) { + options.timestamps = this.$__.timestamps; + } this.$__.$versionError = generateVersionError(this, this.modifiedPaths()); fn = this.constructor.$handleCallbackError(fn); @@ -3455,7 +3458,8 @@ Model.$__insertMany = function(arr, options, callback) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } - if ((!options || options.timestamps !== false) && doc.initializeTimestamps) { + const shouldSetTimestamps = (!options || options.timestamps !== false) && doc.initializeTimestamps && (!doc.$__ || doc.$__.timestamps !== false); + if (shouldSetTimestamps) { return doc.initializeTimestamps().toObject(internalToObjectOptions); } return doc.toObject(internalToObjectOptions); @@ -3696,6 +3700,13 @@ Model.bulkSave = async function(documents, options) { document.$__.saveOptions = document.$__.saveOptions || {}; document.$__.saveOptions.timestamps = options.timestamps; } + } else { + for (const document of documents) { + if (document.$__.timestamps != null) { + document.$__.saveOptions = document.$__.saveOptions || {}; + document.$__.saveOptions.timestamps = document.$__.timestamps; + } + } } await Promise.all(documents.map(buildPreSavePromise)); diff --git a/lib/schema.js b/lib/schema.js index 00680194085..ba8435e0e66 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1789,6 +1789,7 @@ Schema.prototype.post = function(name) { * * @param {Function} plugin The Plugin's callback * @param {Object} [opts] Options to pass to the plugin + * @param {Boolean} [opts.deduplicate=false] If true, ignore duplicate plugins (same `fn` argument using `===`) * @see plugins /docs/plugins.html * @api public */ diff --git a/test/model.test.js b/test/model.test.js index 36de9589f18..3c6585db5ce 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4388,6 +4388,23 @@ describe('Model', function() { }); }); + it('timestamps respect $timestamps() (gh-12117)', async function() { + const schema = new Schema({ name: String }, { timestamps: true }); + const Movie = db.model('Movie', schema); + const start = Date.now(); + + const arr = [ + new Movie({ name: 'Star Wars' }), + new Movie({ name: 'The Empire Strikes Back' }) + ]; + arr[1].$timestamps(false); + + await Movie.insertMany(arr); + const docs = await Movie.find().sort({ name: 1 }); + assert.ok(docs[0].createdAt.valueOf() >= start); + assert.ok(!docs[1].createdAt); + }); + it('insertMany() with nested timestamps (gh-12060)', async function() { const childSchema = new Schema({ name: { type: String } }, { _id: false, @@ -8441,6 +8458,7 @@ describe('Model', function() { assert.ok(userToUpdate.createdAt); assert.ok(userToUpdate.updatedAt); }); + it('`timestamps` has `undefined` as default value (gh-12059)', async() => { // Arrange const userSchema = new Schema({ @@ -8462,6 +8480,27 @@ describe('Model', function() { assert.ok(userToUpdate.createdAt); assert.ok(userToUpdate.updatedAt); }); + + it('respects `$timestamps()` (gh-12117)', async function() { + // Arrange + const userSchema = new Schema({ name: String }, { timestamps: true }); + + const User = db.model('User', userSchema); + + const newUser1 = new User({ name: 'John' }); + const newUser2 = new User({ name: 'Bill' }); + + newUser2.$timestamps(false); + + // Act + await User.bulkSave([newUser1, newUser2]); + + // Assert + assert.ok(newUser1.createdAt); + assert.ok(newUser1.updatedAt); + assert.ok(!newUser2.createdAt); + assert.ok(!newUser2.updatedAt); + }); }); describe('Setting the explain flag', function() { diff --git a/test/timestamps.test.js b/test/timestamps.test.js index a12b29192cd..1315c16af16 100644 --- a/test/timestamps.test.js +++ b/test/timestamps.test.js @@ -626,6 +626,20 @@ describe('timestamps', function() { assert.strictEqual(cat.updatedAt, old); }); + it('can skip with `$timestamps(false)` (gh-12117)', async function() { + const cat = await Cat.findOne(); + const old = cat.updatedAt; + + await delay(10); + + cat.hobby = 'fishing'; + + cat.$timestamps(false); + await cat.save(); + + assert.strictEqual(cat.updatedAt, old); + }); + it('should change updatedAt when findOneAndUpdate', function(done) { Cat.create({ name: 'test123' }, function(err) { assert.ifError(err);