From 50b7bc99d41bbbfc3a35b5357f38903f710de24d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 22 Sep 2024 13:24:07 -0400 Subject: [PATCH 1/7] feat(model): add `Model.applyVirtuals()` to apply virtuals to a POJO Fix #14818 --- lib/helpers/document/applyVirtuals.js | 141 ++++++++++++++++++ lib/model.js | 38 +++++ test/model.test.js | 202 ++++++++++++++++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 lib/helpers/document/applyVirtuals.js diff --git a/lib/helpers/document/applyVirtuals.js b/lib/helpers/document/applyVirtuals.js new file mode 100644 index 00000000000..e3869440b90 --- /dev/null +++ b/lib/helpers/document/applyVirtuals.js @@ -0,0 +1,141 @@ +'use strict'; + +const mpath = require('mpath'); + +module.exports = applyVirtuals; + +function applyVirtuals(schema, doc, virtuals, parent) { + if (doc == null) { + return doc; + } + + let virtualsForChildren = virtuals; + let toApply = null; + + if (Array.isArray(virtuals)) { + virtualsForChildren = []; + toApply = []; + const len = virtuals.length; + for (let i = 0; i < len; ++i) { + const virtual = virtuals[i]; + if (virtual.length === 1) { + toApply.push(virtual[0]); + } else { + virtualsForChildren.push(virtual); + } + } + } + + applyVirtualsToChildren(this, schema, doc, virtualsForChildren, parent); + return applyVirtualsToDocs(schema, doc, toApply); +} + +function applyVirtualsToDocs(schema, res, toApply) { + if (Array.isArray(res)) { + const len = res.length; + for (let i = 0; i < len; ++i) { + applyVirtualsToDoc(schema, res[i], toApply); + } + return res; + } else { + return applyVirtualsToDoc(schema, res, toApply); + } +} + +function applyVirtualsToChildren(doc, schema, res, virtuals, parent) { + const len = schema.childSchemas.length; + let attachedVirtuals = false; + for (let i = 0; i < len; ++i) { + const _path = schema.childSchemas[i].model.path; + const _schema = schema.childSchemas[i].schema; + if (!_path) { + continue; + } + const _doc = mpath.get(_path, res); + if (_doc == null || (Array.isArray(_doc) && _doc.flat(Infinity).length === 0)) { + continue; + } + + let virtualsForChild = null; + if (Array.isArray(virtuals)) { + virtualsForChild = []; + const len = virtuals.length; + for (let i = 0; i < len; ++i) { + const virtual = virtuals[i]; + if (virtual[0] == _path) { + virtualsForChild.push(virtual.slice(1)); + } + } + + if (virtualsForChild.length === 0) { + continue; + } + } + + applyVirtuals.call(doc, _schema, _doc, virtualsForChild, res); + attachedVirtuals = true; + } + + if (virtuals && virtuals.length && !attachedVirtuals) { + applyVirtualsToDoc(schema, res, virtuals, parent); + } +} + +function applyVirtualsToDoc(schema, doc, virtuals) { + if (doc == null || typeof doc !== 'object') { + return; + } + if (Array.isArray(doc)) { + for (let i = 0; i < doc.length; ++i) { + applyVirtualsToDoc(schema, doc[i], virtuals); + } + return; + } + + if (schema.discriminators && Object.keys(schema.discriminators).length > 0) { + for (const discriminatorKey of Object.keys(schema.discriminators)) { + const discriminator = schema.discriminators[discriminatorKey]; + const key = discriminator.discriminatorMapping.key; + const value = discriminator.discriminatorMapping.value; + if (doc[key] == value) { + schema = discriminator; + break; + } + } + } + + if (virtuals == null) { + virtuals = Object.keys(schema.virtuals); + } + const numVirtuals = virtuals.length; + for (let i = 0; i < numVirtuals; ++i) { + const virtual = virtuals[i]; + if (schema.virtuals[virtual] == null) { + continue; + } + const virtualType = schema.virtuals[virtual]; + const sp = Array.isArray(virtual) + ? virtual : + virtual.indexOf('.') === -1 + ? [virtual] + : virtual.split('.'); + let cur = doc; + for (let j = 0; j < sp.length - 1; ++j) { + cur[sp[j]] = sp[j] in cur ? cur[sp[j]] : {}; + cur = cur[sp[j]]; + } + let val = virtualType.applyGetters(cur[sp[sp.length - 1]], doc); + if (isPopulateVirtual(virtualType) && val === undefined) { + if (virtualType.options.justOne) { + val = null; + } else { + val = []; + } + } + cur[sp[sp.length - 1]] = val; + } +} + +function isPopulateVirtual(virtualType) { + return virtualType.options && (virtualType.options.ref || virtualType.options.refPath); +} diff --git a/lib/model.js b/lib/model.js index 6d9458b5d77..777a36607a9 100644 --- a/lib/model.js +++ b/lib/model.js @@ -31,6 +31,7 @@ const applySchemaCollation = require('./helpers/indexes/applySchemaCollation'); const applyStaticHooks = require('./helpers/model/applyStaticHooks'); const applyStatics = require('./helpers/model/applyStatics'); const applyWriteConcern = require('./helpers/schema/applyWriteConcern'); +const applyVirtualsHelper = require('./helpers/document/applyVirtuals'); const assignVals = require('./helpers/populate/assignVals'); const castBulkWrite = require('./helpers/model/castBulkWrite'); const clone = require('./helpers/clone'); @@ -3488,6 +3489,9 @@ function handleSuccessfulWrite(document) { */ Model.applyDefaults = function applyDefaults(doc) { + if (doc == null) { + return doc; + } if (doc.$__ != null) { applyDefaultsHelper(doc, doc.$__.fields, doc.$__.exclude); @@ -3503,6 +3507,40 @@ Model.applyDefaults = function applyDefaults(doc) { return doc; }; +/** + * Apply this model's virtuals to a given POJO. Virtuals execute with the POJO as the context `this`. + * + * #### Example: + * + * const userSchema = new Schema({ name: String }); + * userSchema.virtual('upper').get(function() { return this.name.toUpperCase(); }); + * const User = mongoose.model('User', userSchema); + * + * const obj = { name: 'John' }; + * User.applyVirtuals(obj); + * obj.name; // 'John' + * obj.upper; // 'JOHN', Mongoose applied the return value of the virtual to the given object + * + * @param {Object} obj object or document to apply virtuals on + * @param {Array} [virtualsToApply] optional whitelist of virtuals to apply + * @returns {Object} obj + * @api public + */ + +Model.applyVirtuals = function applyVirtuals(doc, virtualsToApply) { + if (doc == null) { + return doc; + } + // Nothing to do if this is already a hydrated document - it should already have virtuals + if (doc.$__ != null) { + return doc; + } + + applyVirtualsHelper(this.schema, doc, virtualsToApply, null); + + return doc; +}; + /** * Cast the given POJO to the model's schema * diff --git a/test/model.test.js b/test/model.test.js index 1e531c097b5..242cd2333c2 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -7781,6 +7781,208 @@ describe('Model', function() { docs = await User.find(); assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]); }); + + describe('applyVirtuals', function() { + it('handles basic top-level virtuals', async function() { + const userSchema = new Schema({ + name: String + }); + userSchema.virtual('lowercase').get(function() { + return this.name.toLowerCase(); + }); + userSchema.virtual('uppercase').get(function() { + return this.name.toUpperCase(); + }); + const User = db.model('User', userSchema); + + const res = User.applyVirtuals({ name: 'Taco' }); + assert.equal(res.name, 'Taco'); + assert.equal(res.lowercase, 'taco'); + assert.equal(res.uppercase, 'TACO'); + }); + + it('handles virtuals in subdocuments', async function() { + const userSchema = new Schema({ + name: String + }); + userSchema.virtual('lowercase').get(function() { + return this.name.toLowerCase(); + }); + userSchema.virtual('uppercase').get(function() { + return this.name.toUpperCase(); + }); + const groupSchema = new Schema({ + name: String, + leader: userSchema, + members: [userSchema] + }); + const Group = db.model('Group', groupSchema); + + const res = Group.applyVirtuals({ + name: 'Microsoft', + leader: { name: 'Bill' }, + members: [{ name: 'John' }, { name: 'Steve' }] + }); + assert.equal(res.name, 'Microsoft'); + assert.equal(res.leader.name, 'Bill'); + assert.equal(res.leader.uppercase, 'BILL'); + assert.equal(res.leader.lowercase, 'bill'); + assert.equal(res.members[0].name, 'John'); + assert.equal(res.members[0].uppercase, 'JOHN'); + assert.equal(res.members[0].lowercase, 'john'); + assert.equal(res.members[1].name, 'Steve'); + assert.equal(res.members[1].uppercase, 'STEVE'); + assert.equal(res.members[1].lowercase, 'steve'); + }); + + it('handles virtuals on nested paths', async function() { + const userSchema = new Schema({ + name: { + first: String, + last: String + } + }); + userSchema.virtual('name.firstUpper').get(function() { + return this.name.first.toUpperCase(); + }); + userSchema.virtual('name.lastLower').get(function() { + return this.name.last.toLowerCase(); + }); + const User = db.model('User', userSchema); + + const res = User.applyVirtuals({ + name: { + first: 'Bill', + last: 'Gates' + } + }); + assert.equal(res.name.first, 'Bill'); + assert.equal(res.name.last, 'Gates'); + assert.equal(res.name.firstUpper, 'BILL'); + assert.equal(res.name.lastLower, 'gates'); + }); + + it('supports passing an array of virtuals to apply', async function() { + const userSchema = new Schema({ + name: { + first: String, + last: String + } + }); + userSchema.virtual('fullName').get(function() { + return `${this.name.first} ${this.name.last}`; + }); + userSchema.virtual('name.firstUpper').get(function() { + return this.name.first.toUpperCase(); + }); + userSchema.virtual('name.lastLower').get(function() { + return this.name.last.toLowerCase(); + }); + const User = db.model('User', userSchema); + + let res = User.applyVirtuals({ + name: { + first: 'Bill', + last: 'Gates' + } + }, ['fullName', 'name.firstUpper']); + assert.strictEqual(res.name.first, 'Bill'); + assert.strictEqual(res.name.last, 'Gates'); + assert.strictEqual(res.fullName, 'Bill Gates'); + assert.strictEqual(res.name.firstUpper, 'BILL'); + assert.strictEqual(res.name.lastLower, undefined); + + res = User.applyVirtuals({ + name: { + first: 'Bill', + last: 'Gates' + } + }, ['name.lastLower']); + assert.strictEqual(res.name.first, 'Bill'); + assert.strictEqual(res.name.last, 'Gates'); + assert.strictEqual(res.fullName, undefined); + assert.strictEqual(res.name.firstUpper, undefined); + assert.strictEqual(res.name.lastLower, 'gates'); + }); + + it('supports passing an array of virtuals to apply', async function() { + const userSchema = new Schema({ + name: { + first: String, + last: String + } + }); + userSchema.virtual('fullName').get(function() { + return `${this.name.first} ${this.name.last}`; + }); + userSchema.virtual('name.firstUpper').get(function() { + return this.name.first.toUpperCase(); + }); + userSchema.virtual('name.lastLower').get(function() { + return this.name.last.toLowerCase(); + }); + const User = db.model('User', userSchema); + + let res = User.applyVirtuals({ + name: { + first: 'Bill', + last: 'Gates' + } + }, ['fullName', 'name.firstUpper']); + assert.strictEqual(res.name.first, 'Bill'); + assert.strictEqual(res.name.last, 'Gates'); + assert.strictEqual(res.fullName, 'Bill Gates'); + assert.strictEqual(res.name.firstUpper, 'BILL'); + assert.strictEqual(res.name.lastLower, undefined); + + res = User.applyVirtuals({ + name: { + first: 'Bill', + last: 'Gates' + } + }, ['name.lastLower']); + assert.strictEqual(res.name.first, 'Bill'); + assert.strictEqual(res.name.last, 'Gates'); + assert.strictEqual(res.fullName, undefined); + assert.strictEqual(res.name.firstUpper, undefined); + assert.strictEqual(res.name.lastLower, 'gates'); + }); + + it('sets populate virtuals to `null` if `justOne`', async function() { + const userSchema = new Schema({ + name: { + first: String, + last: String + }, + friendId: { + type: 'ObjectId' + } + }); + userSchema.virtual('fullName').get(function() { + return `${this.name.first} ${this.name.last}`; + }); + userSchema.virtual('friend', { + ref: 'User', + localField: 'friendId', + foreignField: '_id', + justOne: true + }); + const User = db.model('User', userSchema); + + const friendId = new mongoose.Types.ObjectId(); + const res = User.applyVirtuals({ + name: { + first: 'Bill', + last: 'Gates' + }, + friendId + }); + assert.strictEqual(res.name.first, 'Bill'); + assert.strictEqual(res.name.last, 'Gates'); + assert.strictEqual(res.fullName, 'Bill Gates'); + assert.strictEqual(res.friend, null); + }); + }); }); From bcbc9e0bbf2b2cf4cac452d577d205b6521353b0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Sep 2024 17:33:43 -0400 Subject: [PATCH 2/7] Update lib/model.js Co-authored-by: hasezoey --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 777a36607a9..849ecb13340 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3521,7 +3521,7 @@ Model.applyDefaults = function applyDefaults(doc) { * obj.name; // 'John' * obj.upper; // 'JOHN', Mongoose applied the return value of the virtual to the given object * - * @param {Object} obj object or document to apply virtuals on + * @param {Object} doc object or document to apply virtuals on * @param {Array} [virtualsToApply] optional whitelist of virtuals to apply * @returns {Object} obj * @api public From a19992962a2c9ba000bae49b5a16586808ce7299 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Sep 2024 17:33:48 -0400 Subject: [PATCH 3/7] Update lib/model.js Co-authored-by: hasezoey --- lib/model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model.js b/lib/model.js index 849ecb13340..8978718d5bb 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3523,7 +3523,7 @@ Model.applyDefaults = function applyDefaults(doc) { * * @param {Object} doc object or document to apply virtuals on * @param {Array} [virtualsToApply] optional whitelist of virtuals to apply - * @returns {Object} obj + * @returns {Object} doc * @api public */ From c143d8ec59ff912f7bb3ef8ca0ebb485f25c565d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 24 Sep 2024 17:34:38 -0400 Subject: [PATCH 4/7] remove duplicate test --- test/model.test.js | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/test/model.test.js b/test/model.test.js index 242cd2333c2..8921bf9cd80 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -7905,49 +7905,6 @@ describe('Model', function() { assert.strictEqual(res.name.lastLower, 'gates'); }); - it('supports passing an array of virtuals to apply', async function() { - const userSchema = new Schema({ - name: { - first: String, - last: String - } - }); - userSchema.virtual('fullName').get(function() { - return `${this.name.first} ${this.name.last}`; - }); - userSchema.virtual('name.firstUpper').get(function() { - return this.name.first.toUpperCase(); - }); - userSchema.virtual('name.lastLower').get(function() { - return this.name.last.toLowerCase(); - }); - const User = db.model('User', userSchema); - - let res = User.applyVirtuals({ - name: { - first: 'Bill', - last: 'Gates' - } - }, ['fullName', 'name.firstUpper']); - assert.strictEqual(res.name.first, 'Bill'); - assert.strictEqual(res.name.last, 'Gates'); - assert.strictEqual(res.fullName, 'Bill Gates'); - assert.strictEqual(res.name.firstUpper, 'BILL'); - assert.strictEqual(res.name.lastLower, undefined); - - res = User.applyVirtuals({ - name: { - first: 'Bill', - last: 'Gates' - } - }, ['name.lastLower']); - assert.strictEqual(res.name.first, 'Bill'); - assert.strictEqual(res.name.last, 'Gates'); - assert.strictEqual(res.fullName, undefined); - assert.strictEqual(res.name.firstUpper, undefined); - assert.strictEqual(res.name.lastLower, 'gates'); - }); - it('sets populate virtuals to `null` if `justOne`', async function() { const userSchema = new Schema({ name: { From 660aad0ec15782eb2ffc8f6a20a0daf281ecd693 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 25 Sep 2024 10:29:20 -0400 Subject: [PATCH 5/7] docs(applyVirtuals): add jsdoc comments and clean up some unnecessary code --- lib/helpers/document/applyVirtuals.js | 89 ++++++++++++++------------- lib/model.js | 18 +++--- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/lib/helpers/document/applyVirtuals.js b/lib/helpers/document/applyVirtuals.js index e3869440b90..43cb0a047cd 100644 --- a/lib/helpers/document/applyVirtuals.js +++ b/lib/helpers/document/applyVirtuals.js @@ -4,7 +4,16 @@ const mpath = require('mpath'); module.exports = applyVirtuals; -function applyVirtuals(schema, doc, virtuals, parent) { +/** + * Apply a given schema's virtuals to a given POJO + * + * @param {Schema} schema + * @param {Object} doc + * @param {Array} [virtuals] optional whitelist of virtuals to apply + * @returns + */ + +function applyVirtuals(schema, doc, virtuals) { if (doc == null) { return doc; } @@ -15,9 +24,7 @@ function applyVirtuals(schema, doc, virtuals, parent) { if (Array.isArray(virtuals)) { virtualsForChildren = []; toApply = []; - const len = virtuals.length; - for (let i = 0; i < len; ++i) { - const virtual = virtuals[i]; + for (const virtual of virtuals) { if (virtual.length === 1) { toApply.push(virtual[0]); } else { @@ -26,28 +33,23 @@ function applyVirtuals(schema, doc, virtuals, parent) { } } - applyVirtualsToChildren(this, schema, doc, virtualsForChildren, parent); - return applyVirtualsToDocs(schema, doc, toApply); + applyVirtualsToChildren(schema, doc, virtualsForChildren); + return applyVirtualsToDoc(schema, doc, toApply); } -function applyVirtualsToDocs(schema, res, toApply) { - if (Array.isArray(res)) { - const len = res.length; - for (let i = 0; i < len; ++i) { - applyVirtualsToDoc(schema, res[i], toApply); - } - return res; - } else { - return applyVirtualsToDoc(schema, res, toApply); - } -} +/** + * Apply virtuals to any subdocuments + * + * @param {Schema} schema subdocument schema + * @param {Object} res subdocument + * @param {Array} [virtuals] optional whitelist of virtuals to apply + */ -function applyVirtualsToChildren(doc, schema, res, virtuals, parent) { - const len = schema.childSchemas.length; +function applyVirtualsToChildren(schema, res, virtuals) { let attachedVirtuals = false; - for (let i = 0; i < len; ++i) { - const _path = schema.childSchemas[i].model.path; - const _schema = schema.childSchemas[i].schema; + for (const childSchema of schema.childSchemas) { + const _path = childSchema.model.path; + const _schema = childSchema.schema; if (!_path) { continue; } @@ -59,9 +61,7 @@ function applyVirtualsToChildren(doc, schema, res, virtuals, parent) { let virtualsForChild = null; if (Array.isArray(virtuals)) { virtualsForChild = []; - const len = virtuals.length; - for (let i = 0; i < len; ++i) { - const virtual = virtuals[i]; + for (const virtual of virtuals) { if (virtual[0] == _path) { virtualsForChild.push(virtual.slice(1)); } @@ -72,22 +72,31 @@ function applyVirtualsToChildren(doc, schema, res, virtuals, parent) { } } - applyVirtuals.call(doc, _schema, _doc, virtualsForChild, res); + applyVirtuals(_schema, _doc, virtualsForChild); attachedVirtuals = true; } if (virtuals && virtuals.length && !attachedVirtuals) { - applyVirtualsToDoc(schema, res, virtuals, parent); + applyVirtualsToDoc(schema, res, virtuals); } } +/** + * Apply virtuals to a given document. Does not apply virtuals to subdocuments: use `applyVirtualsToChildren` instead + * + * @param {Schema} schema + * @param {Object} doc + * @param {Array} [virtuals] optional whitelist of virtuals to apply + * @returns + */ + function applyVirtualsToDoc(schema, doc, virtuals) { if (doc == null || typeof doc !== 'object') { return; } if (Array.isArray(doc)) { - for (let i = 0; i < doc.length; ++i) { - applyVirtualsToDoc(schema, doc[i], virtuals); + for (const el of doc) { + applyVirtualsToDoc(schema, el, virtuals); } return; } @@ -107,25 +116,25 @@ function applyVirtualsToDoc(schema, doc, virtuals) { if (virtuals == null) { virtuals = Object.keys(schema.virtuals); } - const numVirtuals = virtuals.length; - for (let i = 0; i < numVirtuals; ++i) { - const virtual = virtuals[i]; + for (const virtual of virtuals) { if (schema.virtuals[virtual] == null) { continue; } const virtualType = schema.virtuals[virtual]; const sp = Array.isArray(virtual) - ? virtual : - virtual.indexOf('.') === -1 + ? virtual + : virtual.indexOf('.') === -1 ? [virtual] : virtual.split('.'); let cur = doc; - for (let j = 0; j < sp.length - 1; ++j) { - cur[sp[j]] = sp[j] in cur ? cur[sp[j]] : {}; - cur = cur[sp[j]]; + for (let i = 0; i < sp.length - 1; ++i) { + cur[sp[i]] = sp[i] in cur ? cur[sp[i]] : {}; + cur = cur[sp[i]]; } let val = virtualType.applyGetters(cur[sp[sp.length - 1]], doc); - if (isPopulateVirtual(virtualType) && val === undefined) { + const isPopulateVirtual = + virtualType.options && (virtualType.options.ref || virtualType.options.refPath); + if (isPopulateVirtual && val === undefined) { if (virtualType.options.justOne) { val = null; } else { @@ -135,7 +144,3 @@ function applyVirtualsToDoc(schema, doc, virtuals) { cur[sp[sp.length - 1]] = val; } } - -function isPopulateVirtual(virtualType) { - return virtualType.options && (virtualType.options.ref || virtualType.options.refPath); -} diff --git a/lib/model.js b/lib/model.js index 8978718d5bb..1c361bcb495 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3521,24 +3521,24 @@ Model.applyDefaults = function applyDefaults(doc) { * obj.name; // 'John' * obj.upper; // 'JOHN', Mongoose applied the return value of the virtual to the given object * - * @param {Object} doc object or document to apply virtuals on + * @param {Object} obj object or document to apply virtuals on * @param {Array} [virtualsToApply] optional whitelist of virtuals to apply - * @returns {Object} doc + * @returns {Object} obj * @api public */ -Model.applyVirtuals = function applyVirtuals(doc, virtualsToApply) { - if (doc == null) { - return doc; +Model.applyVirtuals = function applyVirtuals(obj, virtualsToApply) { + if (obj == null) { + return obj; } // Nothing to do if this is already a hydrated document - it should already have virtuals - if (doc.$__ != null) { - return doc; + if (obj.$__ != null) { + return obj; } - applyVirtualsHelper(this.schema, doc, virtualsToApply, null); + applyVirtualsHelper(this.schema, obj, virtualsToApply); - return doc; + return obj; }; /** From fdfb3856ef8833ca86f7f1f53c7cddc5a4518886 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 25 Sep 2024 10:33:06 -0400 Subject: [PATCH 6/7] refactor: rename doc to obj in applyVirtuals helper --- lib/helpers/document/applyVirtuals.js | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/helpers/document/applyVirtuals.js b/lib/helpers/document/applyVirtuals.js index 43cb0a047cd..5fbe7ca82ba 100644 --- a/lib/helpers/document/applyVirtuals.js +++ b/lib/helpers/document/applyVirtuals.js @@ -8,14 +8,14 @@ module.exports = applyVirtuals; * Apply a given schema's virtuals to a given POJO * * @param {Schema} schema - * @param {Object} doc + * @param {Object} obj * @param {Array} [virtuals] optional whitelist of virtuals to apply * @returns */ -function applyVirtuals(schema, doc, virtuals) { - if (doc == null) { - return doc; +function applyVirtuals(schema, obj, virtuals) { + if (obj == null) { + return obj; } let virtualsForChildren = virtuals; @@ -33,8 +33,8 @@ function applyVirtuals(schema, doc, virtuals) { } } - applyVirtualsToChildren(schema, doc, virtualsForChildren); - return applyVirtualsToDoc(schema, doc, toApply); + applyVirtualsToChildren(schema, obj, virtualsForChildren); + return applyVirtualsToDoc(schema, obj, toApply); } /** @@ -53,8 +53,8 @@ function applyVirtualsToChildren(schema, res, virtuals) { if (!_path) { continue; } - const _doc = mpath.get(_path, res); - if (_doc == null || (Array.isArray(_doc) && _doc.flat(Infinity).length === 0)) { + const _obj = mpath.get(_path, res); + if (_obj == null || (Array.isArray(_obj) && _obj.flat(Infinity).length === 0)) { continue; } @@ -72,7 +72,7 @@ function applyVirtualsToChildren(schema, res, virtuals) { } } - applyVirtuals(_schema, _doc, virtualsForChild); + applyVirtuals(_schema, _obj, virtualsForChild); attachedVirtuals = true; } @@ -90,12 +90,12 @@ function applyVirtualsToChildren(schema, res, virtuals) { * @returns */ -function applyVirtualsToDoc(schema, doc, virtuals) { - if (doc == null || typeof doc !== 'object') { +function applyVirtualsToDoc(schema, obj, virtuals) { + if (obj == null || typeof obj !== 'object') { return; } - if (Array.isArray(doc)) { - for (const el of doc) { + if (Array.isArray(obj)) { + for (const el of obj) { applyVirtualsToDoc(schema, el, virtuals); } return; @@ -106,7 +106,7 @@ function applyVirtualsToDoc(schema, doc, virtuals) { const discriminator = schema.discriminators[discriminatorKey]; const key = discriminator.discriminatorMapping.key; const value = discriminator.discriminatorMapping.value; - if (doc[key] == value) { + if (obj[key] == value) { schema = discriminator; break; } @@ -126,12 +126,12 @@ function applyVirtualsToDoc(schema, doc, virtuals) { : virtual.indexOf('.') === -1 ? [virtual] : virtual.split('.'); - let cur = doc; + let cur = obj; for (let i = 0; i < sp.length - 1; ++i) { cur[sp[i]] = sp[i] in cur ? cur[sp[i]] : {}; cur = cur[sp[i]]; } - let val = virtualType.applyGetters(cur[sp[sp.length - 1]], doc); + let val = virtualType.applyGetters(cur[sp[sp.length - 1]], obj); const isPopulateVirtual = virtualType.options && (virtualType.options.ref || virtualType.options.refPath); if (isPopulateVirtual && val === undefined) { From 54844e31540079550a522abde157cb9f7fcbe815 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 26 Sep 2024 11:43:27 -0400 Subject: [PATCH 7/7] types: add applyVirtuals() to types --- types/models.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/types/models.d.ts b/types/models.d.ts index 4c2403fd51b..0a5e6e3a585 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -290,6 +290,9 @@ declare module 'mongoose' { applyDefaults(obj: AnyObject): AnyObject; applyDefaults(obj: TRawDocType): TRawDocType; + /* Apply virtuals to the given POJO. */ + applyVirtuals(obj: AnyObject, virtalsToApply?: string[]): AnyObject; + /** * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`, * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one