-
-
Save zarv1k/3ce359af1a3b2a7f1d99b4f66a17f1bc to your computer and use it in GitHub Desktop.
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); | |
} | |
} |
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.
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
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 hasimport { 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).
Thanks a lot for quick response, it works 👍
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;
}
@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.
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
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;
}
@duongle26
... Btw, is there any way to return
409 Conflict
error code for Unique and404 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.
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 )]
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?
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?
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?
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`;
}
}
@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!
@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 withid
on route param oruser.id
ofuser
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?
@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 withid
on route param oruser.id
ofuser
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 ^^
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
.
@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 withid
on route param oruser.id
ofuser
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,
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`;
}
}
I am choosing Interceptor approach, injected params.id to dto.id.
Thank you.
@zarv1k Hey, greate job !!! Is it possible to configure useContainer() globally ? for running tests in nestjs ?
Hey @zarv1k , thank you for your suggestion, did you update this part of code to Typeorm version 3?
Tks you so much <3.
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