最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - NestJs: How to have Body input shape different from entity's DTO? - Stack Overflow

programmeradmin4浏览0评论

I have DTOs for my Photo and Tag objects that look like this:

export class PhotoDto {
    readonly title: string
    readonly file: string
    readonly tags: TagDto[]
}

export class TagDto {
    readonly name: string
}

I use the PhotoDto in my photo.service.ts and eventually in the photo.controller.ts for the creation of Photo:

// In photo.service.ts
async create(createPhotoDto: PhotoDto): Promise<PhotoEntity> {
   // ...
   return await this.photoRepo.create(createPhotoDto)
}

// In photo.controller.ts
@Post()
async create(@Body() createPhotoDto: PhotoDto): Promise<PhotoEntity> {
   // ...
}

However, the input in the Body of the API is expected to have this structure:

{
   "title": "Photo Title",
   "file": "/some/path/file.jpg",
   "tags": [
      {
         "name": "holiday"
      },
      {
         "name": "memories"
      }
   ]
}

How can I change the input shape of the Body to accept this structure instead?

{
   "title": "Photo Title",
   "file": "/some/path/file.jpg",
   "tags": ["holiday", "memories"]
}

I have tried creating 2 different DTOs, a CreatePhotoDto and an InputPhotoDto, one for the desired input shape in the controller and one for use with the service and entity, but this ends up very messy because there is a lot of work with converting between the 2 DTOs.

What is the correct way to have a different input shape from the Body of a Post request and then have it turned into the DTO required for use by the entity?

I have DTOs for my Photo and Tag objects that look like this:

export class PhotoDto {
    readonly title: string
    readonly file: string
    readonly tags: TagDto[]
}

export class TagDto {
    readonly name: string
}

I use the PhotoDto in my photo.service.ts and eventually in the photo.controller.ts for the creation of Photo:

// In photo.service.ts
async create(createPhotoDto: PhotoDto): Promise<PhotoEntity> {
   // ...
   return await this.photoRepo.create(createPhotoDto)
}

// In photo.controller.ts
@Post()
async create(@Body() createPhotoDto: PhotoDto): Promise<PhotoEntity> {
   // ...
}

However, the input in the Body of the API is expected to have this structure:

{
   "title": "Photo Title",
   "file": "/some/path/file.jpg",
   "tags": [
      {
         "name": "holiday"
      },
      {
         "name": "memories"
      }
   ]
}

How can I change the input shape of the Body to accept this structure instead?

{
   "title": "Photo Title",
   "file": "/some/path/file.jpg",
   "tags": ["holiday", "memories"]
}

I have tried creating 2 different DTOs, a CreatePhotoDto and an InputPhotoDto, one for the desired input shape in the controller and one for use with the service and entity, but this ends up very messy because there is a lot of work with converting between the 2 DTOs.

What is the correct way to have a different input shape from the Body of a Post request and then have it turned into the DTO required for use by the entity?

Share Improve this question edited May 2, 2021 at 15:17 Nikita Fedyashev 19k13 gold badges56 silver badges107 bronze badges asked Apr 1, 2019 at 4:18 CarvenCarven 15.7k30 gold badges124 silver badges183 bronze badges 0
Add a ment  | 

3 Answers 3

Reset to default 10

You can use the auto-transform of the ValidationPipe():

1) Add the ValidationPipe to your controller:

@UsePipes(new ValidationPipe({ transform: true }))
@Post()
async create(@Body() createPhotoDto: PhotoDto): Promise<PhotoEntity> {
   // ...
}

2) Add a @Transform to your PhotoDto:

// Transforms string[] to TagDto[]
const transformTags = tags => {
  if (Array.isArray(tags)) {
    return tags.map(tag => ({name: tag}))
  } else {
    return tags;
  }
}


import { Transform } from 'class-transformer';
export class PhotoDto {
    readonly title: string
    readonly file: string
    @Transform(transformTags, {toClassOnly: true})
    readonly tags: TagDto[]
}

Update DTO to

export class PhotoDto { readonly title: string readonly file: string readonly tags: Array<string> }

It will change the API structure to

{ "title": "Photo Title", "file": "/some/path/file.jpg", "tags": ["holiday", "memories"] }

currently your tags property is an array of object of type TagDto, change tags property to just array of string.

You can create a nest custom decorator to convert input data to your DTO object.

export const ConvertToCreateCatDto = createRouteParamDecorator((data, req): CreateCatDto => { // `createParamDecorator` for nest old version
    if (req.body.tags.every(value => typeof value === "string")) { // if input tags is a string[]
        req.body.tags = (req.body.tags as string[]).map<TagDto>((tag) => {
            return { // convert to TagDto
                name: tag + ""
            }
        });
    }
    let result = new CreateCatDto(req.body);
    // TODO: validate `result` object
    return result;
});

add constructor to CreateCatDto

export class CreateCatDto {
    readonly title: string;
    readonly file: number;
    readonly tags: TagDto[];

    constructor(obj: any) {
        this.title = obj.title;
        this.file = obj.file;
        this.tags = obj.tags;
    }
}

Finally, use @ConvertToCreateCatDto instead of @Body in you controller.

// In photo.controller.ts
@Post()
async create(@ConvertToCreateCatDto() createPhotoDto: PhotoDto): Promise<PhotoEntity> {
   //...
}
发布评论

评论列表(0)

  1. 暂无评论