【问题标题】:NestJs reusable controller with validation带有验证的 NestJs 可重用控制器
【发布时间】:2022-03-23 15:58:46
【问题描述】:

我的大多数 NestJs 控制器看起来都一样。它们具有基本的 CRUD 功能并执行完全相同的操作。

控制器之间的唯一区别是:

  • 路径
  • 注入的服务(并且服务都是从抽象服务扩展而来的)
  • 从方法返回的实体
  • 创建、更新和查询 dtos

这是一个示例 CRUD 控制器:

@UseGuards(JwtAuthGuard)
@Controller("/api/warehouse/goods-receipts")
export class GoodsReceiptsController
  implements ICrudController<GoodsReceipt, CreateGoodsReceiptDto, UpdateGoodsReceiptDto, QueryGoodsReceiptDto> {
  constructor(private service: GoodsReceiptsService) {
  }

  @Post()
  create(@Body() body: CreateGoodsReceiptDto, @CurrentUser() user: Partial<User>): Promise<GoodsReceipt> {
    return this.service.createItem(body, user);
  }

  @Delete(":id")
  delete(@Param() params: NumberIdDto): Promise<Partial<GoodsReceipt>> {
    return this.service.deleteItem(params.id);
  }

  @Get(":id")
  getOne(@Param() params: NumberIdDto): Promise<GoodsReceipt> {
    return this.service.getItem(params.id);
  }

  @Get()
  get(@Query() query: QueryGoodsReceiptDto): Promise<GoodsReceipt[]> {
    return this.service.getItems(query);
  }

  @Patch()
  update(@Body() body: UpdateGoodsReceiptDto, @CurrentUser() user: Partial<User>): Promise<GoodsReceipt> {
    return this.service.updateItem(body,user);
  }
}

这是我为我的控制器创建的界面:

export interface ICrudController<EntityType, CreateDto, UpdateDto, QueryDto> {

  getOne(id: NumberIdDto): Promise<EntityType>;

  get(query: QueryDto): Promise<EntityType[]>;

  create(body: CreateDto, user: Partial<User>): Promise<EntityType>;

  update(body: UpdateDto, user: Partial<User>): Promise<EntityType>;

  delete(id: NumberIdDto): Promise<Partial<EntityType>>;
}

编写所有这些重复的控制器已经很累了(是的,我知道nest g resource,但这并不是这个问题的重点),所以我决定创建一个抽象控制器来完成大部分繁重的工作并拥有控制器对此进行了扩展。

export abstract class CrudController<T, C, U, Q> implements ICrudController<T, C, U, Q> {
  protected service: ICrudService<T, C, U, Q>;

  @Post()
  create(@Body() body: C, @CurrentUser() user: Partial<User>): Promise<T> {
    return this.service.createItem(body, user);
  }

  @Get(":id")
  getOne(@Param() params: NumberIdDto): Promise<T> {
    return this.service.getItem(params.id);
  }

  @Get()
  get(@Query() query: Q): Promise<T[]> {
    return this.service.getItems(query);
  }

  @Delete(":id")
  delete(@Param() params: NumberIdDto): Promise<Partial<T>> {
    return this.service.deleteItem(params.id);
  }

  @Patch()
  update(@Body() body: U, @CurrentUser() user: Partial<User>): Promise<T> {
    return this.service.updateItem(body, user);
  }
}

现在我需要做的就是添加一个新的控制器:

@UseGuards(JwtAuthGuard)
@Controller("/api/warehouse/goods-receipts")
export class GoodsReceiptsController
  extends CrudController<GoodsReceipt, CreateGoodsReceiptDto, UpdateGoodsReceiptDto, QueryGoodsReceiptDto> {
  constructor(protected service: GoodsReceiptsService) {
    super();
  }
}

那时我为自己感到非常自豪。直到我发现验证不再有效,因为 class-validator 不适用于泛型类型。

必须有某种方法可以通过最少的干预和最大程度地使用可重用代码来解决这个问题吗?

