23

I'm trying to inject my users service into my validator constraint interface but it doesn't seem to work:

import { ValidatorConstraintInterface, ValidatorConstraint, ValidationArguments, registerDecorator, ValidationOptions } from "class-validator";
import { UsersService } from './users.service';

@ValidatorConstraint({ async: true })
export class IsEmailAlreadyInUseConstraint implements ValidatorConstraintInterface {
    constructor(private usersService: UsersService) {
        console.log(this.usersService);
    }
    validate(email: any, args: ValidationArguments) {
        return this.usersService.findUserByEmail(email).then(user => {
             if (user) return false;
             return true;
        });
        return false;
    }

}

But, as usersService is logged null, I can't access its methods.

Any insight on this matter?

Dinaiscoding
  • 992
  • 1
  • 7
  • 16

3 Answers3

54

For those who might be suffering from this issue:

class-validator requires you to use service containers if you want to inject dependencies into your custom validator constraint classes. From: https://github.com/typestack/class-validator#using-service-container

import {useContainer, Validator} from "class-validator";

// do this somewhere in the global application level:
useContainer(Container);

So that we need to add the user container function into the global application level.

1. Add the following code to your main.ts bootstrap function after app declaration:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  useContainer(app.select(AppModule), { fallbackOnErrors: true });
...}

The {fallbackOnErrors: true} is required, because Nest throw Exception when DI doesn't have required class.

2. Add Injectable() to your constraint:

import {ValidatorConstraint, ValidatorConstraintInterface} from 'class-validator';
import {UsersService} from './user.service';
import {Injectable} from '@nestjs/common';

@ValidatorConstraint({ name: 'isUserAlreadyExist', async: true })
@Injectable() // this is needed in order to the class be injected into the module
export class IsUserAlreadyExist implements ValidatorConstraintInterface {
    constructor(protected readonly usersService: UsersService) {}

    async validate(text: string) {
        const user = await this.usersService.findOne({
            email: text
        });
        return !user;
    }
}

3. Inject the constraint into your module as a provider and make sure that the service you intend to inject into your constraint are also available to a module level:

import {Module} from '@nestjs/common';
import { UsersController } from './user.controller';
import { UsersService } from './user.service';
import { IsUserAlreadyExist } from './user.validator';

@Module({
    controllers: [UsersController],
    providers: [IsUserAlreadyExist, UsersService],
    imports: [],
    exports: []
})
export class UserModule {
}
Dinaiscoding
  • 992
  • 1
  • 7
  • 16
  • This is a great anwer, however, I would love to see how you would inject the container inside app.module.ts instead of main.ts, since when running tests main.ts does not get triggered. – FooBar Jan 05 '22 at 22:31
  • 1
    I couldn't figure out step 3 - you answer just saved me a whole lot of nerves :) – Michael Auerswald Jan 14 '22 at 21:04
  • @FooBar you can inject ModuleRef into the module(`constructor(private moduleRef: ModuleRef) {}`) and then call useContainer in onModuleInit(`onModuleInit() { useContainer(this.moduleRef, { fallbackOnErrors: true }); }`) – juraj Feb 07 '22 at 14:50
  • 1
    When using the repository pattern injecting the repository would work the same way, refer to this [article](https://dev.to/avantar/custom-validation-with-database-in-nestjs-gao) – Onen simon Mar 29 '22 at 11:43
  • @juraj Unfortunately, this does not work for me – xersiee Mar 29 '23 at 09:35
7

By the way, this doesn't work in e2e test. This is the way how I get it running.

beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
        imports: [AppModule],
    }).compile();
    app = moduleFixture.createNestApplication();

    app.useGlobalPipes(GetValidationPipe());
    useContainer(app.select(AppModule), { fallbackOnErrors: true });
    await app.init();
});
passport4j
  • 243
  • 3
  • 6
4

You'll need to update class-validator's container to use the Nest application to allow for Dependency Injection everywhere. This GitHub Issue goes through how to do it and some struggles people have faced with it.

Quick fix without reading the link:

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  useContainer(app, { fallback: true });
  await app.listen(3000);
}
bootstrap();

When doing this, make sure you also register your validators as you normally would any nest @Injectable()

Jay McDoniel
  • 57,339
  • 7
  • 135
  • 147