Skip to content

Bug: execPopulate is not a function with Mongoose 6+ and Virtual Relations #1597

@msakturkoglu

Description

@msakturkoglu

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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions