How to inject service to validator constraint interface in nestjs using class-validator?
Asked Answered
P

4

27

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?

Peart answered 4/2, 2020 at 17:8 Comment(0)
P
73

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 {
}
Peart answered 9/2, 2020 at 21:20 Comment(5)
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.Pica
I couldn't figure out step 3 - you answer just saved me a whole lot of nerves :)Verdaverdant
@Pica you can inject ModuleRef into the module(constructor(private moduleRef: ModuleRef) {}) and then call useContainer in onModuleInit(onModuleInit() { useContainer(this.moduleRef, { fallbackOnErrors: true }); })Pernod
When using the repository pattern injecting the repository would work the same way, refer to this articleCreation
@Pernod Unfortunately, this does not work for meOtten
S
8

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();
});
Sonnysonobuoy answered 24/4, 2022 at 15:38 Comment(0)
B
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()

Brandi answered 4/2, 2020 at 17:45 Comment(3)
thanks for sharing. useContainer(app, { fallback: true }); what is its task?Humfried
can we use useContainer on the AppModule?Rephrase
@Rephrase not to my knowledge, because we need to pass app, the configured application, to the useContainer methodBrandi
W
0

In my case, I've fixed it like this:

import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
import isURL from 'validator/es/lib/isURL'; // Import for URL validation

@ValidatorConstraint({ name: 'isEmptyOrUrl' })
export class IsEmptyOrUrl implements ValidatorConstraintInterface {
  validate(value: string, validationArguments: ValidationArguments): boolean {
    // Check if the value is empty or a valid URL
    return value === '' || this.validateUrl(value);
  }

  private validateUrl(value: string): boolean {
    try {
      return isURL(value, { protocols: ['https'], require_protocol: true });
    } catch (error) {
      return false; // Invalid URL if an error occurs
    }
  }
}

In the DTO file you can use it like this:

import { IsEmptyOrUrl, IsOptional } from './path/to/validators'; // 
// Replace with actual path

export class MyDto {
  @IsOptional()
  @Validate(IsEmptyOrUrl)
  readonly facebook?: string;
}
Wilmerwilmette answered 22/5 at 2:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.