Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 509
Hassan Althaf Avatar asked Jul 11 '26 11:07

Hassan Althaf


1 Answers

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;
}
like image 133
Hassan Althaf Avatar answered Jul 14 '26 01:07

Hassan Althaf