From 4160245ff8697bd36da89d10a4b27eb5d62fe80d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 15 Jul 2022 14:05:42 -0400 Subject: [PATCH] fix(model+timestamps): set timestamps on subdocuments in `insertMany()` Fix #12060 --- .../timestamps/setDocumentTimestamps.js | 26 +++++++ lib/helpers/timestamps/setupTimestamps.js | 32 ++++---- lib/model.js | 2 +- test/model.test.js | 74 +++++++++++++------ 4 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 lib/helpers/timestamps/setDocumentTimestamps.js diff --git a/lib/helpers/timestamps/setDocumentTimestamps.js b/lib/helpers/timestamps/setDocumentTimestamps.js new file mode 100644 index 00000000000..c1b6d5fc2c1 --- /dev/null +++ b/lib/helpers/timestamps/setDocumentTimestamps.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = function setDocumentTimestamps(doc, timestampOption, currentTime, createdAt, updatedAt) { + const skipUpdatedAt = timestampOption != null && timestampOption.updatedAt === false; + const skipCreatedAt = timestampOption != null && timestampOption.createdAt === false; + + const defaultTimestamp = currentTime != null ? + currentTime() : + doc.ownerDocument().constructor.base.now(); + + if (!skipCreatedAt && + (doc.isNew || doc.$isSubdocument) && + createdAt && + !doc.$__getValue(createdAt) && + doc.$__isSelected(createdAt)) { + doc.$set(createdAt, defaultTimestamp, undefined, { overwriteImmutable: true }); + } + + if (!skipUpdatedAt && updatedAt && (doc.isNew || doc.$isModified())) { + let ts = defaultTimestamp; + if (doc.isNew && createdAt != null) { + ts = doc.$__getValue(createdAt); + } + doc.$set(updatedAt, ts); + } +}; diff --git a/lib/helpers/timestamps/setupTimestamps.js b/lib/helpers/timestamps/setupTimestamps.js index c254365ac3e..06c57ea74f8 100644 --- a/lib/helpers/timestamps/setupTimestamps.js +++ b/lib/helpers/timestamps/setupTimestamps.js @@ -4,6 +4,7 @@ const applyTimestampsToChildren = require('../update/applyTimestampsToChildren') const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate'); const get = require('../get'); const handleTimestampOption = require('../schema/handleTimestampOption'); +const setDocumentTimestamps = require('./setDocumentTimestamps'); const symbols = require('../../schema/symbols'); module.exports = function setupTimestamps(schema, timestamps) { @@ -44,24 +45,7 @@ module.exports = function setupTimestamps(schema, timestamps) { return next(); } - const skipUpdatedAt = timestampOption != null && timestampOption.updatedAt === false; - const skipCreatedAt = timestampOption != null && timestampOption.createdAt === false; - - const defaultTimestamp = currentTime != null ? - currentTime() : - this.ownerDocument().constructor.base.now(); - - if (!skipCreatedAt && (this.isNew || this.$isSubdocument) && createdAt && !this.$__getValue(createdAt) && this.$__isSelected(createdAt)) { - this.$set(createdAt, defaultTimestamp, undefined, { overwriteImmutable: true }); - } - - if (!skipUpdatedAt && updatedAt && (this.isNew || this.$isModified())) { - let ts = defaultTimestamp; - if (this.isNew && createdAt != null) { - ts = this.$__getValue(createdAt); - } - this.$set(updatedAt, ts); - } + setDocumentTimestamps(this, timestampOption, currentTime, createdAt, updatedAt); next(); }); @@ -76,6 +60,18 @@ module.exports = function setupTimestamps(schema, timestamps) { if (updatedAt && !this.get(updatedAt)) { this.$set(updatedAt, ts); } + + if (this.$isSubdocument) { + return this; + } + + const subdocs = this.$getAllSubdocs(); + for (const subdoc of subdocs) { + if (subdoc.initializeTimestamps) { + subdoc.initializeTimestamps(); + } + } + return this; }; diff --git a/lib/model.js b/lib/model.js index 5a7a95a7e0e..c386077e880 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3396,7 +3396,7 @@ Model.$__insertMany = function(arr, options, callback) { if (doc.$__schema.options.versionKey) { doc[doc.$__schema.options.versionKey] = 0; } - if (doc.initializeTimestamps) { + if ((!options || options.timestamps !== false) && doc.initializeTimestamps) { return doc.initializeTimestamps().toObject(internalToObjectOptions); } return doc.toObject(internalToObjectOptions); diff --git a/test/model.test.js b/test/model.test.js index 39f3a5dfeeb..7ecc82b40c5 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4346,6 +4346,58 @@ describe('Model', function() { await db.close(); }); + describe('insertMany()', function() { + it('with timestamps (gh-723)', function() { + const schema = new Schema({ name: String }, { timestamps: true }); + const Movie = db.model('Movie', schema); + const start = Date.now(); + + const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; + return Movie.insertMany(arr). + then(docs => { + assert.equal(docs.length, 2); + assert.ok(!docs[0].isNew); + assert.ok(!docs[1].isNew); + assert.ok(docs[0].createdAt.valueOf() >= start); + assert.ok(docs[1].createdAt.valueOf() >= start); + }). + then(() => Movie.find()). + then(docs => { + assert.equal(docs.length, 2); + assert.ok(docs[0].createdAt.valueOf() >= start); + assert.ok(docs[1].createdAt.valueOf() >= start); + }); + }); + + it('insertMany() with nested timestamps (gh-12060)', async function() { + const childSchema = new Schema({ name: { type: String } }, { + _id: false, + timestamps: true + }); + + const parentSchema = new Schema({ child: childSchema }, { + timestamps: true + }); + + const Test = db.model('Test', parentSchema); + + await Test.insertMany([{ child: { name: 'test' } }]); + let docs = await Test.find(); + + assert.equal(docs.length, 1); + assert.equal(docs[0].child.name, 'test'); + assert.ok(docs[0].child.createdAt); + assert.ok(docs[0].child.updatedAt); + + await Test.insertMany([{ child: { name: 'test2' } }], { timestamps: false }); + docs = await Test.find({ 'child.name': 'test2' }); + assert.equal(docs.length, 1); + assert.equal(docs[0].child.name, 'test2'); + assert.ok(!docs[0].child.createdAt); + assert.ok(!docs[0].child.updatedAt); + }); + }); + describe('bug fixes', function() { it('doesnt crash (gh-1920)', function(done) { const parentSchema = new Schema({ @@ -4638,28 +4690,6 @@ describe('Model', function() { }); }); - it('insertMany() with timestamps (gh-723)', function() { - const schema = new Schema({ name: String }, { timestamps: true }); - const Movie = db.model('Movie', schema); - const start = Date.now(); - - const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]; - return Movie.insertMany(arr). - then(docs => { - assert.equal(docs.length, 2); - assert.ok(!docs[0].isNew); - assert.ok(!docs[1].isNew); - assert.ok(docs[0].createdAt.valueOf() >= start); - assert.ok(docs[1].createdAt.valueOf() >= start); - }). - then(() => Movie.find()). - then(docs => { - assert.equal(docs.length, 2); - assert.ok(docs[0].createdAt.valueOf() >= start); - assert.ok(docs[1].createdAt.valueOf() >= start); - }); - }); - it('returns empty array if no documents (gh-8130)', function() { const Movie = db.model('Movie', Schema({ name: String })); return Movie.insertMany([]).then(docs => assert.deepEqual(docs, []));