带验证的 NestJs 可重用控制器
NestJs reusable controller with validation
我的大多数 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 不适用于泛型类型。
必须有某种方法可以通过最少的干预和最大程度地使用可重用代码来解决这个问题吗?
我已经设法使用这个答案使其工作
诀窍是创建控制器工厂,并使用自定义验证管道。
解决方法如下:
@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;
}
要创建实际的控制器,您只需将所需的 dto 传递给工厂即可:
@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 等)移动到工厂控制器定义中。
我的大多数 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 不适用于泛型类型。
必须有某种方法可以通过最少的干预和最大程度地使用可重用代码来解决这个问题吗?
我已经设法使用这个答案使其工作
诀窍是创建控制器工厂,并使用自定义验证管道。
解决方法如下:
@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;
}
要创建实际的控制器,您只需将所需的 dto 传递给工厂即可:
@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 等)移动到工厂控制器定义中。