Injecting services into MongooseModule.forFeatureAsync not triggering the middleware hooks

47 Views Asked by At

Current behavior

When I add the service injection, the pre-save hook won't be triggered. However, if I remove the service injection, the hook will be triggered as expected. Is there a way to inject services in useFactory function that I'm not seeing?

Here is a link to the minimun reproduction code: https://github.com/valtervalik/service-injection-issue

I have a pre-save mongoose hook middleware for the UserSchema that looks like this:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { NextFunction } from 'express';
import { HashingModule } from 'src/auth/hashing/hashing.module';
import { HashingService } from 'src/auth/hashing/hashing.service';
import { User, UserDocument, UserSchema } from 'src/users/schemas/user.schema';

@Module({
  imports: [
    MongooseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        uri: configService.get<string>('DB_URI'),
      }),
      inject: [ConfigService],
    }),
    MongooseModule.forFeatureAsync([
      {
        name: User.name,
        useFactory: (hashingService: HashingService) => {
          const schema = UserSchema;
          schema.pre<UserDocument>('save', async function (next: NextFunction) {
            const doc = this;
            if (doc) {
              doc.password = await hashingService.hash(doc.password);
              console.log(doc);
            }
            next();
          });
          return schema;
        },
        imports: [HashingModule],
        inject: [HashingService],
      },
      
    ]),
  ],
})
export class DatabaseModule {}

This is my UserSchema:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document, HydratedDocument } from 'mongoose';
import { Role } from './role.schema';
import { Permission } from './permission.schema';

export type UserDocument = IDocument & HydratedDocument<User>;

@Schema({ timestamps: true })
export class User extends Document {
  @Prop({ type: String })
  username: string;

  @Prop({ type: String })
  firstName: string;

  @Prop({ type: String })
  lastName: string;

  @Prop({ type: String, required: true, unique: true })
  email: string;

  @Prop({ type: String, required: true, unique: true })
  phone: string;

  @Prop({ type: String, select: false })
  password?: string;

  @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Role' })
  role: Role;

  @Prop({ type: Boolean, default: false })
  isTFAEnabled: boolean;

  @Prop({ type: String })
  tfaSecret?: string;

  @Prop({ type: String })
  googleId?: string;

  @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Permission' })
  permission: Permission;

  @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
  createdBy: User;

  @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' })
  updatedBy: User;
}

export const UserSchema = SchemaFactory.createForClass(User);

My hashingService and Module:

import { Injectable } from '@nestjs/common';

@Injectable()
export abstract class HashingService {
  abstract hash(data: string | Buffer): Promise<string>;
  abstract compare(data: string | Buffer, hash: string): Promise<boolean>;
}
import { Injectable } from '@nestjs/common';
import { HashingService } from './hashing.service';
import { compare, genSalt, hash } from 'bcrypt';

@Injectable()
export class BcryptService implements HashingService {
  async hash(data: string | Buffer): Promise<string> {
    const salt = await genSalt();
    return hash(data, salt);
  }

  async compare(data: string | Buffer, hash: string): Promise<boolean> {
    return compare(data, hash);
  }
}
import { Module } from '@nestjs/common';
import { HashingService } from './hashing.service';
import { BcryptService } from './bcrypt.service';

@Module({
  providers: [{ provide: HashingService, useClass: BcryptService }],
  exports: [{ provide: HashingService, useClass: BcryptService }],
})
export class HashingModule {}

Expected behavior

What I'm expecting is the pre-save hook been triggered and the service method being executed, returning the hashed password of the user.

For example, this works as expected:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { NextFunction } from 'express';
import { BcryptService } from 'src/auth/hashing/bcrypt.service';
import { User, UserDocument, UserSchema } from 'src/users/schemas/user.schema';

@Module({
  imports: [
    MongooseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        uri: configService.get<string>('DB_URI'),
      }),
      inject: [ConfigService],
    }),
    MongooseModule.forFeatureAsync([
      {
        name: User.name,
        useFactory: () => {
          const hashingService = new BcryptService();
          const schema = UserSchema;

          schema.pre<UserDocument>('save', async function (next: NextFunction) {
            const doc = this;
            if (doc) {
              doc.password = await hashingService.hash(doc.password);
            }
            next();
          });
          return schema;
        },
      },
    ]),
  ],
})
export class DatabaseModule {}

Notice that I've created a new instance of BcryptService

Package version

10.0.2

mongoose version

8.0.3

NestJS version

10.2.1

Node.js version

20.9.0

0

There are 0 best solutions below