How to make class-validator validate an object whose type is an intersection type - node.js

There's a method that accepts an intersection of a few DTOs:
export type RegisterUserDto = RegisterCommonUserDto
& RegisterLocationProviderDto
& RegisterFighterDto;
The method looks like that:
#Post('register')
public async registerUser(#Body() registerUserDto: RegisterUserDto): Promise<any> {
// method code
}
The controller itself is decorated with #UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
So when a DTO is a class the validation works, but now it's a type and the class-validator just fails to react to incorrect data, I mean all its decorators like #IsEmail(), #IsPhoneNumber() etc just don't work.
What should I do to make it work?

I've managed to solve this issue with the help of this library https://www.npmjs.com/package/ts-mixer implementing multiple inheritance. So now my RegisterUserDto looks like this.
export class RegisterUserDto extends Mixin(RegisterCommonUserDto, RegisterLocationProviderDto, RegisterFighterDto) {}
However I would appreciate other solutions.

Related

Cannot find module when using type from another module in class-validator

I'm using typescript on both frontend and backend, so I wanted to create a "shared types" package for them. For the backend I'm using nest.js and I recently ran into an issue with the class-validator package.
In my shared types package I created the following enum-like type (since enums itself don't seem to be working if they are being used from a node module):
export const MealTypes = {
BREAKFAST: 'Breakfast',
LUNCH: 'Lunch',
DINNER: 'Dinner',
SNACK: 'Snack'
} as const;
export type ObjectValues<T> = T[keyof T];
export type MealType = ObjectValues<typeof MealTypes>;
I've installed the module locally using npm i and I'm able to import the type in my backend like this:
import { MealType, MealTypes } from '#r3xc1/shared-types';
Since I am not able to use this constant for the IsEnum class validator, I wrote my own:
#ValidatorConstraint({ name: 'CheckEnum', async: false })
export class CheckEnumValidator implements ValidatorConstraintInterface {
validate(value: string | number, validationArguments: ValidationArguments) {
return Object.values(validationArguments.constraints[0]).includes(value);
}
defaultMessage(args: ValidationArguments) {
return `Must be of type XYZ`;
}
}
and then I'm using it in a DTO class like this:
export class CreateMealDTO {
#Validate(CheckEnumValidator, [MealTypes])
#IsNotEmpty()
meal_type: MealType;
}
But as soon as I add the #Validate(...) I get the following error on start:
Error: Cannot find module '#r3xc1/shared-types'
It only does this, if I am passing a type that has been imported from a node module into a validator. It also happens with other validators like IsEnum.
I'm not really sure why this error is happening and I appreciate any hints or help!

NestJS: wrap 3rd party decorator & setMetaData in order to use Reflector to check annotation

I am using NestJS to implement a project with TypeScript.
I am using a 3rd party library which provide a decorator called Protected, I can use the decorator to annotate my controller method:
#Protected()
myFunc(){
...
}
I have a Guard, in which I want to check whether the annotation is there inside my MyGuard class. I tried:
#Injectable()
export class MyGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): Promise<boolean> {
const value = this.reflector.get('Protected', context.getHandler());
console.log(`VALUE: ${metaValue}`);
...
}
}
The log message shows me VALUE: undefined. I read the NestJS doc, there is an example of using setMetadata() and then in Guard method use the metadata key to retrieve metadata to check if the annotation is there. However, this is a 3rd party decorator, there is no information for me whether they use any metadata key.
So, I come up with a workaround, I create my own custom decorator which wraps the 3rd party Protected decorator & use my decorator instead on controller method:
import {Protected} from '3rd-party-lib'
import { SetMetadata } from '#nestjs/common';
export const MyProtected = () => {
Protected();
SetMetadata(IS_PROTECTED, true);
}
But now, the annotation on controller method raises error:
/**
ERROR: Unable to resolve signature of method decorator when called as an expression.
This expression is not callable.
Type 'void' has no call signatures.ts(1241)
**/
#MyProtected()
myFunc(){
...
}
My questions:
Is there a way to use Reflector to check whether the 3rd party annotation is presented in the controller method inside MyGuard ?
If there is no way without using setMetadata & since I don't know what metadata key to check due to 3rd party library. How can I setMetadata myself in my custom decorator in order to achieve what I need?
Why not use Nest's applyDecorators so you can compose the decorator out of several decorators?
export const MyProtected = () => applyDecorators(
Protected(),
SetMetadata(IS_PROTECTED, true)
);

