Nest: Dependency injection with class-validator

Created on 26 Mar 2018  路  45Comments  路  Source: nestjs/nest

I'm submitting a...


[ ] Regression 
[ ] Bug report
[ ] Feature request
[x ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

As mentioned in the documention NestJS is supposed to works well very with class validator, however i'm not successful trying to implement a custom validation class ( https://github.com/typestack/class-validator#custom-validation-classes ) or decorator.
This usecase is more than usefull to implement complex validation logic that require injecting services
As its a pretty standart usecase, i guess that there is a way to do it, but i'm not able to find it! could be a great addition to the online documentation imo !

Expected behavior

I've tried to add my Validator class as a component but not working.
In the class-validator documentation they mentioned that we can register a DI, is this doable with Nest ?

Minimal reproduction of the problem with instructions

@Controller('cars')
export class CarController {

    @Get()
    public test(@Res() res) {
        const dto = new CarInsertDTO();
        dto.brand = 'toyota';

        validate(dto).then(errs => {
            if (errs && errs.length > 0) {
                throw new BadRequestException(errs);
            }

            res.send(200);
        });
    }
}

export class CarInsertDTO {
    @IsString()
    public name: string;

    @Validate(BrandValidator, { message: 'Invalid brand'})
    public readonly strategy;
}


@ValidatorConstraint()
export class BrandValidator implements ValidatorConstraintInterface {
    constructor(private readonly brandService: BrandService) {}

    validate(brandName: any, args: ValidationArguments) {
        // can be a http service , database service or whatever
        const brands: string[] = this.brandService.all();

        return brands.indexOf(brandName) >= 0;
    }
}

Should lead to something like

[Nest] 1736 - 2018-3-26 12:16:38 [ExceptionsHandler] Cannot read property 'all' of undefined
TypeError: Cannot read property 'all' of undefined

question 馃檶

Most helpful comment

Hi guys, I found a solution for that, you can check my repo: https://github.com/neoteric-eu/nestjs-auth (btw, yesterday I was making a tech-workshops for developers in Gda艅sk about how to proper handle authorisation and authentication with nest :smile: ), so, yeah @kamilmysliwiec I spread the love for Nest.js around the world :smile_cat:

@yamid-ny the solution for that in particular is done in two steps, first one:

        this.app = await NestFactory.create(AppModule, {
            logger: new AppLogger('Nest')
        });
        useContainer(this.app, {fallbackOnErrors: true});

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

Second thing:

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

@ValidatorConstraint({ name: 'isUserAlreadyExist', async: true })
@Injectable()
export class IsUserAlreadyExist implements ValidatorConstraintInterface {
    constructor(protected readonly userService: UserService) {}

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

IsUserAlreadyExist have to be Injetable and registered in _nest_ module

Like so:

import {Module} from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { UserController } from './user.controller';
import { userProviders } from './user.providers';
import { UserService } from './user.service';
import {IsUserAlreadyExist} from './user.validator';

@Module({
    controllers: [UserController],
    providers: [...userProviders, IsUserAlreadyExist, UserService],
    imports: [DatabaseModule],
    exports: [UserService]
})
export class UserModule {
}

Then when I try to do POST /users twice with payload as follow:

{"name": "cojack", "email": "[email protected]", "password": "qwer1234", "roles": ["user"]}

I have got following response:

{"statusCode":422,"error":"Unprocessable Entity","message":[{"property":"email","children":[],"constraints":{"isUserAlreadyExist":"User already exists"}}]}

Hell yeah!

@fmeynard @jmaicaaan @hershko765 @roeehershko - you also guys might be interested how to solve this problem.

Regards ;)

All 45 comments

What is the implementation of your brandService @fmeynard ? How is it imported?

@jmaicaaan

This is only a code sample to illustrate my issue but the brandService is Nest component like

@Component
export class BrandService {
  public all() {
     return ['toyota','ford']; // in a real world this can a remote call or a database call
}

So my main goal is the use a @Component inside a validator class

I believe nest couldn't find the BrandValidator you are using.. Maybe do you need to add @component also to the BrandValidator to make it injectable and registered to the nest di?

With typeDI module I guess this would work but in nest it can be another work around.

@jmaicaaan As said in the initial request, i've also tried that ( to add the BrandValidator as a @Component ), but its not working, i guess mainly because class-validator don't use Nest DI

It's impossible to inject dependencies into @ValidatorConstraint with Nest.

So is there a better way of doing it? i have similar issue where i need to validate if the user email is existing in the database and i want to use the class-validator, is there any workaround?

@hershko765

The only "workaround" that i have found is very dirty ATM : i've added typedi to my project and i manually register @Components that will be use by class-validator

import { Container } from 'typedi';
// ... others imports

async function bootstrap() {
    const app = await NestFactory.create(ApplicationModule);
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, validationError: { target: false } }));

    const dataSourceModule = app.select(DataSourceModule);
    Container.set(DataSourceManager, dataSourceModule.get(DataSourceManager));
    Container.set(DataSourceService, dataSourceModule.get(DataSourceService));

    await app.listen(config.get('app.port'));
}

