-
-
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); | |
} | |
} |
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.
I mean what can I do to improve the
args
partThen I made it work with this