Nestjs and Class Validator - at least one field should not be empty

I have NestJS API, which has a PATCH endpoint for modifying a resource. I use the class-validator library for validating the payload. In the DTO, all fields are set to optional with the #IsOptional()decorator. Because of that, if I send an empty payload, the validation goes through and then the update operation errors.
I am wondering if there is a simple way to have all fields set to optional as I do and at the same time make sure that at least one of them is not empty, so the object is not empty.
Thanks!
I don't know if it is possible using DTO.
For that purpose I use Pipes. Like this:
Injectable()
export class ValidatePayloadExistsPipe implements PipeTransform {
transform(payload: any): any {
if (!Object.keys(payload).length) {
throw new BadRequestException('Payload should not be empty');
}
return payload;
}
}

Nestjs extend/combine decorators?

I have simple custom decorator:
export const User: () => ParameterDecorator = createParamDecorator(
(data: any, req): UserIdentity => {
const user = getUser(req);
return user;
},
);
And now, I need to validate if we have email in user object.
The problem is that I can't update my current decorator.
Could I extend my current decorator?
Create a new decorator based on the previous one or create a new decorator and combine it?
Yes, you can do "decorator composition" with Nest, but this might not be a perfect solution for your case, depending on what you intend to do when user has no email property.
As per the example from the documentation:
import { applyDecorators } from '#nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized"' }),
);
}
In this example, Auth is a decorator that can be used to combine all the one passed in applyDecorators.
Thus, I'd recommend extending your decorator using a pipe.
As stated by the documentation:
Nest treats custom param decorators in the same fashion as the built-in ones (#Body(), #Param() and #Query()). This means that pipes are executed for the custom annotated parameters as well (in our examples, the user argument). Moreover, you can apply the pipe directly to the custom decorator:
#Get()
async findOne(#User(new ValidationPipe()) user: UserEntity) {
console.log(user);
}
In this example, User is a custom parameter decorator. And ValidationPipe is passed, but you can imagine passing any pipe.

transform value if falsy

I'm validating my DTOs with the class-validator package. I enabled the transformation via
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
in my main.ts file as described in the docs
https://docs.nestjs.com/techniques/validation#transform-payload-objects
I have a optional configuration field in my DTO. This field should be transformed to an empty object if it doesn't exist. The transformation decorator is described here
https://docs.nestjs.com/techniques/serialization#transform
I was hoping to come up with this solution:
export class MyDTO {
#IsObject()
#IsOptional()
#Transform(configuration => configuration || {})
public configuration: object;
}
When I call my API route
#Post()
public create(#Body() myDTO: MyDTO): void {
console.log(myDTO);
}
with an empty body, so without the field configuration my MyDTO instance is
{}
although I would expect it to be
{
configuration: {}
}
What is wrong or what am I missing? I tried to debug the code and it never hits the transformation function. So the #Transform doesn't trigger.
Update
It seems I have to do this
#IsObject()
#IsOptional()
#Transform(configuration => configuration || {}) // will be used if the field exists
public configuration: object = {}; // will be used if the field doesn't exist
The initial value will be used if you pass in an empty body. The transformation only runs if you pass in the field but assign a value like null to it.
Gonna go ahead n put this here too: why not just let typescript manage the default value with setting the value like
export class MyDTO {
#IsObject()
#IsOptional()
public configuration: object = {};
}
That way, if you get a value, great, and if it isn't there, class-transform will put the correct value there.
Looks like there is more discussion about solutions here.

Resources