According to @kamilmysliwiec 's answer, don't think we have a better way to do that unfortunately

@fmeynard i found a solution, i think its new feature of class-validator called "useContainer",
once you do it, class-validator will use nestJS di

main.ts

import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './app.module';
import {useContainer} from 'class-validator';
import {ValidationPipe} from '@nestjs/common';

async function bootstrap() {
    const app = await NestFactory.create(ApplicationModule);
    const validationPipe = app
        .select(ApplicationModule)
        .get(ValidationPipe);

        // This will cause class-validator to use the nestJS module resolution, 
       // the fallback option is to spare our selfs from importing all the class-validator modules to nestJS
    useContainer(app.select(ApplicationModule), { fallback: true });

    app.useGlobalPipes(validationPipe);
    await app.listen(3000);
}
bootstrap();

my constaint: entityExists.constraint.ts

@ValidatorConstraint({ name: 'entityExists', async: false })
@Component()
export class EntityExists implements ValidatorConstraintInterface {

    constructor(
        @Inject('DbConnectionToken') private readonly connection: Connection,
    ) {}

    async validate(text: string, validationArguments: ValidationArguments) {
        const entity = await this.connection.getRepository(validationArguments.constraints[0]).findOneById(text);
        return !! entity;
    }

    defaultMessage(args: ValidationArguments) {
        console.log(args);
        return 'Entity not exists';
    }
}

My dto file:

...
export class CreateCustomerDto {
    @Validate(EntityExists, [Customer])
    customer_id: number;

    ....
}

And last step is to add the constraint to your desired module

@Module({
    imports: [],
    controllers: [
        ...
    ],
    components: [
        Validator,
        ValidationPipe,
        EntityExists,
        ...
    ],
})
export class ApplicationModule implements NestModule {

}

pretty simple, 1 line makes it all work :)
you can create your own "ValidationModule" add there all the constraints and stuff, i am just a random lazy guy

Great job @roeehershko!
Also, in v5.0.0 you should be able to use the following syntax:

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

馃敟 馃敟 馃敟 馃敟

Hello Nestjs entrepreneurs, in core version : 5.0.0-beta.6 I implement

import { useContainer } from 'class-validator';
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  useContainer(app, { fallback: true });
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

Not work and get this error
[Nest] 9880 - 2018-6-4 20:39:27 [ExceptionHandler] Nest cannot find given element (it does not exist in current context)

I would like do dependency injection on a custom validator like this

@ValidatorConstraint({ name: 'isUserAlreadyExist', async: false })
export class IsUserAlreadyExist implements ValidatorConstraintInterface {
  constructor(private readonly usersService: UsersService) {}

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

@yamid-ny Could you share your repository? I'd love to reproduce this issue somewhere.

Yes @kamilmysliwiec https://github.com/yamid-ny/dependency-injection-on-custom-validator thanks !!

http://localhost:3000/auth/register // when register same email user

{
    "email": "[email protected]",
    "password": "123456"
}

Hi guys, I found a solution for that, you can check my repo: https://github.com/neoteric-eu/nestjs-auth (btw, yesterday I was making a tech-workshops for developers in Gda艅sk about how to proper handle authorisation and authentication with nest :smile: ), so, yeah @kamilmysliwiec I spread the love for Nest.js around the world :smile_cat:

@yamid-ny the solution for that in particular is done in two steps, first one:

