Skip to content

Instantly share code, notes, and snippets.

@zarv1k
Last active January 18, 2024 16:01
Show Gist options
  • Star 58 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save zarv1k/3ce359af1a3b2a7f1d99b4f66a17f1bc to your computer and use it in GitHub Desktop.
Save zarv1k/3ce359af1a3b2a7f1d99b4f66a17f1bc to your computer and use it in GitHub Desktop.
Unique Validator Example for NestJS
import { ValidationArguments, ValidatorConstraintInterface } from 'class-validator';
import { Connection, EntitySchema, FindConditions, ObjectType } from 'typeorm';
interface UniqueValidationArguments<E> extends ValidationArguments {
constraints: [
ObjectType<E> | EntitySchema<E> | string,
((validationArguments: ValidationArguments) => FindConditions<E>) | keyof E,
];
}
export abstract class UniqueValidator implements ValidatorConstraintInterface {
protected constructor(protected readonly connection: Connection) {}
public async validate<E>(value: string, args: UniqueValidationArguments<E>) {
const [EntityClass, findCondition = args.property] = args.constraints;
return (
(await this.connection.getRepository(EntityClass).count({
where:
typeof findCondition === 'function'
? findCondition(args)
: {
[findCondition || args.property]: value,
},
})) <= 0
);
}
public defaultMessage(args: ValidationArguments) {
const [EntityClass] = args.constraints;
const entity = EntityClass.name || 'Entity';
return `${entity} with the same '${args.property}' already exist`;
}
}
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from './config.service';
import useFactory from './db.factory';
import { Unique } from './validator';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [LoggerModule],
inject: [ConfigService],
useFactory,
}),
],
providers: [Unique],
})
export class DbModule {}
import { IsInt, IsOptional, IsString, MinLength, Validate } from 'class-validator';
import { Category } from './category.entity';
import { Unique } from './validator';
export class CategoryDto {
@IsInt()
// checks if value of CategoryDto.id is unique by searching in Category.id in DB
@Validate(Unique, [Category])
public id: string;
@IsString()
// checks if value of CategoryDto.title is unique by searching in Category.title in DB
@Validate(Unique, [Category])
public title: string;
@IsString()
// checks if value of CategoryDto.someField is unique by searching in Category.title in DB
@Validate(Unique, [Category, 'title'])
public someField: string;
@Column()
@IsNotEmpty()
@IsString()
// checks if pair of provided title+description is unique in DB
@Validate(
Unique,
[
Category,
({ object: { title, description } }: { object: CategoryForm }) => ({
title,
description,
}),
],
{
message: ({
targetName,
}: ValidationArguments) =>
`${targetName} with the same pair of title and description already exist`,
},
)
public description: string;
}
import { ValidatorConstraint } from 'class-validator';
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { UniqueValidator } from '../../utils/validator';
@ValidatorConstraint({ name: 'unique', async: true })
@Injectable()
export class UniqueAnotherConnection extends UniqueValidator {
constructor(
@InjectConnection('another-connection')
protected readonly connection: Connection,
) {
super(connection);
}
}
import { ValidatorConstraint } from 'class-validator';
import { Injectable } from '@nestjs/common';
import { InjectConnection } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { UniqueValidator } from '../../../utils/validator';
@ValidatorConstraint({ name: 'unique', async: true })
@Injectable()
export class Unique extends UniqueValidator {
constructor(@InjectConnection() protected readonly connection: Connection) {
super(connection);
}
}
@juniorpaiva95
Copy link

Hi, I was looking for a solution like that. However, when trying to apply it the moment my application starts I can recover the connection that is injected into my custom validator, but when executing the validate method this connection is undefined and I cannot get the repository through getRepository ().

Did you go through this? Or do you have any ideas?
Thanks in advance

@zarv1k
Copy link
Author

zarv1k commented Apr 16, 2020

Hey @juniorpaiva95, have you set up DI container of class-validator to use the NestJS' one at the app bootstrap stage?

// main.ts
...
import { useContainer } from 'class-validator';
...

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

Details are here. Hope this helps.

@keyurmodi21
Copy link

Hi @zarv1k,
Thanks for this gist code 😊
I'm trying to implement your unique validator to check if user exist before create method. I had a question regarding your example-dto.ts file, line number 3 has import { Exist, Unique } from './validator';. Would you mind helping how to create those methods? I couldn't find it in your abstract-unique-validator.ts file or any other.

