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