        this.app = await NestFactory.create(AppModule, {
            logger: new AppLogger('Nest')
        });
        useContainer(this.app, {fallbackOnErrors: true});

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

Second thing:

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

@ValidatorConstraint({ name: 'isUserAlreadyExist', async: true })
@Injectable()
export class IsUserAlreadyExist implements ValidatorConstraintInterface {
    constructor(protected readonly userService: UserService) {}

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

IsUserAlreadyExist have to be Injetable and registered in _nest_ module

Like so:

import {Module} from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { UserController } from './user.controller';
import { userProviders } from './user.providers';
import { UserService } from './user.service';
import {IsUserAlreadyExist} from './user.validator';

@Module({
    controllers: [UserController],
    providers: [...userProviders, IsUserAlreadyExist, UserService],
    imports: [DatabaseModule],
    exports: [UserService]
})
export class UserModule {
}

Then when I try to do POST /users twice with payload as follow:

{"name": "cojack", "email": "[email protected]", "password": "qwer1234", "roles": ["user"]}

I have got following response:

{"statusCode":422,"error":"Unprocessable Entity","message":[{"property":"email","children":[],"constraints":{"isUserAlreadyExist":"User already exists"}}]}

Hell yeah!

@fmeynard @jmaicaaan @hershko765 @roeehershko - you also guys might be interested how to solve this problem.

Regards ;)

@kamilmysliwiec

useContainer(app, { fallbackOnErrors: true });

and
```
useContainer(app.select(AppModule), { fallbackOnErrors: true });

behaves different. 
In first case I've got few errors spitted into the console like this 

[Nest] 3442 - 2018-6-12 16:43:51 [ExceptionHandler] Nest cannot find given element (it does not exist in current context)
```
while container tries to get Validator and MetadataStorage classes.
But finally code works correctly, and I got all injected services as expected;

In the second case there are no errors and everything works like a charm.

Can you explain why?

Hello Community !!!
How can I validate (all / multiple) Schema fields required ?
For example, UserSchema has email, nickname; PostSchema has title ( ... 3 or more fields, 3 or more dependency injections ... )
Currenly I use IsUserAlreadyExist from user.validator, this only validate one field in one schema (email).
In a real project We need some like "@Unique" in class-validator ModelDto.
You know a solution ?

@kamilmysliwiec I am also interested in why useContainer(app.select(AppModule), { fallbackOnErrors: true }); works perfectly, while useContainer(app, { fallbackOnErrors: true }); throws
[Nest] 3442 - 2018-6-12 16:43:51 [ExceptionHandler] Nest cannot find given element (it does not exist in current context)

Anyways, just wanted to tell you that this project is amazing! Keep up the great work!

@ullalaaron I've explain it in my comment.

@cojack where exactly?

In version 5.1.0, everything works if i write

In bootstrap function:

const app = await NestFactory.create(ApplicationModule);
useContainer(app.select(ApplicationModule), { fallbackOnErrors: true });
await app.listen(config.api.port);

Validator function:

@ValidatorConstraint({ name: 'isUserAlreadyExist', async: true })
@Injectable()
export class IsUserAlreadyExist {
  constructor(
    @Inject('UserService') private readonly userService: UserService,
  ) {}

  async validate(text: string) {
    const user = await this.userService.findOneByEmail(text);
    return !user;
  }

  defaultMessage(args: ValidationArguments) {
    return 'User with this email already exists.';
  }
}

User entity:

@Entity()
export default class User {
  @PrimaryGeneratedColumn('uuid') id: string;

