Skip to content

Commit e91fcf4

Browse files
authored
Merge pull request #14533 from Automattic/vkarpov15/gh-14503
Make `hydrate()` recursively hydrate virtual populate docs if `hydratedPopulatedDocs` is set
2 parents 6861d8d + bd8fa0d commit e91fcf4

File tree

4 files changed

+106
-19
lines changed

4 files changed

+106
-19
lines changed

lib/helpers/populate/getModelsMapForPopulate.js

+1-15
Original file line numberDiff line numberDiff line change
@@ -410,26 +410,12 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
410410
justOne = options.justOne;
411411
}
412412

413+
modelNames = virtual._getModelNamesForPopulate(doc);
413414
if (virtual.options.refPath) {
414-
modelNames =
415-
modelNamesFromRefPath(virtual.options.refPath, doc, options.path);
416415
justOne = !!virtual.options.justOne;
417416
data.isRefPath = true;
418417
} else if (virtual.options.ref) {
419-
let normalizedRef;
420-
if (typeof virtual.options.ref === 'function' && !virtual.options.ref[modelSymbol]) {
421-
normalizedRef = virtual.options.ref.call(doc, doc);
422-
} else {
423-
normalizedRef = virtual.options.ref;
424-
}
425418
justOne = !!virtual.options.justOne;
426-
// When referencing nested arrays, the ref should be an Array
427-
// of modelNames.
428-
if (Array.isArray(normalizedRef)) {
429-
modelNames = normalizedRef;
430-
} else {
431-
modelNames = [normalizedRef];
432-
}
433419
}
434420

435421
data.isVirtual = true;

lib/schema.js

+20-4
Original file line numberDiff line numberDiff line change
@@ -2297,7 +2297,10 @@ Schema.prototype.virtual = function(name, options) {
22972297
throw new Error('Reference virtuals require `foreignField` option');
22982298
}
22992299

2300-
this.pre('init', function virtualPreInit(obj) {
2300+
const virtual = this.virtual(name);
2301+
virtual.options = options;
2302+
2303+
this.pre('init', function virtualPreInit(obj, opts) {
23012304
if (mpath.has(name, obj)) {
23022305
const _v = mpath.get(name, obj);
23032306
if (!this.$$populatedVirtuals) {
@@ -2314,13 +2317,26 @@ Schema.prototype.virtual = function(name, options) {
23142317
_v == null ? [] : [_v];
23152318
}
23162319

2320+
if (opts?.hydratedPopulatedDocs && !options.count) {
2321+
const modelNames = virtual._getModelNamesForPopulate(this);
2322+
const populatedVal = this.$$populatedVirtuals[name];
2323+
if (!Array.isArray(populatedVal) && !populatedVal.$__ && modelNames?.length === 1) {
2324+
const PopulateModel = this.db.model(modelNames[0]);
2325+
this.$$populatedVirtuals[name] = PopulateModel.hydrate(populatedVal);
2326+
} else if (Array.isArray(populatedVal) && modelNames?.length === 1) {
2327+
const PopulateModel = this.db.model(modelNames[0]);
2328+
for (let i = 0; i < populatedVal.length; ++i) {
2329+
if (!populatedVal[i].$__) {
2330+
populatedVal[i] = PopulateModel.hydrate(populatedVal[i]);
2331+
}
2332+
}
2333+
}
2334+
}
2335+
23172336
mpath.unset(name, obj);
23182337
}
23192338
});
23202339

2321-
const virtual = this.virtual(name);
2322-
virtual.options = options;
2323-
23242340
virtual.
23252341
set(function(v) {
23262342
if (!this.$$populatedVirtuals) {

lib/virtualType.js

+29
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
'use strict';
22

3+
const modelNamesFromRefPath = require('./helpers/populate/modelNamesFromRefPath');
34
const utils = require('./utils');
45

6+
const modelSymbol = require('./helpers/symbols').modelSymbol;
7+
58
/**
69
* VirtualType constructor
710
*
@@ -168,6 +171,32 @@ VirtualType.prototype.applySetters = function(value, doc) {
168171
return v;
169172
};
170173

174+
/**
175+
* Get the names of models used to populate this model given a doc
176+
*
177+
* @param {Document} doc
178+
* @return {Array<string> | null}
179+
* @api private
180+
*/
181+
182+
VirtualType.prototype._getModelNamesForPopulate = function _getModelNamesForPopulate(doc) {
183+
if (this.options.refPath) {
184+
return modelNamesFromRefPath(this.options.refPath, doc, this.path);
185+
}
186+
187+
let normalizedRef = null;
188+
if (typeof this.options.ref === 'function' && !this.options.ref[modelSymbol]) {
189+
normalizedRef = this.options.ref.call(doc, doc);
190+
} else {
191+
normalizedRef = this.options.ref;
192+
}
193+
if (normalizedRef != null && !Array.isArray(normalizedRef)) {
194+
return [normalizedRef];
195+
}
196+
197+
return normalizedRef;
198+
};
199+
171200
/*!
172201
* exports
173202
*/

test/model.hydrate.test.js

+56
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,61 @@ describe('model', function() {
117117
const C = Company.hydrate(company, null, { hydratedPopulatedDocs: true });
118118
assert.equal(C.users[0].name, 'Val');
119119
});
120+
it('should hydrate documents in virtual populate (gh-14503)', async function() {
121+
const StorySchema = new Schema({
122+
userId: {
123+
type: Schema.Types.ObjectId,
124+
ref: 'User'
125+
},
126+
title: {
127+
type: String
128+
}
129+
}, { timestamps: true });
130+
131+
const UserSchema = new Schema({
132+
name: String
133+
}, { timestamps: true });
134+
135+
UserSchema.virtual('stories', {
136+
ref: 'Story',
137+
localField: '_id',
138+
foreignField: 'userId'
139+
});
140+
UserSchema.virtual('storiesCount', {
141+
ref: 'Story',
142+
localField: '_id',
143+
foreignField: 'userId',
144+
count: true
145+
});
146+
147+
const User = db.model('User', UserSchema);
148+
const Story = db.model('Story', StorySchema);
149+
150+
const user = await User.create({ name: 'Alex' });
151+
const story1 = await Story.create({ title: 'Ticket 1', userId: user._id });
152+
const story2 = await Story.create({ title: 'Ticket 2', userId: user._id });
153+
154+
const populated = await User.findOne({ name: 'Alex' }).populate(['stories', 'storiesCount']).lean();
155+
const hydrated = User.hydrate(
156+
JSON.parse(JSON.stringify(populated)),
157+
null,
158+
{ hydratedPopulatedDocs: true }
159+
);
160+
161+
assert.equal(hydrated.stories[0]._id.toString(), story1._id.toString());
162+
assert(typeof hydrated.stories[0]._id == 'object', typeof hydrated.stories[0]._id);
163+
assert(hydrated.stories[0]._id instanceof mongoose.Types.ObjectId);
164+
assert(typeof hydrated.stories[0].createdAt == 'object');
165+
assert(hydrated.stories[0].createdAt instanceof Date);
166+
167+
assert.equal(hydrated.stories[1]._id.toString(), story2._id.toString());
168+
assert(typeof hydrated.stories[1]._id == 'object');
169+
170+
assert(hydrated.stories[1]._id instanceof mongoose.Types.ObjectId);
171+
assert(typeof hydrated.stories[1].createdAt == 'object');
172+
assert(hydrated.stories[1].createdAt instanceof Date);
173+
174+
assert.strictEqual(hydrated.storiesCount, 2);
175+
});
120176
});
121177
});

0 commit comments

Comments
 (0)