diff --git a/lib/document.js b/lib/document.js index 5b9d3fcc5c7..bd28b440222 100644 --- a/lib/document.js +++ b/lib/document.js @@ -4582,6 +4582,38 @@ Document.prototype.getChanges = function() { return changes; }; +/** + * Returns a copy of this document with a deep clone of `_doc` and `$__`. + * + * @return {Document} a copy of this document + * @api private + * @method $clone + * @memberOf Document + * @instance + */ + +Document.prototype.$clone = function() { + const Model = this.constructor; + const clonedDoc = new Model(); + clonedDoc.$isNew = this.$isNew; + if (this._doc) { + clonedDoc._doc = clone(this._doc); + } + if (this.$__) { + const Cache = this.$__.constructor; + const clonedCache = new Cache(); + for (const key of Object.getOwnPropertyNames(this.$__)) { + if (key === 'activePaths') { + continue; + } + clonedCache[key] = clone(this.$__[key]); + } + Object.assign(clonedCache.activePaths, clone({ ...this.$__.activePaths })); + clonedDoc.$__ = clonedCache; + } + return clonedDoc; +}; + /*! * Module exports. */ diff --git a/test/document.test.js b/test/document.test.js index df17aa9da77..b0f4ea132c3 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11929,6 +11929,64 @@ describe('document', function() { assert.deepStrictEqual(rawDoc.tags, ['mongodb']); }); + it('$clone() (gh-11849)', async function() { + const schema = new mongoose.Schema({ + name: { + type: String, + validate: { + validator: (v) => v !== 'Invalid' + } + } + }); + const Test = db.model('Test', schema); + + const item = await Test.create({ name: 'Test' }); + + const doc = await Test.findById(item._id); + const clonedDoc = doc.$clone(); + + assert.deepEqual(clonedDoc, doc); + assert.deepEqual(clonedDoc._doc, doc._doc); + assert.deepEqual(clonedDoc.$__, doc.$__); + + // Editing a field in the cloned doc does not effect + // the original doc + clonedDoc.name = 'Test 2'; + assert.equal(doc.name, 'Test'); + assert.equal(clonedDoc.name, 'Test 2'); + assert.ok(!doc.$isModified('name')); + assert.ok(clonedDoc.$isModified('name')); + + // Saving the cloned doc does not effect `modifiedPaths` + // in the original doc + const modifiedPaths = [...doc.modifiedPaths()]; + await clonedDoc.save(); + assert.deepEqual(doc.modifiedPaths(), modifiedPaths); + + // Cloning a doc with invalid field preserve the + // invalid field value + doc.name = 'Invalid'; + await assert.rejects(async() => { + await doc.validate(); + }); + + await clonedDoc.validate(); + + const invalidClonedDoc = doc.$clone(); + doc.name = 'Test'; + await doc.validate(); + await assert.rejects(async() => { + await invalidClonedDoc.validate(); + }); + + // Setting a session on the cloned doc does not + // affect the session in the original doc + const session = await Test.startSession(); + clonedDoc.$session(session); + assert.strictEqual(doc.$session(), null); + assert.strictEqual(clonedDoc.$session(), session); + }); + it('can create document with document array and top-level key named `schema` (gh-12480)', async function() { const AuthorSchema = new Schema({ fullName: { type: 'String', required: true }