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
CodePudding user response:
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.
CodePudding user response:
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;
}