  @IsNotEmpty()
  @MaxLength(100)
  @Validate(IsUserAlreadyExist)
  @IsEmail()
  @Column({
    type: 'varchar',
    length: 100,
    primary: true,
  })
  email: string;
}

I use the sample provided and works fine, when i change the service
with @InjectRepository the validation

I have the following problem
"Nest can't resolve dependencies of the AnagraficaService (?). Please make sure that the argument at index [0] is available in the AnagraficaService context."

If i remove the Validate all is ok

@ApiModelProperty()
    @IsNotEmpty()
    @IsEmail()
    /*@ValidateIf(o => o.id === undefined || o.id <= 0)
    @Validate(IsEmailAlreadyExist, {
        message: "Email gi脿 utilizzata"
    })*/
    @Column({ length: 50 })
    Email: string;


@Injectable()
export class AnagraficaService extends BaseService<Anagrafica> {
    constructor(
        @InjectRepository(Anagrafica)
        private readonly todoRepository: Repository<Anagrafica>,
    ) {
        super(todoRepository);
    }
}

@Module({
    imports: [
        TypeOrmModule.forFeature([Anagrafica]), 
    ],
    controllers: [
... ],
    providers: [
        AnagraficaService,
        IsEmailAlreadyExist,
        IsAnagraficaAlreadyExist,
    ]

})
export class AnagraficaModule {}

@andreanta I have the same problem, did you find any solutions ?

To answer to @andreanta, when using typeorm forFeature, the validation container is initialized before the nest container.
We can't inject the repository because he is not yet in the container.

The workaround i found, is to create a method on your implementation of the ValidatorConstraintInterface to set the service before the validation happens in the constructor of any module.
To get the constraint, we can use the getFromContainer function from the 'class-validator' module.
Then we can inject manually any service implementing a setter.

@Module({
    imports: [TypeOrmModule.forFeature([MyEntity])]
})
export class AppModule {
    constructor(
        private myRepository: Repository<MyEntity>
    ) {
        const t = getFromContainer(MyConstraint);
        t.setRepository(this.myRepository);
    }
}
@ValidatorConstraint({ name: 'MyConstraint' })
export class MyConstraint implements ValidatorConstraintInterface {

    private myRepository: Repository<MyEntity>;

    defaultMessage?(validationArguments?: ValidationArguments): string {
        return 'Validation fail';
    }

   validate(value: any, args: ValidationArguments) {
        // your validation code
        // here you can now use this.myRepository.find(...)
    }

    setRepository(myRepository: Repository<MyEntity>) {
        this.myRepository = myRepository;
    }
}

@Rmannn and @andreanta check my comment, it's already explained how it should be configured to use di from class-validator https://github.com/nestjs/nest/issues/528#issuecomment-395338798

@cojack Thank you, I saw your comment, but, as I said, when using typeorm forFeature() in a module,
the validation container is initialized before the nest container and the service is injected before to be created. So the class-validator will fail to find the service to inject.
The useContainer is call to late, (only in our case) because my model requires the decorator that requires the constraint that requires the repository that is not ready...
Model -> validation decorator -> constraint -> repo.
A repository need the model to be initialized.
The pb occurs only because we are using a Repository i guess. it should works as you said if we use any others services out of the scope of dependencies of typeorm.

How is this issue closed? Are there any updates on this? As @Rmannn explained, @cojack's solution doesn't work when using TypeOrmModule.forFeature([Entity])

I also have the same problem when using forFeature()

Got the same problem when using TypeOrmModule.forFeature([Entity])

it shows "TypeError: Cannot read property 'findOneByEmail' of undefined" from calling injected UserService method

@ullalaaron I was wrong, I didn't explain that:

@kamilmysliwiec I am also interested in why useContainer(app.select(AppModule), { fallbackOnErrors: true }); works perfectly, while useContainer(app, { fallbackOnErrors: true }); throws
[Nest] 3442 - 2018-6-12 16:43:51 [ExceptionHandler] Nest cannot find given element (it does not exist in current context)

Anyways, just wanted to tell you that this project is amazing! Keep up the great work!

And I'm also curious why it works like this, @kamilmysliwiec could you?

@cdiaz and @Vergil0327 my solution is working for concept described here https://docs.nestjs.com/recipes/sql-typeorm I know that TypeOrmModule do a lot of out of the box, but I really like this approach, a bit more to write by hand, but is fine for me.

bump

Also I wonder if it possible to @Inject(REQUEST) in custom constraint.

A proper Nest solution to this is surely needed. The Nest docs talk about and reference class-validator _a lot_. As a person learning the framework you assume that it's integrated, or able to be integrated, fully.

In any case, here's a fairly simple workaround I'm using that works due to the fact that class-validator doesn't create constraints until they are needed...

In main.ts export the application instance...

export let app: INestApplication;

async function bootstrap() {
  app = await NestFactory.create(AppModule);
  await app.listen(3000);
}

Then, manually inject what you need In your validation constraint...

@ValidatorConstraint({ async: true })
class NotExistsConstraint implements ValidatorConstraintInterface {

