typescript - NestJs återanvändbar controller med validering


De flesta av mina NestJs-kontroller ser likadana ut. De har grundläggande CRUD-funktionalitet och gör exakt samma saker.

De enda skillnaderna mellan kontrollerna är:

  • stigen
  • tjänsten som injiceras (och tjänsterna är alla utökade från en abstrakt tjänst)
  • entiteten som returneras från metoderna
  • skapa, uppdatera och fråga dtos

Här är ett exempel på en CRUD-kontroller:

@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);
  }
}

Det här är gränssnittet jag har skapat för mina kontroller.

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>>;
}

Att skriva alla dessa repetitiva kontroller har blivit ganska tröttsamt (ja, jag vet om nest g resource men det är egentligen inte poängen med den här frågan), så jag bestämde mig för att skapa en abstrakt kontroll som kommer att göra det mesta av det tunga arbetet och låta kontrollerna utöka denna.

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);
  }
}

Nu allt jag behöver göra för att lägga till en ny kontroller är detta:

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

Jag var mycket stolt över mig själv vid den tidpunkten. Det var tills jag upptäckte att validering inte längre fungerar eftersom class-validator inte fungerar med generiska typer.

Det måste finnas något sätt jag kan fixa detta med minimal inblandning och maximal användning av återanvändbar kod?

Svar #1

Jag har lyckats få det att fungera med hjälp av detta svar https://stackoverflow.com/a/64802874/1320704

Tricket är att skapa en controller-fabrik och använda ett anpassat valideringsrör.

Här är lösningen:

@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;
}

Och för att skapa själva kontrollern skickar du helt enkelt de önskade DTO:erna till fabriken:

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

Du kan också valfritt skicka in svarsenhetstypen till fabriken och använda den med @ApiResponse-taggen om du använder Swagger. Du kan också skicka in sökvägen till fabriken och flytta alla dekoratorer (Controller, UseGuards etc.) till fabrikscontrollerdefinitionen.

Relaterade artiklar