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[];
//...
}
Related
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;
}
I have a situation where my client user can enter zero or multiple addresses. My problem is that if he enters an address, some fields need to be mandatory.
user.controller.ts
#Post()
#UsePipes(ValidationPipe)
async createUser(
#Body() createUser: CreateUserDto,
) {
return await this.service.saveUserAndAddress(createUser);
}
create-user.dto.ts
export class CreateUserDto {
#IsNotEmpty({ message: 'ERROR_REQUIRED_FULL_NAME' })
fullName?: string;
#IsNotEmpty({ message: 'ERROR_REQUIRED_PASSWORD' })
password?: string;
#IsNotEmpty({ message: 'ERROR_REQUIRED_EMAIL' })
#IsEmail({}, { message: 'ERROR_INVALID_EMAIL' })
email?: string;
...
addresses?: CreateUserAddressDto[];
}
create-user-address.dto.ts
export class CreateUserAddressDto {
...
#IsNotEmpty()
street: string;
...
}
CreateUserDto data is validated correctly and generates InternalServerErrorResponse, but CreateUserAddressDto data is not validated when there is some item in my array. Any idea how I can do this validation?
Nest fw uses class-transformer to convert a json to a class object. You have to set the correct type for the sub-attribute if it is not a primitive value. And your attribute is an array, you have to config to tell class-validator that it is an array, and validate on each item.
Let's update CreateUserDto
import { Type } from 'class-transformer';
import { ..., ValidateNested } from 'class-validator';
export class CreateUserAddressDto {
...
#ValidateNested({ each: true })
#Type(() => CreateUserAddressDto)
addresses?: CreateUserAddressDto[];
...
}
What you are trying to do is - to basically add logic to primitive validators provided out of the box with nest - aka - defining a custom validator.
This can be done by using the two classes ValidatorConstraint and ValidatorConstraintInterface provided by the class validator.
In order to sort this, transform the incoming input / club whatever data you want to validate at once into an object - either using a pipe in nestjs or sent it as an object in the API call itself, then attach a validator on top of it.
To define a custom validator:
import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
/**
* declare your custom validator here
*/
#ValidatorConstraint({ name: 'MyValidator', async: false })
export class MyValidator implements ValidatorConstraintInterface {
/** return true when tests pass **/
validate(incomingObject: myIncomingDataInterface) {
try {
// your logic regarding what all is required in the object
const output = someLogic(incomingObject);
return output;
} catch (e) {
return false;
}
}
defaultMessage() {
return 'Address update needs ... xyz';
}
}
Once you have defined this, keep this safe somewhere as per your project structure. Now you just need to call it whenever you want to put this validation.
In the data transfer object,
// import the validator
import { Validate } from 'class-validator';
import { MyValidator } from './../some/safe/place'
export class SomeDto{
#ApiProperty({...})
#Validate(MyValidator)
thisBecomesIncomingObjectInFunction: string;
}
As simple as that.
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;
I have UpdateUserDto:
export class UpdateUserDto extends PartialType(CreateUserDto) {
}
CreateUserDto:
export class CreateUserDto {
#ValidateNested({ each: true })
#IsOptional()
Point: CreateUserPointDto;
}
CreateUserPointDto:
export class CreateUserPointDto{
#IsString()
name: string
#IsString()
color: string
}
Now partial type makes all properties of CreateUserDto optional, the problem is, it doesn't create all properties of Point that is inside CreateUserDto optional.
How do I go about solving this issue?
Also another unrelated problem, any validation to Point in UpdateUser only works with { PartialType } from '#nestjs/mapped-types'
If I use import { PartialType } from '#nestjs/swagger', For the same code it says Point.property name/color should not exist.
I'm sure you may have moved on from this, but here's something that may resolve the issue in case you come around in the future.
You need to use #Type from class-transformers to ensure you get the types for the nested Point attribute.
Sample code
import { Type } from 'class-transformer';
export class CreateUserDto {
#ValidateNested({ each: true })
#IsOptional()
#Type(() => CreateUserPointDto) // -> this line
Point: CreateUserPointDto;
}
I want to transform my query param from string to number. I use dto technic.
import { IsOptional, IsInt, Min } from 'class-validator';
import { Transform } from 'class-transformer';
export class PaginationDto {
#IsOptional()
#IsInt()
#Transform(val => Number.parseInt(val))
#Min(1)
perPage: number;
Use dto in controller
#Get('/company')
public async getCompanyNews(
#Query() query: PaginationDto
) {
console.log(typeof query.page);
Result: string.
How do I change the type correctly?
To ensure that DTOs get transformed, the transform: true option must be set for the ValidationPipe. Without that, the original incoming object will be passed after going through validations.