Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependency injection with class-validator #528

Closed
fmeynard opened this issue Mar 26, 2018 · 45 comments
Closed

Dependency injection with class-validator #528

fmeynard opened this issue Mar 26, 2018 · 45 comments

Comments

@fmeynard
Copy link

fmeynard commented Mar 26, 2018

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

@jmaicaaan
Copy link

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

@fmeynard
Copy link
Author

@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

@jmaicaaan
Copy link

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.

@fmeynard
Copy link
Author

@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

@kamilmysliwiec
Copy link
Member

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

@hershko765
Copy link

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?

@fmeynard
Copy link
Author

fmeynard commented Apr 2, 2018

@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

@roeehershko
Copy link

roeehershko commented Apr 18, 2018

@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

@kamilmysliwiec
Copy link
Member

kamilmysliwiec commented Apr 18, 2018

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();

🔥 🔥 🔥 🔥

@kamilmysliwiec kamilmysliwiec added this to the 5.0.0 milestone Apr 20, 2018
@yamid-ny
Copy link

yamid-ny commented Jun 5, 2018

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

@kamilmysliwiec
Copy link
Member

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

@yamid-ny
Copy link

yamid-ny commented Jun 5, 2018

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

@cojack
Copy link
Contributor

cojack commented Jun 7, 2018

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 😄 ), so, yeah @kamilmysliwiec I spread the love for Nest.js around the world 😸

@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 ;)

@evilive3000
Copy link

@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?

@yamid-ny
Copy link

yamid-ny commented Jun 21, 2018

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 ?

@ullalaaron
Copy link

ullalaaron commented Jun 30, 2018

@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!

@cojack
Copy link
Contributor

cojack commented Jul 2, 2018

@ullalaaron I've explain it in my comment.

@ullalaaron
Copy link

@cojack where exactly?

@aduryagin
Copy link

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

@andreanta
Copy link

andreanta commented Nov 7, 2018

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

@Rmannn
Copy link

Rmannn commented Dec 21, 2018

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

@Rmannn
Copy link

Rmannn commented Dec 22, 2018

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

@cojack
Copy link
Contributor

cojack commented Dec 24, 2018

@Rmannn and @andreanta check my comment, it's already explained how it should be configured to use di from class-validator #528 (comment)

@Rmannn
Copy link

Rmannn commented Dec 24, 2018

@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.

@ullalaaron
Copy link

ullalaaron commented Feb 1, 2019

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])

@cdiaz
Copy link

cdiaz commented Mar 3, 2019

I also have the same problem when using forFeature()

@Vergil0327
Copy link

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

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

@cojack
Copy link
Contributor

cojack commented Mar 8, 2019

@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.

@cojack
Copy link
Contributor

cojack commented Mar 20, 2019

bump

@vkartaviy
Copy link

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

@kdubb
Copy link

kdubb commented May 29, 2019

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.

@arjovanramshorst
Copy link

arjovanramshorst commented May 29, 2019

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

@piotrfrankowski
Copy link

piotrfrankowski commented Jun 8, 2019

@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.

@matzmz
Copy link

matzmz commented Jun 12, 2019

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

@hellraisercenobit
Copy link

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

@javialon26
Copy link

@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?

@frco9
Copy link

frco9 commented Jul 19, 2019

@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.

@julianomqs
Copy link

julianomqs commented Aug 20, 2019

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

@frco9
Copy link

frco9 commented Aug 20, 2019

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 ?

@julianomqs
Copy link

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.

@frco9
Copy link

frco9 commented Aug 20, 2019

That's clearer to me many thanks @julianomqs !

@TrejGun
Copy link

TrejGun commented Sep 4, 2019

@kamilmysliwiec this should be added to docs

@gemox94
Copy link

gemox94 commented Oct 26, 2019

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

@iturn
Copy link

iturn commented Nov 18, 2019

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

@lock
Copy link

lock bot commented Feb 16, 2020

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.

@lock lock bot locked as resolved and limited conversation to collaborators Feb 16, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests