Skip to content

Commit

Permalink
Merge branch 'master' into gh-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
IslandRhythms committed Apr 26, 2024
2 parents b6628db + 5137eeb commit 126c02d
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 23 deletions.
16 changes: 1 addition & 15 deletions lib/helpers/populate/getModelsMapForPopulate.js
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
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
@@ -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
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);
});
});
});
46 changes: 46 additions & 0 deletions test/types/populate.test.ts
Expand Up @@ -373,3 +373,49 @@ async function gh13070() {
const doc2 = await Child.populate<{ child: IChild }>(doc, 'child');
const name: string = doc2.child.name;
}

function gh14441() {
interface Parent {
child?: Types.ObjectId;
name?: string;
}
const ParentModel = model<Parent>(
'Parent',
new Schema({
child: { type: Schema.Types.ObjectId, ref: 'Child' },
name: String
})
);

interface Child {
name: string;
}
const childSchema: Schema = new Schema({ name: String });
model<Child>('Child', childSchema);

ParentModel.findOne({})
.populate<{ child: Child }>('child')
.orFail()
.then(doc => {
expectType<string>(doc.child.name);
const docObject = doc.toObject();
expectType<string>(docObject.child.name);
});

ParentModel.findOne({})
.populate<{ child: Child }>('child')
.lean()
.orFail()
.then(doc => {
expectType<string>(doc.child.name);
});

ParentModel.find({})
.populate<{ child: Child }>('child')
.orFail()
.then(docs => {
expectType<string>(docs[0]!.child.name);
const docObject = docs[0]!.toObject();
expectType<string>(docObject.child.name);
});
}
24 changes: 24 additions & 0 deletions test/types/queries.test.ts
Expand Up @@ -612,3 +612,27 @@ function gh14473() {
const query2: FilterQuery<D> = { deletedAt: { $lt: new Date() } };
};
}

async function gh14525() {
type BeAnObject = Record<string, any>;

interface SomeDoc {
something: string;
func(this: TestDoc): string;
}

interface PluginExtras {
pfunc(): number;
}

type TestDoc = Document<unknown, BeAnObject, SomeDoc> & PluginExtras;

type ModelType = Model<SomeDoc, BeAnObject, PluginExtras, BeAnObject>;

const doc = await ({} as ModelType).findOne({}).populate('test').orFail().exec();

doc.func();

let doc2 = await ({} as ModelType).create({});
doc2 = await ({} as ModelType).findOne({}).populate('test').orFail().exec();
}
41 changes: 37 additions & 4 deletions types/query.d.ts
Expand Up @@ -205,6 +205,18 @@ declare module 'mongoose' {
? (ResultType extends any[] ? Require_id<FlattenMaps<RawDocType>>[] : Require_id<FlattenMaps<RawDocType>>)
: ResultType;

type MergePopulatePaths<RawDocType, ResultType, QueryOp, Paths, TQueryHelpers> = QueryOp extends QueryOpThatReturnsDocument
? ResultType extends null
? ResultType
: ResultType extends (infer U)[]
? U extends Document
? HydratedDocument<MergeType<RawDocType, Paths>, Record<string, never>, TQueryHelpers>[]
: (MergeType<U, Paths>)[]
: ResultType extends Document
? HydratedDocument<MergeType<RawDocType, Paths>, Record<string, never>, TQueryHelpers>
: MergeType<ResultType, Paths>
: MergeType<ResultType, Paths>;

class Query<ResultType, DocType, THelpers = {}, RawDocType = DocType, QueryOp = 'find'> implements SessionOperation {
_mongooseOptions: MongooseQueryOptions<DocType>;

Expand Down Expand Up @@ -602,22 +614,43 @@ declare module 'mongoose' {
polygon(...coordinatePairs: number[][]): this;

/** Specifies paths which should be populated with other documents. */
populate<Paths = {}>(
populate(
path: string | string[],
select?: string | any,
model?: string | Model<any, THelpers>,
match?: any
): QueryWithHelpers<
ResultType,
DocType,
THelpers,
RawDocType,
QueryOp
>;
populate(
options: PopulateOptions | (PopulateOptions | string)[]
): QueryWithHelpers<
ResultType,
DocType,
THelpers,
RawDocType,
QueryOp
>;
populate<Paths>(
path: string | string[],
select?: string | any,
model?: string | Model<any, THelpers>,
match?: any
): QueryWithHelpers<
UnpackedIntersection<ResultType, Paths>,
MergePopulatePaths<RawDocType, ResultType, QueryOp, Paths, THelpers>,
DocType,
THelpers,
UnpackedIntersection<RawDocType, Paths>,
QueryOp
>;
populate<Paths = {}>(
populate<Paths>(
options: PopulateOptions | (PopulateOptions | string)[]
): QueryWithHelpers<
UnpackedIntersection<ResultType, Paths>,
MergePopulatePaths<RawDocType, ResultType, QueryOp, Paths, THelpers>,
DocType,
THelpers,
UnpackedIntersection<RawDocType, Paths>,
Expand Down

0 comments on commit 126c02d

Please sign in to comment.