I’m looking for some help about custom validator & custom decorator in Nest.
FIRST CASE : working one
A DTO, with class-validator anotations :
import { IsNotEmpty, IsString } from 'class-validator';
import { IsOwnerExisting } from '../decorators/is-owner-existing.decorator';
export class CreatePollDto {
@IsNotEmpty()
@IsString()
@IsOwnerExisting() // custom decorator, calling custom validator, using a service to check in db
ownerEmail: string;
@IsNotEmpty()
@IsString()
@NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
}
I use it in a controller :
@Controller()
@ApiTags('/polls')
export class PollsController {
constructor(private readonly pollsService: PollsService) {}
@Post()
public async create(@Body() createPollDto: CreatePollDto): Promise<Poll> {
return await this.pollsService.create(createPollDto);
}
}
When this endpoint is called, the dto is validating by class-validator, and my custom validator works. If the email doesn’t fit any user in database, a default message is displayed. That is how I understand it.
SECOND CASE : how to make it work ?
Now, I want to do something similar but in a nested route, with an ApiParam. I’d like to check with a custom validator if the param matches some object in database.
In that case, I can’t use a decorator in the dto, because the dto doesn’t handle the "slug" property, it’s a ManyToOne, and the property is on the other side.
// ENTITIES
export class Choice {
@ManyToOne((type) => Poll)
poll: Poll;
}
export class Poll {
@Column({ unique: true })
slug: string;
@OneToMany((type) => Choice, (choice) => choice.poll, { cascade: true, eager: true })
@JoinColumn()
choices?: Choice[];
}
// DTOs
export class CreateChoiceDto {
@IsNotEmpty()
@IsString()
label: string;
@IsOptional()
@IsString()
imageUrl?: string;
}
export class CreatePollDto {
@IsNotEmpty()
@IsString()
@NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
slug: string;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateChoiceDto)
choices: CreateChoiceDto[] = [];
}
So where should I hook my validation ?
I’d like to use some decorator directly in the controller. Maybe it’s not the good place, I don’t know. I could do it in the service too.
@Controller()
@ApiTags('/polls/{slug}/choices')
export class ChoicesController {
constructor(private readonly choicesService: ChoicesService) {}
@Post()
@ApiParam({ name: 'slug', type: String })
async create(@Param('slug') slug: string, @Body() createChoiceDto: CreateChoiceDto): Promise<Choice> {
return await this.choicesService.create(slug, createChoiceDto);
}
}
As in my first case, I’d like to use something like following, but in the create method of the controller.
@ValidatorConstraint({ async: true })
export class IsSlugMatchingAnyExistingPollConstraint implements ValidatorConstraintInterface {
constructor(@Inject(forwardRef(() => PollsService)) private readonly pollsService: PollsService) {}
public async validate(slug: string, args: ValidationArguments): Promise<boolean> {
return (await this.pollsService.findBySlug(slug)) ? true : false;
}
public defaultMessage(args: ValidationArguments): string {
return `No poll exists with this slug : $value. Use an existing slug, or register one.`;
}
}
Do you understand what I want to do ? Is it feasible ? What is the good way ?
Thanks a lot !