Skip to content

Commit

Permalink
Merge pull request #14533 from Automattic/vkarpov15/gh-14503
Browse files Browse the repository at this point in the history
Make `hydrate()` recursively hydrate virtual populate docs if `hydratedPopulatedDocs` is set
  • Loading branch information
vkarpov15 authored Apr 24, 2024
2 parents 6861d8d + bd8fa0d commit e91fcf4
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 19 deletions.
16 changes: 1 addition & 15 deletions lib/helpers/populate/getModelsMapForPopulate.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,26 +410,12 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
justOne = options.justOne;
}

modelNames = virtual._getModelNamesForPopulate(doc);
if (virtual.options.refPath) {
modelNames =
modelNamesFromRefPath(virtual.options.refPath, doc, options.path);
justOne = !!virtual.options.justOne;
data.isRefPath = true;
} else if (virtual.options.ref) {
let normalizedRef;
if (typeof virtual.options.ref === 'function' && !virtual.options.ref[modelSymbol]) {
normalizedRef = virtual.options.ref.call(doc, doc);
} else {
normalizedRef = virtual.options.ref;
}
justOne = !!virtual.options.justOne;
// When referencing nested arrays, the ref should be an Array
// of modelNames.
if (Array.isArray(normalizedRef)) {
modelNames = normalizedRef;
} else {
modelNames = [normalizedRef];
}
}

data.isVirtual = true;
Expand Down
24 changes: 20 additions & 4 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2297,7 +2297,10 @@ Schema.prototype.virtual = function(name, options) {
throw new Error('Reference virtuals require `foreignField` option');
}

this.pre('init', function virtualPreInit(obj) {
const virtual = this.virtual(name);
virtual.options = options;

this.pre('init', function virtualPreInit(obj, opts) {
if (mpath.has(name, obj)) {
const _v = mpath.get(name, obj);
if (!this.$$populatedVirtuals) {
Expand All @@ -2314,13 +2317,26 @@ Schema.prototype.virtual = function(name, options) {
_v == null ? [] : [_v];
}

if (opts?.hydratedPopulatedDocs && !options.count) {
const modelNames = virtual._getModelNamesForPopulate(this);
const populatedVal = this.$$populatedVirtuals[name];
if (!Array.isArray(populatedVal) && !populatedVal.$__ && modelNames?.length === 1) {
const PopulateModel = this.db.model(modelNames[0]);
this.$$populatedVirtuals[name] = PopulateModel.hydrate(populatedVal);
} else if (Array.isArray(populatedVal) && modelNames?.length === 1) {
const PopulateModel = this.db.model(modelNames[0]);
for (let i = 0; i < populatedVal.length; ++i) {
if (!populatedVal[i].$__) {
populatedVal[i] = PopulateModel.hydrate(populatedVal[i]);
}
}
}
}

mpath.unset(name, obj);
}
});

const virtual = this.virtual(name);
virtual.options = options;

virtual.
set(function(v) {
if (!this.$$populatedVirtuals) {
Expand Down
29 changes: 29 additions & 0 deletions lib/virtualType.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use strict';

const modelNamesFromRefPath = require('./helpers/populate/modelNamesFromRefPath');
const utils = require('./utils');

const modelSymbol = require('./helpers/symbols').modelSymbol;

/**
* VirtualType constructor
*
Expand Down Expand Up @@ -168,6 +171,32 @@ VirtualType.prototype.applySetters = function(value, doc) {
return v;
};

/**
* Get the names of models used to populate this model given a doc
*
* @param {Document} doc
* @return {Array<string> | null}
* @api private
*/

VirtualType.prototype._getModelNamesForPopulate = function _getModelNamesForPopulate(doc) {
if (this.options.refPath) {
return modelNamesFromRefPath(this.options.refPath, doc, this.path);
}

let normalizedRef = null;
if (typeof this.options.ref === 'function' && !this.options.ref[modelSymbol]) {
normalizedRef = this.options.ref.call(doc, doc);
} else {
normalizedRef = this.options.ref;
}
if (normalizedRef != null && !Array.isArray(normalizedRef)) {
return [normalizedRef];
}

return normalizedRef;
};

/*!
* exports
*/
Expand Down
56 changes: 56 additions & 0 deletions test/model.hydrate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,61 @@ describe('model', function() {
const C = Company.hydrate(company, null, { hydratedPopulatedDocs: true });
assert.equal(C.users[0].name, 'Val');
});
it('should hydrate documents in virtual populate (gh-14503)', async function() {
const StorySchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'User'
},
title: {
type: String
}
}, { timestamps: true });

const UserSchema = new Schema({
name: String
}, { timestamps: true });

UserSchema.virtual('stories', {
ref: 'Story',
localField: '_id',
foreignField: 'userId'
});
UserSchema.virtual('storiesCount', {
ref: 'Story',
localField: '_id',
foreignField: 'userId',
count: true
});

const User = db.model('User', UserSchema);
const Story = db.model('Story', StorySchema);

const user = await User.create({ name: 'Alex' });
const story1 = await Story.create({ title: 'Ticket 1', userId: user._id });
const story2 = await Story.create({ title: 'Ticket 2', userId: user._id });

const populated = await User.findOne({ name: 'Alex' }).populate(['stories', 'storiesCount']).lean();
const hydrated = User.hydrate(
JSON.parse(JSON.stringify(populated)),
null,
{ hydratedPopulatedDocs: true }
);

assert.equal(hydrated.stories[0]._id.toString(), story1._id.toString());
assert(typeof hydrated.stories[0]._id == 'object', typeof hydrated.stories[0]._id);
assert(hydrated.stories[0]._id instanceof mongoose.Types.ObjectId);
assert(typeof hydrated.stories[0].createdAt == 'object');
assert(hydrated.stories[0].createdAt instanceof Date);

assert.equal(hydrated.stories[1]._id.toString(), story2._id.toString());
assert(typeof hydrated.stories[1]._id == 'object');

assert(hydrated.stories[1]._id instanceof mongoose.Types.ObjectId);
assert(typeof hydrated.stories[1].createdAt == 'object');
assert(hydrated.stories[1].createdAt instanceof Date);

assert.strictEqual(hydrated.storiesCount, 2);
});
});
});

0 comments on commit e91fcf4

Please sign in to comment.