  private entityManager: EntityManager;

  constructor() {
    this.entityManager = app.get(EntityManager);
  }

  async validate(value: any, validationArguments?: ValidationArguments): Promise<boolean> {
    ...
  }

I'm fairly new to Nestjs and I assume this configuration is safe to use, since it currently works.

Also I wonder if it possible to @Inject(REQUEST) in custom constraint.

I was working on this issue, and figured out a workaround that works relatively well if you want to use request context stuff in custom validators for example. It requires creating an additional interceptor and pipe. It is a little hacky though, and haven't completely tested it, but it appears to work:

context.interceptor.ts

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'
import { Observable } from 'rxjs'

/**
 * Injects customerId and userId into the context, so that the ValidationPipe can use it.
 */
@Injectable()
export class ContextInterceptor implements NestInterceptor {
    intercept(
        context: ExecutionContext,
        next: CallHandler
    ): Observable<any> {
        const request = context.switchToHttp().getRequest()

        if (request.body ) {
            request.body.context = {
                customerId: request.customer && request.customer.id,
                userId: request.user && request.user.id,
            }
        }

        return next.handle()
    }
}

and strip-context.pipe.ts

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'

/**
 * Strips the context that is injected to be used by the validation pipe
 */
@Injectable()
export class StripContextPipe implements PipeTransform {
    transform(value: any, metadata: ArgumentMetadata) {
         if ( value.context) {
                const { context, ...rest } = value
                return rest
         }
        return value
    }
}

And an interface to use in validators:

import { ValidationArguments } from 'class-validator'

export interface ContextValidationArguments extends ValidationArguments {
    object: {
        context: {
            customerId: number
            userId: number
        }
    } & Object
}

And in the actual ValidatorConstraint class that can be used to create decorators you can reference those values using the validationArguments:

@ValidatorConstraint()
@Injectable()
export class VariableNotExistsConstraint implements ValidatorConstraintInterface {
    defaultMessage(validationArguments?: ValidationArguments): string {
        return ""
    }

    async validate(value: any, validationArguments?: ContextValidationArguments): Promise<boolean> {
        const customerId = validationArguments.object.context.customerId
        const userId = validationArguments.object.context.userId

        return true
    }
}

Hope this helps some of you who need access to request stuff in ValidationOptions, let me know if you have questions

EDIT: I forgot to mention that you need to add the interceptor and pipe to your main.ts file if you want them on every request (StripContextPipe after the ValidationPipe)

EDIT2: Also discovered that pipes are applied on controller parameter decorators as well, so I modified the strip-context pipe a bit to not break those

@kamilmysliwiec as per @evilive3000 's comment
useContainer(app, { fallbackOnErrors: true }); and useContainer(app.select(AppModule), { fallbackOnErrors: true }); behave differently.

class-validator tries to get the instance of given class using findInstanceByPrototypeOrToken method of NestApplicationContext. Possible cause of failure when using NestApplication instance instead of NestApplicationContext is the lack of tokenization that happens when NestApplicationContext's this.moduleTokenFactory.create(module, scope); is run.

Got the same problem when using TypeOrmModule.forFeature([Entity])

it shows "TypeError: Cannot read property 'findOneByEmail' of undefined" from calling injected UserService method

I've the same problem, i think we need a clean solution for that

What about unit test?
Would like to test ContraintClass with dependencies.
Is there any temporary solution to get/simulate iocContainer from TestingModule instance?

`
const module: TestingModule = await Test.createTestingModule(...);
const app: INestApplication = module.createNestApplication();

useContainer(app, { fallbackOnErrors: true });
`

No luck on this

@cdiaz @Vergil0327 @matzmz, for now, I'm injecting the service with moduleRef:

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

@ValidatorConstraint({ name: 'isUserAlreadyExist', async: true })
@Injectable()
export class IsUserAlreadyExist implements ValidatorConstraintInterface {
  private userService: UserService;

  constructor(private readonly moduleRef: ModuleRef) {}

