-
Notifications
You must be signed in to change notification settings - Fork 143
Description
Bug: execPopulate is not a function with Mongoose 6+ and Virtual Relations
Description
When using @Relation decorator with Mongoose virtual fields in NestJS Query, the following error occurs with Mongoose 6+:
TypeError: foundEntity.populate(...).execPopulate is not a function
Environment
- @nestjs-query/query-graphql: 0.30.0
- @nestjs-query/query-mongoose: 0.30.0
- mongoose: 9.1.3
- @nestjs/common: 11.1.11
- Node.js: 24.7.0
Root Cause
The execPopulate() method was deprecated in Mongoose 5.x and removed in Mongoose 6.x. In Mongoose 6+, the populate() method directly returns a Promise, so calling .execPopulate() is no longer valid.
Reference: https://mongoosejs.com/docs/migrating_to_6.html#removed-execpopulate
NestJS Query's ReferenceQueryService still uses the old API:
// node_modules/@nestjs-query/query-mongoose/src/services/reference-query.service.ts:152
return foundEntity.populate(relationName).execPopulate(); // ❌ Doesn't work in Mongoose 6+Steps to Reproduce
1. Entity Setup
// park.entity.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
@Schema({ timestamps: true, collection: 'park' })
export class ParkEntity extends Document {
@Prop()
discountCode?: string;
// Virtual field
discountCard?: DiscountCardEntity;
}
export const ParkSchema = SchemaFactory.createForClass(ParkEntity);
// Define virtual relation
ParkSchema.virtual('discountCard', {
ref: 'DiscountCardEntity',
localField: 'discountCode',
foreignField: 'code',
justOne: true,
});// discount-card.entity.ts
@Schema({ timestamps: true, collection: 'discountCard' })
export class DiscountCardEntity extends Document {
@Prop()
code?: string;
@Prop()
name?: string;
}2. DTO Setup
// park.dto.ts
import { ObjectType } from '@nestjs/graphql';
import { Relation } from '@nestjs-query/query-graphql';
import { DiscountCardDTO } from './discount-card.dto';
@ObjectType('Park')
@Relation('discountCard', () => DiscountCardDTO, {
nullable: true,
disableRemove: true,
disableUpdate: true,
})
export class ParkDTO {
@Field()
_id!: string;
@Field({ nullable: true })
discountCode?: string;
@Field(() => DiscountCardDTO, { nullable: true })
discountCard?: DiscountCardDTO;
}3. Module Setup
// park.module.ts
@Module({
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryMongooseModule.forFeature([
{ document: ParkEntity, name: 'ParkEntity', schema: ParkSchema },
{ document: DiscountCardEntity, name: 'DiscountCardEntity', schema: DiscountCardSchema },
]),
],
resolvers: [{
DTOClass: ParkDTO,
ServiceClass: ParkQueryService,
}],
}),
],
})
export class ParkModule {}4. GraphQL Query
query {
parks {
edges {
node {
_id
discountCode
discountCard { # ❌ Error occurs here
_id
code
name
}
}
}
}
}5. Error
{
"errors": [
{
"message": "foundEntity.populate(...).execPopulate is not a function",
"path": ["parks", "edges", 0, "node", "discountCard"],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"stacktrace": [
"TypeError: foundEntity.populate(...).execPopulate is not a function",
" at ParkQueryService.findRelation (/node_modules/@nestjs-query/query-mongoose/src/services/reference-query.service.ts:152:94)"
]
}
}
]
}Current Workaround
Override query() and findById() methods in the QueryService to manually populate virtual fields:
// park-query.service.ts
import { QueryService } from '@nestjs-query/core';
import { MongooseQueryService } from '@nestjs-query/query-mongoose';
import { Injectable } from '@nestjs/common';
@QueryService(ParkEntity)
@Injectable()
export class ParkQueryService extends MongooseQueryService<ParkEntity> {
constructor(
@InjectModel('ParkEntity')
private readonly parkModel: Model<ParkEntity>,
) {
super(parkModel);
}
/**
* Override query to populate discountCard virtual field
*/
async query(query: Query<ParkEntity>): Promise<ParkEntity[]> {
const results = await super.query(query);
if (results && results.length > 0) {
await this.parkModel.populate(results, {
path: 'discountCard',
match: { isActive: true },
});
}
return results;
}
/**
* Override findById to populate discountCard virtual field
*/
async findById(id: string): Promise<ParkEntity | undefined> {
const result = await this.parkModel.findById(id).exec();
if (result) {
await this.parkModel.populate(result, {
path: 'discountCard',
match: { isActive: true },
});
}
return result || undefined;
}
}Proposed Solution
Update ReferenceQueryService to use Mongoose 6+ compatible API:
// Before (Mongoose 5.x)
return foundEntity.populate(relationName).execPopulate();
// After (Mongoose 6+)
return foundEntity.populate(relationName);Or use a compatibility check:
const populated = foundEntity.populate(relationName);
// execPopulate is deprecated/removed in Mongoose 6+
return typeof populated.execPopulate === 'function'
? populated.execPopulate()
: populated;Expected Behavior
The @Relation decorator should work seamlessly with Mongoose 6+ virtual fields without requiring manual override of query methods.
Additional Context
- Mongoose 6 Migration Guide: https://mongoosejs.com/docs/migrating_to_6.html#removed-execpopulate
- This affects all projects using NestJS Query with Mongoose 6+ and virtual relations
- The workaround works but defeats the purpose of using NestJS Query's automatic relation handling
Would you like me to submit a PR?
I'd be happy to contribute a fix for this issue if the maintainers are open to it.