0

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 !

4 Answers 4

1

If you're needing to validate the slug with your custom rules you have one of two options

  1. make a custom pipe that doesn't use class-validator and does the validation directly in it.

  2. Use @Param() { slug }: CreatePollDto. This assumes that everything will be sent via URL parameters. You could always make the DTO a simple one such as

export class SlugDto {
  @IsNotEmpty()
  @IsString()
  @NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
  slug: string;
}

And then use @Param() { slug }: SlugDto, and now Nest will do the validation via the ValidationPipe for you.

Sign up to request clarification or add additional context in comments.

Comments

0

If it didn't work with you with service try to use

getConnection().createQueryBuilder().select().from().where() I used it in custom decorator to make a isUnique and it works well, but niot with injectable service.

public async validate(slug: string, args: ValidationArguments): Promise<boolean> { return (await getConnection().createQueryBuilder().select(PollsEntityAlias).from(PollsEntity).where('PollsEntity.slug =:slug',{slug}))) ? true : false; }

2 Comments

I don't think my problem is that. It's not that the validator isn't working, that it isn't validating. I wonder about my understanding of how to use a validator somewhere other than a dto : in a controller or maybe in a service.
I’ve added explanations in my question above. However, I’ve figured out there was a bad copy-paste in the slug validator (it was injecting UsersService instead of PollsService). But that doesn’t change my questioning.
0

That’s so greeat! Thanks a lot, it’s working.

I’ve tried something like that, but can’t find the good way.

The deconstructed { slug }: SlugDto, so tricky & clever ! I’ve tried slug : SlugDto, but it couldn’t work, I was like «..hmmm… how to do that… »

Just something else : in the controller method, I was using (as in documentation) @Param('slug'), but with the slugDto, it can’t work. Instead, it must be just @Param().

Finally, my method :

  @Post()
  @ApiParam({ name: 'slug', type: String })
  public async create(@Param() { slug }: SlugDto, @Body() createChoiceDto: CreateChoiceDto): Promise<Choice> {
    return await this.choicesService.create(slug, createChoiceDto);
  }

And the dto :

export class SlugDto {
  @IsNotEmpty()
  @IsString()
  @NotContains(' ', { message: 'Slug should NOT contain any whitespace.' })
  @IsSlugMatchingAnyExistingPoll()
  slug: string;
}

Comments

0

Personally, I wouldn't register this as a class-validator decorator, because these are beyond the scopes of Nestjs's dependency injection. Getting a grasp of a service/database connection in order to check the existence of a poll would be troublesome and messy from a validator constraint. Instead, I would suggest implementing this as a pipe. If you want to only check if the poll exists, you could do something like:

@Injectable()
export class VerifyPollBySlugPipe implements PipeTransform {
    constructor(@InjectRepository(Poll) private repository: Repository<Poll>) {}

    transform(slug: string, metadata: ArgumentsMetadata): Promise<string> {
        let found: Poll = await repository.findOne({
          where: { slug }
        });
        if (!found) throw new NotFoundException(`No poll with slug ${slug} was found`);
        return slug;
    }
}

But, since you're already fetching the poll entry from the database, maybe you can give it a use in the service, so the same pipe can work to retrieve the entity and throw if not found. I answered a similar question here, you'd just need to add the throwing of the 404 to match your case. Hope it helps, good luck!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.