  async validate(username: string) {
    const user = await this.userService.findByUsername(username);
    return !user;
  }

  defaultMessage() {
    return 'user with this username already exists.';
  }

  onModuleInit() {
    this.userService = this.moduleRef.get('UserService');
  }
}

If I try to inject the UserService directly a circular import error comes:

Error: A circular dependency has been detected inside @InjectRepository(). Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()". Also, try to eliminate barrel files because they can lead to an unexpected behavior too.

My UserService inject a repository with the InjectRepository decorator. Maybe this decorator has a check for circular dependencies?

@javialon26 your solution isn't working for me, the userService is undefined when the validate method is called. Have you done something else ?

@kamilmysliwiec any updates on this ? It seems the bug is related to the TypeOrmModule from nestjs/typeorm as @cojack made this work without the nestjs/typeOrm Module.

This is working for me, hope it can be of use for anyone.

Basically I created a separate module for the validator constraints and imported the feature for using in the validator.

main.ts - Application start point (calls useContainer of class-validator).

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import { useContainer } from "class-validator";

async function bootstrap() {
    const app = await NestFactory.create(AppModule);

    app.useGlobalPipes(
        new ValidationPipe({
            transform: true,
            forbidUnknownValues: true,
            validationError: { target: false }
        })
    );

    useContainer(app.select(AppModule), { fallbackOnErrors: true });

    await app.listen(3000);
}

bootstrap();

validator.module.ts - Nest.js module for injecting class-validator validators that need dependency injection and imports a TypeOrm repository for use inside the validator.

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Agencia } from "./cliente/agencia.entity";
import { AgenciaExists } from "./cliente/agencia-exists";

@Module({
    imports: [TypeOrmModule.forFeature([Agencia])],
    providers: [AgenciaExists]
})
export class ValidatorModule {}

app.module.ts - Main module importing the ValidatorModule.

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { ValidatorModule } from "./validator.module";
import { ClienteModule } from "./cliente/cliente.module";

@Module({
    imports: [TypeOrmModule.forRoot(), ValidatorModule, ClienteModule]
})
export class AppModule {}

agencia-exists.ts - My custom validator that checks an Agencia entity exists in the database.

import {
    ValidatorConstraint,
    ValidatorConstraintInterface,
    ValidationArguments
} from "class-validator";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Agencia } from "./agencia.entity";

@ValidatorConstraint({ async: true })
@Injectable()
export class AgenciaExists implements ValidatorConstraintInterface {
    constructor(
        @InjectRepository(Agencia) private repository: Repository<Agencia>
    ) {}

    async validate(id: number, args: ValidationArguments): Promise<boolean> {
        const entity = await this.repository.findOne(id);
        return entity !== undefined;
    }
}

cliente-input.ts - A DTO used for request body in POST and PATCH methods in cliente.controller.ts.

import { Validate } from "class-validator";
import { AgenciaExists } from "./agencia-exists";

export class ClienteInput {
    @Validate(AgenciaExists, { message: "Ag锚ncia com id $value n茫o existe." })
    agencia: number;
}

cliente-controller.ts - Controller of entity Cliente. The entity Agencia is a required related entity (foreign key).

import { Controller, Post, Body } from "@nestjs/common";
import { ClienteInput } from "./cliente.input";
import { ClienteService } from "./cliente.service";

@Controller("clientes")
export class ClienteController {
    constructor(private service: ClienteService) {}

    @Post()
    async create(@Body() input: ClienteInput): Promise<void> {
        // Cast DTO to entity and save...
    }
}

Thanks @julianomqs, are you using the @Validate on the Cliente entity ? It's working fine for me in that case.

Although I get an A circular dependency has been detected inside @InjectRepository(). if I try to @Validate a property on the Agencia entity itself (for example a slug field is unique)

Can you confirm you have the same behavior ?

Hello @frco9

Sorry I forgot to put where the validator was used. I updated my previous comment.

No, I'm not using the validator in an entity, but in a DTO used for input (request body) for POST and PATCH methods of my controller.

The entity Agencia is a required foreign key of the entity Cliente.

That's clearer to me many thanks @julianomqs !

@kamilmysliwiec this should be added to docs

