Skip to content

Instantly share code, notes, and snippets.

@zarv1k
Last active April 12, 2024 07:14
Show Gist options
  • 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);
}
}
@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?

@HORKimhab
Copy link

How to validate unique and ignore for current id like laravel.
Thank you.
IngnoreUnique

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