It sounds like a quite simple question but I've been searching for a solution for a very long time now. I want to validate an array of UUIDs in an endpoint.
Like this:
["9322c384-fd8e-4a13-80cd-1cbd1ef95ba8", "986dcaf4-c1ea-4218-b6b4-e4fd95a3c28e"]
I have already successfully implemented it as a JSON object { "id": ["9322c384-fd8e-4a13-80cd-1cbd1ef95ba8", "986dcaf4-c1ea-4218-b6b4-e4fd95a3c28e"]} with the following code:
public getIds(
#Body(ValidationPipe)
uuids: uuidDto
) {
console.log(uuids);
}
import { ApiProperty } from '#nestjs/swagger';
import { IsUUID } from 'class-validator';
export class uuidDto {
#IsUUID('4', { each: true })
#ApiProperty({
type: [String],
example: [
'9322c384-fd8e-4a13-80cd-1cbd1ef95ba8',
'986dcaf4-c1ea-4218-b6b4-e4fd95a3c28e',
],
})
id!: string;
}
But unfortunately I can't customize the function that calls that endpoint. So I need a solution to only validate a array of uuids.
instead of type string , write string[]. like below:
import { ApiProperty } from '#nestjs/swagger';
import { IsUUID } from 'class-validator';
export class uuidDto {
#IsUUID('4', { each: true })
#ApiProperty({
type: string[],
example: [
'9322c384-fd8e-4a13-80cd-1cbd1ef95ba8',
'986dcaf4-c1ea-4218-b6b4-e4fd95a3c28e',
],
})
id!: string[];
}
You can build a custom validation pipe for it:
#Injectable()
export class CustomClassValidatorArrayPipe implements PipeTransform {
constructor(private classValidatorFunction: (any)) {}
transform(value: any[], metadata: ArgumentMetadata) {
const errors = value.reduce((result, value, index) => {
if (!this.classValidatorFunction(value))
result.push(`${value} at index ${index} failed validation`)
return result
}, [])
if (errors.length > 0) {
throw new BadRequestException({
status: HttpStatus.BAD_REQUEST,
message: 'Validation failed',
errors
});
}
return value;
}
}
In your controller:
#Post()
createExample(#Body(new CustomClassValidatorArrayPipe(isUUID)) body: string[]) {
...
}
Ensure to use the lowercase functions from class-validator. It has to be isUUID instead of IsUUID. (This is used for the manual validation with class-validator.)
CustomClassValidatorArrayPipe is build modular. You can validate any other type with it. For example a MongoId: #Body(new CustomClassValidatorArrayPipe(isMongoId)) body: ObjectId[]
Result
If you send this:
POST http://localhost:3000/example
Content-Type: application/json
[
"986dcaf4-c1ea-4218-b6b4-e4fd95a3c28e",
"123",
"test"
]
Server will reply:
{
"status": 400,
"message": "Validation failed",
"errors": [
"123 at index 1 failed validation",
"test at index 2 failed validation"
]
}
Related
I have a project in Node JS with Typescript in which I am creating an API to get data from a local JSON file depending on the given variable.
This is my model.ts:
interface ListProductBatchModel {
resp: ResultMsg
}
interface ResultMsg {
name?: string,
price?: string,
error?: string
}
export { ListProductBatchModel, ResultMsg };
This is my properties JSON file:
{
"CT": {
"name": "box",
"price": "5,00"
},
"CC": {
"name": "car",
"price": "6,00"
}
}
This is my controller.ts:
import * as logger from 'winston';
import { Controller, Get, Response, Route, SuccessResponse, Tags } from 'tsoa';
import { ListProductBatchModel } from './models/listProduct.models';
import { ListProductBatchUtils } from './utils/listProductBatch.utils';
#Route('/list/product')
#Tags('list-product')
export class ListProductBatchController {
private listProductBatchUtils: ListProductBatchUtils;
constructor() {
this.listProductBatchUtils = new ListProductBatchUtils();
}
#Get('/{codProduct}')
#SuccessResponse(200, 'Success Response')
async listProductBatch(codProduct: string): Promise<ListProductBatchModel> {
try {
const listProductBatch = await this.listProductBatchUtils.getDataProduct(codProduct);
return Promise.resolve(listProductBatch as ListProductBatchModel);
} catch (error) {
logger.info(JSON.stringify(error));
return Promise.reject(error);
}
}
}
This is my utils.ts:
import * as logger from 'winston';
import * as getProperty from '../json/product.json';
import { ListProductBatchModel, ResultMsg } from '../models/listProduct.models';
export class ListProductBatchUtils {
public async getDataProduct(codProduct: string): Promise<ListProductBatchModel> {
try {
let result: ResultMsg;
if (getProperty[codProduct.toUpperCase()]) {
result = {
name: getProperty[codProduct.toUpperCase()].name,
price: getProperty[codProduct.toUpperCase()].price
}
}else {
result = {
error: "ERROR"
}
}
logger.info('start')
return Promise.resolve({ resp: result });
} catch (error) {
logger.info(JSON.stringify(error));
return Promise.reject(error);
}
}
}
This is the error I get in getProperty [codProduct.toUpperCase ()]:
The element has a type "any" implicitly because the expression of type "string" cannot be used to index the type "{CT: {name: string; price: string;}; CC: {name: string; price : string;};} ".
No index signature was found with a parameter of type "string" in type "{CT: {name: string; price: string;}; CC: {name: string; price: string;};}".
My problem: I don't understand how the error is generated, what I want is to take the name and price properties that match the codProduct variable. Why can this happen? What am I doing wrong and how can I solve it?
Right now, codProduct is a string. When you're accessing getProduct via its subscript [], TypeScript expects you to use an index of getProduct (which, in this case is either "CT" or "CC").
You can satisfy the TypeScript compiler by casting your string as a keyof getProperty's type. Note that this will work at compile time, but will not guarantee that it is in fact a key of getProperty at runtime. But, since you're doing boolean checks already, that seems like it will be okay in your case.
Here's a simplified example:
const getProperty = {
"CT": {
"name": "box",
"price": "5,00"
},
"CC": {
"name": "car",
"price": "6,00"
}
};
type GetPropertyType = typeof getProperty;
function myFunc(input: string) {
const result = getProperty[input.toUpperCase() as keyof GetPropertyType].name;
}
In NestJs I can not figure out how can get the validaiton error message from ValidationPipe. Always send generic response that includes no "message" parameter like this. :
{
"statusCode": 400,
"timestamp": "2021-05-22T09:59:27.708Z",
"path": "/batchs"
}
In main.ts I put the pipeline defination
app.useGlobalPipes(new ValidationPipe());
And this my dto
export class BatchCreateDto {
#ApiProperty()
#IsNotEmpty({ message: 'username is required' })
code: string;
#ApiProperty({ type: Lang })
title: Lang;
#ApiProperty({ type: Lang })
description: Lang;
#ApiProperty()
startingDate: Date;
#ApiProperty()
duraitonInHours: number;
}
If I send data with emty "code" field to api, the api returns generic Bad Request response that not includes message parameter. If the "code" field contains any data, there is no problem, api insert object to database. So I understand that validation pipe works, but why does not return any detailed message if validation fails.
I want to get the message parameter that includes something like "username is required" or any generic message. Thanks for help.
UPDATE
Body of the request that causes error
{
"code": "",
"title": {
"en": "this is test",
"tr": "bu testtir"
},
"description": {
"en": "some explination...",
"tr": "bazı açıklamalar..."
},
"startingDate": "2021-05-24T07:51:03.197Z",
"duraitonInHours": 14
}
BatchController uses BatchCreateDto
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UsePipes, ValidationPipe } from '#nestjs/common';
import { BatchService } from '../services/batch.service';
import { ApiBody, ApiTags } from '#nestjs/swagger';
import { BatchCreateDto } from 'src/entities/dto/batch.create.dto';
#ApiTags('Batchs')
#Controller('batchs')
export class BatchController {
constructor(private batchService: BatchService) { }
#Post()
public async addBatch(#Body() batch: BatchCreateDto) {
const result = await this.batchService.add(batch);
return result;
}
}
I need to change the error response 404 error for exemple.
This is de case:
#Entity('role')
export class RoleEntity extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column({ type: 'varchar', length: 100, nullable: false })
name: string;
}
#Injectable()
export class RoleService extends TypeOrmCrudService<RoleEntity> {
constructor(#InjectRepository(RoleEntity) repo) {
super(repo);
}
}
#Crud({
model: {
type: RoleEntity,
},
})
#Controller('role')
export class RoleController implements CrudController<RoleEntity> {
constructor(public service: RoleService) {}
}
When i try to get an id not created in the base i got the error
{
"statusCode": 404,
"message": "RoleEntitynot found",
"error": "Not Found"
}
I want to change this error to something like this:
{
"success": false,
"message": "The given id was not found in the database",
}
What is the best way to do this?
Override nestjs/crud getOneBase method (Doc nestjs/crud)
#Override('getOneBase')
getOneAndDoStuff(
#ParsedRequest() req: CrudRequest,
) {
// Catch below error and return your message
this.base.getOneBase(req)
return ...;
}
You can wrap the base call in try catch statement like below, by this you can override errors object.
#Override()
async getOne(#ParsedRequest() req: CrudRequest) {
try {
return await this.base.getOneBase(req);
} catch (error) {
throw new HttpException(
{
returnCode: error.response.statusCode,
message: error.response.message,
type: ResponseTypes[error.response.statusCode],
},
error.status
);
}
}
I'm trying to validate the parameters that come in the query of a get request, but for some reason, the validation pipe is unable to identify the elements of the query.
import {
Controller,
Post,
Query,
Body,
UseInterceptors,
Param,
Res,
Logger,
} from '#nestjs/common';
import { Crud, CrudController, Override } from '#nestjsx/crud';
import { OpenScheduleDto } from './open-schedule.dto';
#Crud(Schedule)
export class ScheduleController
implements CrudController<ScheduleService, Schedule> {
constructor(public service: ScheduleService) {}
get base(): CrudController<ScheduleService, Schedule> {
return this;
}
#Override()
async getMany(#Query() query: OpenScheduleDto) {
return query;
}
}
OpenSchedule.dto
import { IsNumber, IsOptional, IsString } from 'class-validator';
export class OpenScheduleDto {
#IsNumber()
companyId: number;
#IsNumber()
#IsOptional()
professionalId: number;
#IsString()
#IsOptional()
scheduleDate: string;
}
When I make a get request to http://localhost:3000/schedules?companyId=3&professionalId=1
I get unexpected errors:
{
"statusCode": 400,
"error": "Bad Request",
"message": [
{
"target": {
"companyId": "3",
"professionalId": "1"
},
"value": "3",
"property": "companyId",
"children": [],
"constraints": {
"isNumber": "companyId must be a number"
}
},
{
"target": {
"companyId": "3",
"professionalId": "1"
},
"value": "1",
"property": "professionalId",
"children": [],
"constraints": {
"isNumber": "professionalId must be a number"
}
}
]
}
That is because when you use #Query parameters, everything is a string. It does not have number or boolean as data types like json. So you have to transform your value to a number first. For that, you can use class-transformer's #Transform:
import { IsNumber, IsOptional, IsString } from 'class-validator';
import { Transform } from 'class-transformer';
export class OpenScheduleDto {
#Transform(id => parseInt(id))
#IsNumber()
companyId: number;
#Transform(id => id ? parseInt(id) : id)
#IsNumber()
#IsOptional()
professionalId?: number;
#IsString()
#IsOptional()
scheduleDate?: string;
}
Note though, that this is unsafe because e.g. parseInt('5abc010') is 5. So you might want to do additional checks in your transformation function.
Using Nestjs OpenAPI module, I can specify the response type using a decorator like so:
#Controller('app')
class MyController {
#Get()
#ApiResponse({ status: 200, description: 'Success', type: MyDto })
getThing() {
// code here
}
}
However, the documentation does not specify how to have a polymorphic response type (or in OpenAPI terms a oneOf response type). E.g., in the example below I would want to have my response type be oneOf: [MyDto, MyOtherDto]:
#Controller('app')
class MyController {
#Get()
#ApiResponse({ status: 200, description: 'Success', type: MyDto }) // What do do for type here?
getThing(): MyDto | MyOtherDto {
// code here
}
}
How can I do this?
Okay, so not sure if this is the best/easiest way, but a "raw schema definition" can be supplied for the #ApiResponse() decorator, like so:
#Controller('app')
class MyController {
#Get()
#ApiResponse({
status: 200,
description: 'Success',
schema: {
oneOf: [
{ $ref: getSchemaPath(MyDto) },
{ $ref: getSchemaPath(MyOtherDto) },
]
}
})
getThing(): MyDto | MyOtherDto {
// code here
}
}
In case your polymorphic class has a discriminator - you can take it one step further with the following code:
enum DtoType {
MY_DTO = 'MY_DTO',
MY_OTHER_DTO = 'MY_OTHER_DTO',
}
#Controller('app')
class MyController {
#Get()
#ApiResponse({
status: 200,
description: 'Success',
schema: {
oneOf: [
{ $ref: getSchemaPath(MyDto) },
{ $ref: getSchemaPath(MyOtherDto) }
],
discriminator: {
propertyName: 'type',
mapping: {
[DtoType.MY_DTO]: 'MyDto',
[DtoType.MY_OTHER_DTO]: 'MyOtherDto',
},
},
}
})
getThing(): MyDto | MyOtherDto {
// code here
}
}