Just FYI, I'm not using TypeORM, instead I'm using mongoose, so, I have a service that connects to the DB schema, to achieve that in the ValidatorConstraint it's pretty the same way as the steps that @julianomqs posted before.

1. Set up service container

main.ts

import { NestFactory } from '@nestjs/core';

import { AppModule } from './app/app.module';
import { ValidationPipe } from '@nestjs/common';
import { useContainer } from 'class-validator';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const globalPrefix = 'api';
  app.setGlobalPrefix(globalPrefix);
  app.useGlobalPipes(new ValidationPipe({
    forbidUnknownValues: true,
    validationError: {
      target: false,
    }
  }));

  useContainer(app.select(AppModule), {fallbackOnErrors: true});
  const port = process.env.port || 3333;
  await app.listen(port, () => {
    console.log('Listening at http://localhost:' + port + '/' + globalPrefix);
  });
}

2. Create my validator

unique.ts (custom validation)

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments
} from "class-validator";
import { UserService } from '@alp-api/db/services';
import { Injectable } from '@nestjs/common';

@ValidatorConstraint({ async: true })
@Injectable()
export class EmailIsUnique implements ValidatorConstraintInterface {

  constructor(private userService: UserService){}

  validate(email: any, args: ValidationArguments) {
    return this.userService.findByEmail(email).then(user => !user);
  }

}

3. Set up custom validation into db.module (validator module)

Instead of creating a new module for custom validations, I used my already created db module to set my validation into the providers

db.module.ts

import { Module, Global } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

// Schemas
import { UserSchema } from './schemas';
import { UserService } from './services';

// DB Validators
import  { EmailIsUnique }  from './validators/unique';

@Global()
@Module({
  imports: [
    MongooseModule.forFeature([
      { name: 'User', schema: UserSchema }
    ])
  ],
  providers: [
    UserService,
    EmailIsUnique
  ],
  exports: [
    MongooseModule,
    UserService,
  ]
})
export class DbModule {}

Note

Surely, my db.module.ts is imported into my app.module.ts

A generic validator without a global pipe

is-unique.validator.ts

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
  ValidationOptions,
  registerDecorator
} from "class-validator";
import { getManager } from "typeorm";

@ValidatorConstraint({ async: true })
export class isUniqueValidator implements ValidatorConstraintInterface {
  validate(columnNameValue: any, args: ValidationArguments) {
    const params = args.constraints[0];
    return getManager()
      .query(
        `SELECT * FROM ${params.table} WHERE ${params.column} = '${columnNameValue}'`
      )
      .then(user => {
        if (user[0]) return false;
        return true;
      });
  }
}
export function IsUnique(params: {}, validationOptions?: ValidationOptions) {
  return function(object: Object, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [params],
      validator: isUniqueValidator
    });
  };
}

validator.module.ts

import { isUniqueValidator } from "./is-unique.validator";
import { Module } from "@nestjs/common";

@Module({
  providers: [isUniqueValidator]
})
export class ValidatorsModule {}

user.entity.ts

import { IsUnique } from "./../validators/is-unique.validator";
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { IsOptional, IsString, IsNotEmpty, IsEmail } from "class-validator";
import { CrudValidationGroups } from "@nestjsx/crud";

const { CREATE, UPDATE } = CrudValidationGroups;

@Entity("users")
export class UserEntity {
  @PrimaryGeneratedColumn()
  id: string;

  @IsOptional({ groups: [UPDATE] })
  @IsNotEmpty({ groups: [CREATE] })
  @IsEmail({}, { always: true })
  @IsUnique(
    { table: "users", column: "email" },
    { message: "Email $value already exists", groups: [CREATE] }
  )
  @Column({ unique: true })
  email: string;

  @IsOptional({ groups: [UPDATE] })
  @IsNotEmpty({ groups: [CREATE] })
  @IsString({ always: true })
  @Column({ nullable: true })
  name: string;

  @Column()
  password: string;
}

Import ValidatorsModule in app.module.ts

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

FranciZ picture FranciZ  路  3Comments

anyx picture anyx  路  3Comments

yanshuf0 picture yanshuf0  路  3Comments

mishelashala picture mishelashala  路  3Comments

JulianBiermann picture JulianBiermann  路  3Comments