nestjs/mongoose

Injecting services into MongooseModule.forFeatureAsync not triggering the middleware hooks

Closed this issue · 1 comments

Is there an existing issue for this?

  • I have searched the existing issues

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?

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 {}

If you need a minimun reproduction code, please let me know.

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 {}

Note

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

In which operating systems have you tested?

  • macOS
  • Windows
  • Linux

Thank you for taking the time to submit your report! From the looks of it, this could be better discussed on our Discord. If you haven't already, please join here and send a new post in the #⁠ 🐈 nestjs-help forum. Make sure to include a link to this issue, so you don't need to write it all again. We have a large community of helpful members, who will assist you in getting this to work.