Transform the #Body without requiring it in NestJs - nestjs

What I basically want to do is to parse the date string from the request to a Date object like in this question.
However, this is not my use case because in my case the date is not required. So if I use the solution from the question above it responds with a 400: due must be a Date instance.
This is my DTO:
export class CreateTaskDto {
#IsDefined()
#IsString()
readonly name: string;
#IsDefined()
#IsBoolean()
readonly done: boolean;
#Type(() => Date)
#IsDate()
readonly due: Date;
}
Then in my controller:
#Post('tasks')
async create(
#Body(new ValidationPipe({transform: true}))
createTaskDto: CreateTaskDto
): Promise<TaskResponse> {
const task = await this.taskService.create(createTaskDto);
return this.taskService.fromDb(task);
}
Post request with this payload is working fine:
{
"name":"task 1",
"done":false,
"due": "2021-07-13T17:30:11.517Z"
}
This request however fails:
{
"name":"task 2",
"done":false
}
{
"statusCode":400
"message":["due must be a Date instance"],
"error":"Bad Request"
}
Is it somehow possible to tell nestjs to ignore transformation if there is no date?

#IsOptional()
Checks if given value is empty (=== null, === undefined) and if so, ignores all the validators on the property.
https://github.com/typestack/class-validator#validation-decorators
#Type(() => Date)
#IsDate()
#IsOptional()
readonly due?: Date;

Related

DTO Optional during the PATCH call

I have a DTO to validate data coming from the POST request to create my Entity.
I want to have the "same" DTO to update my Entity during PATCH request, but some fields need to be Optional.
I tried to use Partial, but in that Way I miss some important control.
I can't use #ValidateIf because I don't have a property to detect if is it a POST or PATCH.
I can't use Extends, because I need to override every properties with #IsOptional
Is it possible to use "createDTO" to and update the entity, without duplicate the it and rename to "updateDTO" only for add #IsOptional property?
I think you can accomplish this in two ways:
Implement a custom interceptor/decorator that inject the http method in the body with a special key, then use ValidateIf with that injected property, like.
#Injectable()
export class HttpMethodInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<any> {
const req = context.switchToHttp().getRequest();
req.body.httpMethod = req.method;
return next.handle();
}
}
export class TestDto {
#IsNotEmpty()
#IsString()
public property: string;
#IsNotEmpty()
#IsString()
#ValidateIf(o => o.httpMethod === 'POST')
public otherProperty: string;
#IsString()
public httpMethod: string;
}
Use class validators groups passing the method as a value for each function, and pass the expected value on the decorators, something like:
#Post()
#UsePipes(new ValidationPipe({ groups: ['post'] }))
public create(#Body() body: TestDto): Promise<any> {
return;
}
#Patch()
#UsePipes(new ValidationPipe({ groups: ['patch'] }))
public update(#Body() body: TestDto): Promise<any> {
return;
}
export class TestDto {
#IsNumberString(null, { groups: ['post'] }) //This validation will only apply if group has post in it, so on patch it will do nothing
public test: string;
}

Two validators for one single entity in DTO

Are there any ways or would it be possible to have two validator in one single entity? Like for the given example code below, the identifier would accept an email as its payload but it would also accept
number/mobile number as its payload as well.
#ApiProperty()
#IsString()
#IsNotEmpty()
#IsEmail()
identifier: string;
EDIT:
I have tried,
#ApiProperty()
#IsString()
#IsNotEmpty()
#IsEmail()
#IsPhoneNumber('US')
identifier: string;
But it does not work.
EDIT 2:
I found a reference code based on this previous thread, How to use else condition in validationif decorator nestjs class-validator?, and I copied his validation class.
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from "class-validator";
import { IdentifierType } from "../interface/access.interface";
#ValidatorConstraint({ name: 'IdentifierValidation', async: false })
export class IdentifierValidation implements ValidatorConstraintInterface {
validate(identifier: string, args: ValidationArguments) {
if (JSON.parse(JSON.stringify(args.object)).type === IdentifierType.MOBILE) {
var regexp = new RegExp('/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/im');
// "regexp" variable now validate phone number.
return regexp.test(identifier);
} else {
regexp = new RegExp("^[a-zA-Z0-9_.+-]+#[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$");
// "regexp" variable now validate email address.
return regexp.test(identifier);
}
}
defaultMessage(args: ValidationArguments) {
if (JSON.parse(JSON.stringify(args.object)).type === IdentifierType.MOBILE) {
return 'Enter a valid phone number.'
} else {
return 'Enter a valid email address.'
}
}
}
DTO -
export class VerifyOtpDto {
#Validate(IdentifierValidation)
#ApiProperty()
#IsNotEmpty()
identifier: string;
#ApiProperty({ enum: IdentifierType })
#IsNotEmpty()
identifierType: IdentifierType;
}
ENUM -
export enum IdentifierType {
EMAIL = 'email',
MOBILE = 'mobile',
}
It does work with email but trying to feed a mobile number still does not work.
You have two ways to do this, first with regex:
#Matches(/YOUR_REGEX/, {message: 'identifier should be email or phone'})
identifier: string;
Or you can get the idea from this:
#IsType(Array<(val: any) => boolean>)
#IsType([
val => typeof val == 'string',
val => typeof val == 'boolean',
])
private readonly foo: boolean | string;
Of course it can get more than one validator in one DTO column.
Did you check https://www.npmjs.com/package/class-validator here?
if you want to check mobile number, you can use to #IsMobilePhone(locale: string).

How to remove Field Name in custom message in class-validator NestJS

