Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(document): add $clone method #12549

Merged
merged 8 commits into from Oct 24, 2022
Merged
32 changes: 32 additions & 0 deletions lib/document.js
Expand Up @@ -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.
*/
Expand Down
58 changes: 58 additions & 0 deletions test/document.test.js
Expand Up @@ -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 }
Expand Down