带验证的 NestJs 可重用控制器

Hom*_*cIs 8 typescript class-validator nestjs

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

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

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

下面是一个 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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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>>;
}
Run Code Online (Sandbox Code Playgroud)

编写所有这些重复的控制器非常烦人(是的,我知道,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);
  }
}
Run Code Online (Sandbox Code Playgroud)

现在添加新控制器所需要做的就是:

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

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

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

Hom*_*cIs 9

我已经设法使用这个答案让它工作/sf/answers/4536201211/

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

这是解决方案:

@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;
}
Run Code Online (Sandbox Code Playgroud)

要创建实际的控制器,您只需将所需的 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();
  }
}
Run Code Online (Sandbox Code Playgroud)

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