I have problems with regards to creating custom messages using the class-validator. It always returns the field name.
export class EditOrderAddressDto implements Partial<CreateOrderAddressDto> {
#IsOptional()
#IsString()
#MinLength(1, { message: 'Last Name should at least have more than 1 character.'})
lastName?: string;
}
export class EditOrderDto implements EditOrderDtoModel {
#ApiProperty({ required: false })
#IsOptional()
#ValidateNested()
#Type(() => EditOrderAddressDto)
billingAddress?: Partial<CreateOrderAddressDto>;
}
Then for my payload I just pass this.
{ deliveryAddress:
{ lastName: "" }
}
And I get this as the error message "deliveryAddress.Last Name should at least have more than 1 character.'
Is there any way I can remove the deliveryAddress and return only the custom message?

TypeORM getRawOne<T> not returning type T

I'm working on refactoring a koa api to nest and am kinda stuck on refactoring the queries from native psql to typeorm. I have the following table, view and dto.
#Entity()
export class Challenge {
#PrimaryGeneratedColumn()
id!: number;
#Column()
endDate!: Date;
#CreateDateColumn()
createdAt!: Date;
}
#ViewEntity({
expression: (connection: Connection) => connection.createQueryBuilder()
.select('SUM(cp.points)', 'score')
.addSelect('cp.challenge', 'challengeId')
.addSelect('cp.user', 'userId')
.addSelect('RANK() OVER (PARTITION BY cp."challengeId" ORDER BY SUM(cp.points) DESC) AS rank')
.from(ChallengePoint, 'cp')
.groupBy('cp.challenge')
.addGroupBy('cp.user')
})
export class ChallengeRank {
#ViewColumn()
score!: number;
#ViewColumn()
rank!: number;
#ViewColumn()
challenge!: Challenge;
#ViewColumn()
user!: User;
}
export class ChallengeResultReponseDto {
#ApiProperty()
id!: number;
#ApiProperty()
endDate!: Date;
#ApiProperty()
createdAt!: Date;
#ApiProperty()
score: number;
#ApiProperty()
rank: number;
test() {
console.log("test")
}
}
As the object I want to return is not of any entity type, I'm kinda lost on how to select it and return the correct class. I tried the following:
this.challengeRepository.createQueryBuilder('c')
.select('c.id', 'id')
.addSelect('c.endDate', 'endDate')
.addSelect('c.createdAt', 'createdAt')
.addSelect('cr.score', 'score')
.addSelect('cr.rank', 'rank')
.leftJoin(ChallengeRank, 'cr', 'c.id = cr."challengeId" AND cr."userId" = :userId', { userId })
.where('c.id = :id', { id })
.getRawOne<ChallengeResultReponseDto>();
Which returns an object that has the correct fields, but that is not of the class type "ChallengeResultReponseDto". If I try to call the function "test" the application crashes. Further it feels weird to use the challengeRepository but not return a challenge, should I use the connection or entity manager for this instead?
I'm rather certain that getRawOne<T>() returns a JSON that looks like whatever you give the generic (T), but an not instance of that class. You should try using getOne() instead to get the instance of the returned entity

NestJS: How to transform an array in a #Query object

I'm new to NestJS and I am trying to fill a filter DTO from query Parameters.
Here is what I have:
Query:
localhost:3000/api/checklists?stations=114630,114666,114667,114668
Controller
#Get()
public async getChecklists(#Query(ValidationPipe) filter: ChecklistFilter): Promise<ChecklistDto[]> {
// ...
}
DTO
export class ChecklistFilter {
#IsOptional()
#IsArray()
#IsString({ each: true })
#Type(() => String)
#Transform((value: string) => value.split(','))
stations?: string[];
// ...
}
With this, the class validator does not complain, however, in the filter object stations is not actually an array but still a single string.
I want to transform it into an array within the validation pipe. How can I achieve that?
You can pass an instance of the ValidationPipe instead of the class, and in doing so you can pass in options such as transform: true which will make class-validatorand class-transformer run, which should pass back the transformed value.
#Get()
public async getChecklists(#Query(new ValidationPipe({ transform: true })) filter: ChecklistFilter): Promise<ChecklistDto[]> {
// ...
}
export class ChecklistFilter {
#IsOptional()
#IsArray()
#IsString({ each: true })
#Type(() => String)
#Transform(({ value }) => value.split(','))
stations?: string[];
// ...
}
--
#Get()
public async getChecklists(#Query() filter: ChecklistFilter): Promise<ChecklistDto[]> {
// ...
}
"class-transformer": "^0.4.0"
"class-validator": "^0.13.1"
This can be handled without a separate DTO class using the ParseArrayPipe:
#Get()
findByIds(
#Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
ids: number[],
) {
console.log(ids);
console.log(Array.isArray(ids)); //returns true
return 'This action returns users by ids';
}
ref: https://docs.nestjs.com/techniques/validation#parsing-and-validating-arrays
You can change your initial query a bit:
localhost:3000/api/checklists?stations[]=114630&stations[]=114666&stations[]=114667&stations[]=114668
And your controller:
#Get()
public async getChecklists(#Query('stations') filter: string[]): Promise<ChecklistDto[]> {
// ...
}
This way the default mechanism will work fine and will convert query params into string array and no any additional dependencies or handling required.
You also can wrap it with your DTO if needed, but you get the idea.
the only problem you had there is in the order of validations. You can do it like this:
export class ChecklistFilter {
#IsOptional()
#Transform((params) => params.value.split(',').map(Number))
#IsInt({ each: true })
stations?: number[]
// ...
}
If you want numbers instead of ints: #IsNumber({}, { each: true })
Had a similar issue, what works for me is apply a custom transform:
export class ChecklistFilter {
#ApiProperty({ type: [Number] })
#IsOptional()
#IsArray()
#Transform((item) => item.value.map((v) => parseInt(v, 10)))
stations?: number[];
//...
}

Resources