Thanks in advance

@zarv1k
Copy link
Author

zarv1k commented May 27, 2020

Hi @zarv1k,
Thanks for this gist code 😊
I'm trying to implement your unique validator to check if user exist before create method. I had a question regarding your example-dto.ts file, line number 3 has import { Exist, Unique } from './validator';. Would you mind helping how to create those methods? I couldn't find it in your abstract-unique-validator.ts file or any other.

Thanks in advance

@keyurmodi21
Exist is not used, so I've just deleted it. Unique is a class - the UniqueValidator implementation for default DB connection (like UniqueAnotherConnection if your app has many DB connections).

@keyurmodi21
Copy link

Thanks a lot for quick response, it works 👍

@duongleh
Copy link

duongleh commented Jul 6, 2020

I changed the condition in abstract-unique-validator.ts to check for existence.
What can I do to validate array of id? I want to check whether every id of ExampleDto exists in ExampleEntity or not.

@IsArray()
public examples: ExampleDto[];
export class ExampleDto {
@IsNumber()
id: number;
}

@zarv1k
Copy link
Author

zarv1k commented Jul 7, 2020

@duongle26, you can use your custom Exist validator alongside with IsNumber in your nested ExampleDto, but this approach is not perfect, because for N ids you'll get N DB requests which is not perfect.

The gist is provided just as an example and just for checking uniqueness, so get creative and feel free to implement and apply another custom DB class validator, like this:

import { ValidationArguments, ValidatorConstraintInterface } from 'class-validator';
import { Connection, EntitySchema, FindConditions, In, ObjectType } from 'typeorm';
import { isObject } from '@nestjs/common/utils/shared.utils';

export interface ExistArrayValidationArguments<E> extends ValidationArguments {
  constraints: [
    ObjectType<E> | EntitySchema<E> | string,
    ((validationArguments: ValidationArguments, value: any[]) => FindConditions<E>) | keyof E,

    (
      | ((validationArguments: ValidationArguments, value: any[], entityCount: number) => boolean)
      | string
      | undefined
    ),
    number | undefined, // DB count result for (use only for customizing message)
  ];
}

export abstract class ExistArrayValidator implements ValidatorConstraintInterface {
  protected constructor(protected readonly connection: Connection) {}

  public async validate<E>(value: object[], args: ExistArrayValidationArguments<E>) {
    const [EntityClass, findCondition = args.property, validationCondition] = args.constraints;
    if (!value.length) {
      return true; // allows empty array
    }
    const entityCount = await this.connection.getRepository(EntityClass).count({
      where:
        typeof findCondition === 'function'
          ? findCondition(args, value)
          : {
              [findCondition]: In(
                validationCondition && typeof validationCondition !== 'function'
                  ? value.map(val => (isObject(val) ? val[validationCondition] : val))
                  : value,
              ),
            },
    });
    args.constraints[3] = entityCount;
    return typeof validationCondition === 'function'
      ? validationCondition(args, value, entityCount)
      : value.length === entityCount;
  }

  public defaultMessage(args: ValidationArguments) {
    const [EntityClass] = args.constraints;
    const entity = EntityClass.name || 'Entity';
    return `Some ${entity} does not exist`;
  }
}

Then implement it for your connection as ExistArray class and apply to your examples field in parent dto, e.g. like this:

@ValidateNested()
@Type(() => ExampleDto)
@Validate(ExistArray, [ExampleEntity, 'id', 'id'])
public examples: ExampleDto[];

This way you'll get the only one DB request with IN condition instead of N requests for every id.
Hope this helps.

@duongleh
Copy link

duongleh commented Jul 7, 2020

Thank you very much. You are brilliant. Btw, is there any way to return 409 Conflict error code for Unique and 404 Not found for Exist instead of default 400

@duongleh
Copy link

duongleh commented Jul 7, 2020

My second question: is there any proper way to get the arguments?

@Validate(Exist, [TransactionMethod, (args) => ({ id: args.value.transactionMethodId })], {  //here I pass args since I'm not very familiar TS typing
    message: () => 'Transaction method not found'
  })
transaction: CreateTransactionDto;
class CreateTransactionDto {
  @ApiProperty()
  @IsDefined()
  @IsNumber()
  transactionMethodId: number;
}