【问题讨论】:

    标签: typescript nestjs class-validator


    【解决方案1】:

    我已经设法使用这个答案https://stackoverflow.com/a/64802874/1320704使其工作

    诀窍是创建控制器工厂,并使用自定义验证管道。

    解决办法如下:

    @Injectable()
    export class AbstractValidationPipe extends ValidationPipe {
      constructor(
        options: ValidationPipeOptions,
        private readonly targetTypes: { body?: Type; query?: Type; param?: Type; }
      ) {
        super(options);
      }
    
      async transform(value: any, metadata: ArgumentMetadata) {
        const targetType = this.targetTypes[metadata.type];
        if (!targetType) {
          return super.transform(value, metadata);
        }
        return super.transform(value, { ...metadata, metatype: targetType });
      }
    }
    
    export function ControllerFactory<T, C, U, Q>(
      createDto: Type<C>,
      updateDto: Type<U>,
      queryDto: Type<Q>
    ): ClassType<ICrudController<T, C, U, Q>> {
      const createPipe = new AbstractValidationPipe({ whitelist: true, transform: true }, { body: createDto });
      const updatePipe = new AbstractValidationPipe({ whitelist: true, transform: true }, { body: updateDto });
      const queryPipe = new AbstractValidationPipe({ whitelist: true, transform: true }, { query: queryDto });
    
      class CrudController<T, C, U, Q> implements ICrudController<T, C, U, Q> {
        protected service: ICrudService<T, C, U, Q>;
    
        @Post()
        @UsePipes(createPipe)
        async create(@Body() body: C, @CurrentUser() user: Partial<User>): Promise<T> {
          return this.service.createItem(body, user);
        }
    
        @Get(":id")
        getOne(@Param() params: NumberIdDto): Promise<T> {
          return this.service.getItem(params.id);
        }
    
        @Get()
        @UsePipes(queryPipe)
        get(@Query() query: Q): Promise<T[]> {
          return this.service.getItems(query);
        }
    
        @Delete(":id")
        delete(@Param() params: NumberIdDto): Promise<Partial<T>> {
          return this.service.deleteItem(params.id);
        }
    
        @Patch()
        @UsePipes(updatePipe)
        update(@Body() body: U, @CurrentUser() user: Partial<User>): Promise<T> {
          return this.service.updateItem(body, user);
        }
      }
    
      return CrudController;
    }
    

    要创建实际的控制器,您只需将所需的 dtos 传递给工厂:

    @UseGuards(JwtAuthGuard)
    @Controller("/api/warehouse/goods-receipts")
    export class GoodsReceiptsController
      extends ControllerFactory<GoodsReceipt, CreateGoodsReceiptDto, UpdateGoodsReceiptDto, QueryGoodsReceiptDto>
      (CreateGoodsReceiptDto,UpdateGoodsReceiptDto,QueryGoodsReceiptDto){
      constructor(protected service: GoodsReceiptsService) {
        super();
      }
    }
    

    如果您使用 swagger,您还可以选择将响应实体类型传递给工厂,并将其与 @ApiResponse 标记一起使用。 您也可以将路径传递给工厂并将所有装饰器(Controller、UseGuards 等)移动到工厂控制器定义中。

    【讨论】:

    • 你能解释一下什么是ClassType吗?
    • @MML1357 我认为它来自 swagger 或 graphql 包。此后,我将其替换为来自@nestjs/commonType
    • 我正在使用招摇。在创建时,当我尝试时,插入正文的文本区域不显示。
    • 需要在工厂创建函数中添加@ApiBody装饰器。 @ApiBody({ type: createDto }) 您还需要在传递实体类型的工厂构造函数中添加另一个参数,然后添加 @ApiResponse({ type: entityType }) 以便 swagger 知道端点返回什么。您可能会觉得有用的其他装饰器是 @ApiParam@ApiQuery 等。
    • 您是否使用@ApiProperty 装饰了您的DTO 中的字段?或者,如果您不想这样做,您可以在 nest-cli.json 中启用 swagger 插件。
    猜你喜欢
    • 1970-01-01
    • 2022-08-02
    • 2019-08-29
    • 1970-01-01
    • 1970-01-01
    • 2016-12-05
    • 1970-01-01
    • 2021-02-03
    • 1970-01-01
    相关资源
    最近更新 更多