Nest JS Нужны ли мне DTO вместе с сущностями? - PullRequest
1 голос
/ 04 августа 2020

Я создаю простой сервис, который будет делать простой CRUD. Пока у меня есть entity user:

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column({ name: "first_name" })
  firstName: string;

  @Column({ name: "last_name" })
  lastName: string;

  @Column({ name: "date_of_birth" })
  birthDate: string;
}

Контроллер:

import { Controller, Get,  Query } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('api/v1/backoffice')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':username')
  findOne(@Query('username') username: string) {
    return this.usersService.findByUsername(username);
  }
}

Service:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}


  findByUsername(username: string): Promise<User | undefined> {
    return this.usersRepository.findOne({ username });
  }
}

С этой базой c Например, я возвращаю значения из БД, где некоторые столбцы переименованы: first_name -> firstName

Это действительно служит моей цели, но во многих местах я вижу, что используется DTO. Я знаю, что делаю неправильные вещи, и что мне тоже следует начать использовать его. Как мне использовать подход DTO в моем примере?

Я пытаюсь asp изложить здесь эту концепцию.

1 Ответ

1 голос
/ 08 августа 2020

Во-первых, комментарий @Carlo Corradini правильный, вам следует взглянуть на библиотеки class-transformer и class-validator, которые также заметно используются под капотом в каналах Nest JS и могут быть красиво объединены с TypeORM.

1: в соответствии с комментарием @Carlo Corradini и соответствующими ссылками

Теперь, поскольку ваш экземпляр DTO является представлением данных, которые вы хотите предоставить своему потребителю , вам необходимо создать его экземпляр после того, как вы получили свою пользовательскую сущность.

  1. создайте новый user-response.dto.ts файл и объявите внутри него класс UserResponseDto, который вы будете экспортировать. Допустим, код будет выглядеть следующим образом, если вы хотите открыть все из ранее полученной User сущности

user-response.dto.ts

import { IsNumber, IsString } from 'class-validator';
import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class UserResponseDto {
  @Expose()
  @IsNumber()
  id: number;

  @Expose()
  @IsString()
  username: string;

  @Expose()
  @IsString()
  firstName: string;

  @Expose()
  @IsString()
  lastName: string;

  @Expose()
  @IsString()
  birthDate: string;
}

Здесь с помощью @Exclude () в в верхней части UserResponseDto, мы говорим class-transformer исключить любое поле, которое не имеет декоратора @Expose() в файле DTO, когда мы будем создавать экземпляр UserResponseDto из любого другого объекта. Затем с помощью @IsString() и @IsNumber() мы говорим классу-валидатору проверять типы данных полей при их проверке.

Преобразуйте свой объект User в экземпляр UserResponseDto:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}


  async findByUsername(username: string): Promise<User | undefined> {
    const retrievedUser = await this.usersRepository.findOne({ username });

    // instantiate our UserResponseDto from retrievedUser
    const userResponseDto = plainToClass(UserResponseDto, retrievedUser);

    // validate our newly instantiated UserResponseDto
    const errors = await validate(userResponseDto);
    if (errors.length) {
        throw new BadRequestException('Invalid user',
this.modelHelper.modelErrorsToReadable(errors));
    }
    return userResponseDto;
  }
}

2: Другой способ достижения этого:

Вы также можете использовать ClassSerializerInterceptor перехватчик из @ nestjs / common , чтобы автоматически преобразовывать ваши возвращенные Entity экземпляры из служб в правильный возвращаемый тип, определенный в методе вашего контроллера. Это означало бы, что вам даже не пришлось бы беспокоиться об использовании plainToClass в вашем сервисе и позволить выполнять эту работу самому перехватчику Nest, с небольшими деталями, как указано в официальных документах.

Обратите внимание, что мы должны вернуть экземпляр класса. Если вы вернете простой объект JavaScript, например {user: new UserEntity ()}, объект не будет должным образом сериализован.

Код будет выглядеть следующим образом:

users.controller.ts

import { ClassSerializerInterceptor, Controller, Get,  Query } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('api/v1/backoffice')
@UseInterceptors(ClassSerializerInterceptor) // <== diff is here
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':username')
  findOne(@Query('username') username: string) {
    return this.usersService.findByUsername(username);
  }
}

users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getRepository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}


  async findByUsername(username: string): Promise<User | undefined> {
    return this.usersRepository.findOne({ username }); // <== must be an instance of the class, not a plain object
  }
}

Последние мысли : с последним решением вы даже можете использовать class-transformer декораторы в вашем файле сущности пользователя и не должны объявлять файл DTO, но вы потеряете проверку данных.

Сообщите мне, поможет ли это или если что-то неясно:)

РЕДАКТИРОВАТЬ с проверкой входящей полезной нагрузки и преобразованием в правильный DTO

Вы должны объявить GetUserByUsernameRequestDto с атрибутом имени пользователя в нем, например: get-user-by-username.request.dto.ts

import { IsString } from 'class-validator';
import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class GetUserByUsernameRequestDto {
  @Expose()
  @IsString()
  @IsNotEmpty()
  username: string;
}

users.controller.ts

import { ClassSerializerInterceptor, Controller, Get,  Query } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('api/v1/backoffice')
@UseInterceptors(ClassSerializerInterceptor) // <== diff is here
@UsePipes( // <= this is where magic happens :)
    new ValidationPipe({
        forbidUnknownValues: true,
        forbidNonWhitelisted: true,
        transform: true
    })
)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':username')
  findOne(@Param('username') getUserByUsernameReqDto: GetUserByUsernameRequestDto) {  
    return this.usersService.findByUsername(getUserByUsernameReqDto.username);
  }
}

Здесь мы используем концепцию каналов Nest - @UsePipes () - для выполнения работы. вместе со встроенным ValidationPipe от Nest. Вы можете обратиться к документации как из Nest , так и из class-validator , чтобы узнать больше о параметрах передачи ValidationPipe

Таким образом, ваш входящий параметры и данные полезной нагрузки могут быть проверены перед обработкой :)

...