@zarv1k
Copy link
Author

zarv1k commented Jul 8, 2020

@duongle26

... Btw, is there any way to return 409 Conflict error code for Unique and 404 Not found for Exist instead of default 400.

Validator implementations shouldn't depend on application's transport layer, they should work in any application type (http, rpc microservices, etc.), coz in any application you should be able to validate objects. I believe that you're able to implement different status codes in HTTP application via nestjs interceptors and/or ValidationPipe.exceptionFactory.

My second question: is there any proper way to get the arguments?

I'm not sure I understood your problem.

@duongleh
Copy link

duongleh commented Jul 8, 2020

I'm not sure I understood your problem.

I mean what can I do to improve the args part

@Validate(Exist, [TransactionMethod, (args) => ({ id: args.value.transactionMethodId })]

Then I made it work with this

@Validate(Exist, [TransactionMethod, ({ value: { transactionMethodId } }: { value: CreateTransactionDto }) => ({ id: transactionMethodId )]

@vahidid
Copy link

vahidid commented Dec 7, 2020

Tnx for this gist, is really helpful for me, but i have an issue for connection in abstract-unique-validator.ts, it return undefined.
I use connection in json file and I use connection no where in my code. for repositories I use InjectRepository decorator.
can anyone give me an idea?

@zarv1k
Copy link
Author

zarv1k commented Dec 7, 2020

Tnx for this gist, is really helpful for me, but i have an issue for connection in abstract-unique-validator.ts, it return undefined.
I use connection in json file and I use connection no where in my code. for repositories I use InjectRepository decorator.
can anyone give me an idea?

@vahidid Have you already setup DI of class-validator?

@vahidid
Copy link

vahidid commented Dec 7, 2020

Tnx for this gist, is really helpful for me, but i have an issue for connection in abstract-unique-validator.ts, it return undefined.
I use connection in json file and I use connection no where in my code. for repositories I use InjectRepository decorator.
can anyone give me an idea?

@vahidid Have you already setup DI of class-validator?

yes i do, but still i get undefined and i found another solution, i use getConnection from typeorm:

import { Connection } from 'typeorm';

export abstract class UniqueValidator implements ValidatorConstraintInterface {
  protected constructor(
    protected readonly connection: Connection = getConnection('default'),
  ) {}

  public async validate<E>(value: string, args: UniqueValidationArguments<E>) {
    const [EntityClass, findCondition = args.property] = args.constraints;
    return (
      (await this.connection.getRepository(EntityClass).count({
        where:
          typeof findCondition === 'function'
            ? findCondition(args)
            : {
                [findCondition || args.property]: value,
              },
      })) <= 0
    );
  }

  public defaultMessage(args: ValidationArguments) {
    const [EntityClass] = args.constraints;
    const entity = EntityClass.name || 'Entity';
    return `${entity} with the same '${args.property}' already exist`;
  }
}

@hoangphunam0604
Copy link

@zarv1k
Thanks for code!
It work with create.
However when i update a record, this code will check unique with current record. This will return error.
I want skip check with id on route param or user.id of user on custom request.
How do i can get param on route or user on custom request?
Can you help me? Thanks!

@msyahidin
Copy link

@zarv1k
Thanks for code!
It work with create.
However when i update a record, this code will check unique with current record. This will return error.
I want skip check with id on route param or user.id of user on custom request.
How do i can get param on route or user on custom request?
Can you help me? Thanks!

Hi, I have same problem, did you get the answer? or how do you solved it?

@hoangphunam0604
Copy link

@zarv1k
Thanks for code!
It work with create.
However when i update a record, this code will check unique with current record. This will return error.
I want skip check with id on route param or user.id of user on custom request.
How do i can get param on route or user on custom request?
Can you help me? Thanks!

Hi, I have same problem, did you get the answer? or how do you solved it?

not yet now! my project is stopped ^^

@zarv1k
Copy link
Author

zarv1k commented Aug 12, 2021

@msyahidin,
@hoangphunam0604,

I believe you could inject any of your transport-layer (in HTTP or Microservice) param (not only req.user.id but any query, param,body, etc.) into your DTO somehow.
E.g. create (createParamDecorator) custom @Body decorator that would be responsible to injection of route-based params, or you can implement params injection into body by creating custom NestInterceptor.

@Jedliu
Copy link

Jedliu commented Aug 12, 2021

@zarv1k
Thanks for code!
It work with create.
However when i update a record, this code will check unique with current record. This will return error.
I want skip check with id on route param or user.id of user on custom request.
How do i can get param on route or user on custom request?
Can you help me? Thanks!

Hi, I have same problem, did you get the answer? or how do you solved it?

You could check this link nestjs/nest#173 (comment). It provides a possibility to have the RequestContext in the subscriber and also in the validator.

Please let me know if anyone has a better solution. Thanks,

@dan-scott-dev
Copy link

dan-scott-dev commented Aug 15, 2021

@msyahidin,
@hoangphunam0604

Here's my solution. Still not what I would have liked, but is my favourite of all solutions I've seen so far. My code needs some refactoring, I just don't want to look at it for a few days as I've been working on this issue for a couple days already!

As you cannot get the current request within a custom validator, even if you make the validtor injected for the request scope, you need to add the id (or whatever the primary key is or whatever else you'll be using the search for the row to be skipped) property to your DTO and then add the id to the request body before the validation code is run. There are several ways to do the latter, I opted for a custom decorator.

The decorator:

import { createParamDecorator, ExecutionContext, ParseIntPipe } from "@nestjs/common";

export enum transformToTypeTypes {
    INT = 'int'
}

export interface IAddParamsToBodyArgs {
    paramName: string,
    transformTo?: transformToTypeTypes
}

export const AddParamToBody = createParamDecorator ((args: IAddParamsToBodyArgs, ctx: ExecutionContext) => {
    const req = ctx.switchToHttp().getRequest();

    let value = req.params[args.paramName];

   if(args.transformTo === transformToTypeTypes.INT)
        value = parseInt(value);

    req.body[args.paramName] = value;

    return req;
});

The DTO:

export class UpdateThingDto {
    @IsNumber()
    @IsNotEmpty()
    id: number;

    @Validate(IsUniqueInDbRule, [Thing, 'myProperty', 'id'])
    myProperty: string;
}

The controller method:

@Put(':id')
    async updateOne (@Param('id', new ParseIntPipe()) id, @AddParamToBody({
        paramName: 'id',
        transformTo: transformToTypeTypes.INT
    }) @Body() thing: UpdateThingDto): Promise<Thing> {
        return await this.thingsService.updateOne(id, thing);
    }

And the validator (don't mean to re-invent what this gist already has, but just for ease here's my own, but you should be able to implement it in the validator further above too):

import {
    registerDecorator,
    ValidationOptions,
    ValidationArguments,
    ValidatorConstraint,
    ValidatorConstraintInterface
} from 'class-validator';
import { Injectable } from "@nestjs/common";
import { Connection, ObjectType, EntitySchema, FindConditions, Not } from "typeorm";

export interface UniqueValidationArguments<E> extends ValidationArguments {
    constraints: [
        EntitySchema<E>, // typeorm entity
        string, // column name
        string // other DTO's property name that will be used to search and skip
    ];
}


@ValidatorConstraint({ name: 'IsUniqueInDb', async: true })
@Injectable()
export class IsUniqueInDbRule implements ValidatorConstraintInterface {
    constructor(protected readonly connection: Connection) {}

    async validate<E> (value: any, args: UniqueValidationArguments<E>) {
        let repo = await this.connection.getRepository(args.constraints[0]);

// @todo: improve this, will be bad if multiple primary keys
        let primaryKey = await repo.metadata.primaryColumns[0].propertyName;

        let query = {};

        query[args.constraints[1]] = value;

        if(args.constraints[2])
            query[primaryKey] = Not((args.object as any)[args.constraints[2]]);

        let count = await repo.count(query);
``
        return count <= 0;
    }


    defaultMessage<E>(args: UniqueValidationArguments<E>) {
        return `A ${this.connection.getRepository(args.constraints[0]).metadata.tableName} with this ${args.constraints[1]} already exists`;
    }
}

@msyahidin
Copy link

I am choosing Interceptor approach, injected params.id to dto.id.
Thank you.

@nadyrbek97
Copy link

@zarv1k Hey, greate job !!! Is it possible to configure useContainer() globally ? for running tests in nestjs ?

@dancornilov
Copy link

Hey @zarv1k , thank you for your suggestion, did you update this part of code to Typeorm version 3?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment