From 8fcdc131710ca52fa78d2a5d89802643da9df281 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Jun 2024 18:23:28 -0400 Subject: [PATCH 1/2] fix: allow calling `parent()` on parent, allow accessing parent's virtual from child subdoc Fix #65 Fix #64 --- index.d.ts | 10 ++++++---- index.js | 30 ++++++++++++++++++++++++----- package.json | 2 +- test/index.test.js | 48 ++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/index.d.ts b/index.d.ts index 01c5d76..e89b632 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,7 @@ declare module "mongoose-lean-virtuals" { - import mongoose = require('mongoose'); - export default function mongooseLeanVirtuals(schema: mongoose.Schema, opts?: any): void; - export function mongooseLeanVirtuals(schema: mongoose.Schema, opts?: any): void; - } \ No newline at end of file + import mongoose = require('mongoose'); + export default function mongooseLeanVirtuals(schema: mongoose.Schema, opts?: any): void; + export function mongooseLeanVirtuals(schema: mongoose.Schema, opts?: any): void; + + export function parent(value: any): any; +} \ No newline at end of file diff --git a/index.js b/index.js index 532ff8d..4a88143 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const flat = require('array.prototype.flat'); const mpath = require('mpath'); const documentParentsMap = new WeakMap(); +const attachVirtualsFnMap = new WeakMap(); module.exports = function mongooseLeanVirtuals(schema, options) { const fn = attachVirtualsMiddleware(schema, options); @@ -32,7 +33,14 @@ module.exports.parent = function(obj) { if (obj == null) { return void 0; } - return documentParentsMap.get(obj); + const res = documentParentsMap.get(obj); + // Since we do we apply virtuals to children before parents, + // we may need to call `applyVirtuals()` on the parent if we're + // accessing the parent from the child. + if (attachVirtualsFnMap.get(res)) { + attachVirtualsFnMap.get(res)(); + } + return res; }; function attachVirtualsMiddleware(schema, options = {}) { @@ -75,9 +83,19 @@ function attachVirtuals(schema, res, virtuals, parent) { } } + let called = false; + const applyVirtualsToResultOnce = () => { + if (called) { + return; + } + called = true; + applyVirtualsToResult(schema, res, toApply); + }; + + addToParentMap(res, parent, applyVirtualsToResultOnce); + applyVirtualsToChildren(this, schema, res, virtualsForChildren, parent); - addToParentMap(res, parent); - return applyVirtualsToResult(schema, res, toApply); + return applyVirtualsToResultOnce(); } function applyVirtualsToResult(schema, res, toApply) { @@ -92,8 +110,8 @@ function applyVirtualsToResult(schema, res, toApply) { } } -function addToParentMap(res, parent) { - if (parent == null || res == null) { +function addToParentMap(res, parent, attachVirtualsToResult) { + if (res == null) { return; } @@ -108,6 +126,7 @@ function addToParentMap(res, parent) { for (const _res of res) { if (_res != null && typeof _res === 'object') { documentParentsMap.set(_res, parent); + attachVirtualsFnMap.set(_res, attachVirtualsToResult); } } return; @@ -115,6 +134,7 @@ function addToParentMap(res, parent) { if (typeof res === 'object') { documentParentsMap.set(res, parent); + attachVirtualsFnMap.set(res, attachVirtualsToResult); } } diff --git a/package.json b/package.json index be63f50..00f26ba 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "acquit-ignore": "0.1.0", "acquit-markdown": "0.1.0", "co": "4.6.0", - "eslint": "6.8.0", + "eslint": "7.x", "istanbul": "0.4.5", "mocha": "5.2.x", "mongoose": "7.0.0-rc0" diff --git a/test/index.test.js b/test/index.test.js index 0878410..3063509 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -623,7 +623,7 @@ describe('Discriminators work', () => { it('sets empty array if no result and justOne: false', async function() { const childSchema = new mongoose.Schema({ name: String, parentId: 'ObjectId' }); - const Child = mongoose.model('C2', childSchema); + mongoose.model('C2', childSchema); const parentSchema = new mongoose.Schema({ name: String }); parentSchema.virtual('children', { @@ -688,7 +688,7 @@ describe('Discriminators work', () => { const Parent = mongoose.model('Parent2', parentSchema); const child = await Child.create({ name: 'Luke', surname: { name: 'Skywalker' } }); - const parent = await Parent.create({ role: 'Father', surname: { name: 'Vader' }, allegiance: { name: 'Empire' }, child: child }); + await Parent.create({ role: 'Father', surname: { name: 'Vader' }, allegiance: { name: 'Empire' }, child: child }); let doc = await Parent.findOne().populate('child').lean({ virtuals: true }); assert.ok(childGetterCalled); assert.ok(parentGetterCalled); @@ -744,4 +744,48 @@ describe('Discriminators work', () => { assert.equal(doc.lowercase, 'test testerson'); }); }); + + it('supports parent() on deeply nested docs (gh-65)', function() { + const getParent = (doc) => { + if (doc instanceof mongoose.Document) { + return doc.parent(); + } + return mongooseLeanVirtuals.parent(doc); + }; + + const grandchildSchema = new mongoose.Schema({ firstName: String }); + grandchildSchema.virtual('fullName').get(function() { + return `${this.firstName} ${getParent(getParent(this)).lastName}`; + }); + + const childSchema = new mongoose.Schema({ firstName: String, child: grandchildSchema }); + childSchema.virtual('fullName').get(function() { + return `${this.firstName} ${getParent(this).lastName}`; + }); + + const parentSchema = new mongoose.Schema({ + firstName: String, + lastName: String, + child: childSchema + }, { id: false }); + + parentSchema.plugin(mongooseLeanVirtuals); + const Parent = mongoose.model('gh65', parentSchema); + + return co(function*() { + const { _id } = yield Parent.create({ + firstName: 'Anakin', + lastName: 'Skywalker', + child: { + firstName: 'Luke', + child: { + firstName: 'Ben' + } + } + }); + const doc = yield Parent.findById(_id).lean({ virtuals: true }).orFail(); + assert.equal(doc.child.fullName, 'Luke Skywalker'); + assert.equal(doc.child.child.fullName, 'Ben Skywalker'); + }); + }); }); From 7f0642098b3fcbe68fd1ed530785927be011c4ba Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 7 Jun 2024 18:34:11 -0400 Subject: [PATCH 2/2] add test case for #64 --- test/index.test.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index 3063509..989cda1 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -788,4 +788,41 @@ describe('Discriminators work', () => { assert.equal(doc.child.child.fullName, 'Ben Skywalker'); }); }); + + it('can access parent virtuals from child subdocument (gh-64)', async function() { + const childSchema = new mongoose.Schema({ firstName: String }); + childSchema.virtual('uri').get(function() { + // This `uri` virtual is in a subdocument, so in order to get the + // parent's `uri` you need to use this plugin's `parent()` function. + + const parent = this instanceof mongoose.Document + ? this.parent() + : mongooseLeanVirtuals.parent(this) + ; + return `${parent.uri}/child/gh-64-child`; + }); + + const parentSchema = new mongoose.Schema({ + child: childSchema + }); + parentSchema.virtual('uri').get(function() { + return `/parent/gh-64-parent`; + }); + + parentSchema.plugin(mongooseLeanVirtuals); + + const Parent = mongoose.model('gh64', parentSchema); + + const doc = { child: {} }; + + await Parent.create(doc); + + let result = await Parent + .findOne() + .lean({ virtuals: true }); + assert.equal( + result.child.uri, + '/parent/gh-64-parent/child/gh-64-child' + ); + }); });