Class-Validator (Node.js) Get another property value within custom validation
Asked Answered
A

3

7

At the moment, I have a very simple class-validator file with a ValidationPipe in Nest.js as follows:

import {
  IsDateString,
  IsEmail,
  IsOptional,
  IsString,
  Length,
  Max,
} from 'class-validator';

export class UpdateUserDto {
  @IsString()
  id: string;

  @Length(2, 50)
  @IsString()
  firstName: string;

  @IsOptional()
  @Length(2, 50)
  @IsString()
  middleName?: string;

  @Length(2, 50)
  @IsString()
  lastName: string;

  @IsEmail()
  @Max(255)
  email: string;

  @Length(8, 50)
  password: string;

  @IsDateString()
  dateOfBirth: string | Date;
}

Lets say in the above "UpdateUserDto," the user passes an "email" field. I want to build a custom validation rule through class-validator such that:

  • Check if email address is already taken by a user from the DB
  • If the email address is already in use, check if the current user (using the value of 'id' property) is using it, if so, validation passes, otherwise, if it is already in use by another user, the validation fails.

While checking if the email address is already in use is a pretty simple task, how would you be able to pass the values of other properties within the DTO to a custom decorator @IsEmailUsed

Alsatian answered 29/1, 2022 at 17:23 Comment(0)
A
20

It was pretty simple to solve, I solved it by creating a custom class-validation Decorator as below:

import { PrismaService } from '../../prisma/prisma.service';
import {
  registerDecorator,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
} from 'class-validator';
import { Injectable } from '@nestjs/common';

@ValidatorConstraint({ name: 'Unique', async: true })
@Injectable()
export class UniqueConstraint implements ValidatorConstraintInterface {
  constructor(private readonly prisma: PrismaService) {}

  async validate(value: any, args: ValidationArguments): Promise<boolean> {
    const [model, property = 'id', exceptField = null] = args.constraints;

    if (!value || !model) return false;

    const record = await this.prisma[model].findUnique({
      where: {
        [property]: value,
      },
    });

    if (record === null) return true;

    if (!exceptField) return false;

    const exceptFieldValue = (args.object as any)[exceptField];
    if (!exceptFieldValue) return false;

    return record[exceptField] === exceptFieldValue;
  }

  defaultMessage(args: ValidationArguments) {
    return `${args.property} entered is not valid`;
  }
}

export function Unique(
  model: string,
  uniqueField: string,
  exceptField: string = null,
  validationOptions?: ValidationOptions,
) {
  return function (object: any, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      constraints: [model, uniqueField, exceptField],
      validator: UniqueConstraint,
    });
  };
}

However, to allow DI to that particular Decorator, you need to also add this to your main.ts bootstrap function:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  ...
  // Line below needs to be added.
  useContainer(app.select(AppModule), { fallbackOnErrors: true });
  ...
}

Also, make sure to import the "Constraint" in the app module:

@Module({
  imports: ...,
  controllers: [AppController],
  providers: [
    AppService,
    PrismaService,
    ...,
    // Line below added
    UniqueConstraint,
  ],
})
export class AppModule {}

Finally, add it to your DTO as such:

export class UpdateUserDto {
  @IsString()
  id: string;

  @IsEmail()
  @Unique('user', 'email', 'id') // Adding this will check in the user table for a user with email entered, if it is already taken, it will check if it is taken by the same current user, and if so, no issues with validation, otherwise, validation fails.
  email: string;
}
Alsatian answered 30/1, 2022 at 10:5 Comment(0)
H
1

Luckily for us, the class-validator provides a very handy useContainer function, which allows setting the container to be used by the class-validor library. So add this code in your main.ts file (app variable is your Nest application instance):

useContainer(app.select(AppModule), { fallbackOnErrors: true });

It allows the class-validator to use the NestJS dependency injection container.

@ValidatorConstraint({ name: 'emailId', async: true })
@Injectable()
export class CustomEmailvalidation implements ValidatorConstraintInterface {
  constructor(private readonly prisma: PrismaService) {}

  async validate(value: string, args: ValidationArguments): Promise<boolean> {
    return this.prisma.user
      .findMany({ where: { email: value } })
      .then((user) => {
        if (user) return false;
        return true;
      });
  }
  defaultMessage(args: ValidationArguments) {
    return `Email already exist`;
  }
}

Don't forget to declare your injectable classes as providers in the appropriate module. Now you can use your custom validation constraint. Simply decorate the class property with @Validate(CustomEmailValidation) decorator:

export class CreateUserDto {
  @Validate(customEmailValidation)
  email: string;

  name: string;
  mobile: number;
}

If the email already exists in the database, you should get an error with the default message "Email already exists". Although using @Validate() is fine enough, you can write your own decorator, which will be much more convenient. Having written Validator Constraint is quick and easy. We need to just write decorator factory with registerDecorator() function.

export function Unique(validationOptions?: ValidationOptions) {
  return function (object: any, propertyName: string) {
    registerDecorator({
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      validator: CustomEmailvalidation,
    });
  };
}

As you can see, you can either write new validator logic or use written before validator constraint (in our case - Unique class). Now we can go back to our User class and use the @Unique validator instead of the @Validate(CustomEmailValidation) decorator.

export class CreateUserDto {
  @Unique()
  email: string;

  name: string;
  mobile: number;
}
Hulbig answered 27/7, 2022 at 5:18 Comment(1)
Pretty much the same thing I did, good stuff.Alsatian
E
0

I think your first use case (Check if email address is already taken by a user from the DB), can be solved by using custom-validator

For the second one there is no option to get the current user before the validation. Suppose you are getting the current user using the @CurrentUser decorator. Then once the normal dto validation is done, you need to check inside the controller or service if the current user is accessing your resource.

Everyplace answered 29/1, 2022 at 23:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.