繁体   English   中英

TypeORM+nestjs:如何使用父泛型序列化+CRUD 子实体类,具有单个控制器、存储库、子类型验证?

[英]TypeORM+nestjs: How to serialize+CRUD child entity classes using parent generics, with single controller, repository, validation on subtypes?

我一直很难让子实体与 REST api 一起自动工作。

我有一个基类:

class Block {

    @PrimaryGeneratedColumn('uuid')
    public id: string;

    @Column()
    public type: string;

}

然后将其扩展到其他块类型,例如:

@Entity('sites_blocks_textblock')
class TextBlock extends Block {

    @Column()
    public text: string;

}

我使每个块类型成为自己的实体,以便列可以正确序列化到数据库,并对每个属性进行验证。

所以......我有 10 多种块类型,我试图避免使用单独的控制器和端点来 CRUD 每种块类型。 我只想要一个 BlockController,一个 /block 端点,POSTing 来创建,并在 /block/:id 上 PUT 用于更新,它可以从请求的“type”主体参数推断块的类型。

问题是在请求中,最后一个 @Body() 参数不会验证(请求不会通过),除非我使用类型“any”......因为每个自定义块类型都在传递它的额外/自定义属性。 否则我将不得不使用每个特定的 Block 子类作为参数类型,需要为每种类型定制方法。

为了实现这一点,我尝试使用自定义验证管道和泛型,我可以在其中查看传入的“类型”主体参数,并将传入数据转换或实例化为特定的块类型。

控制器处理程序:

@Post()
@UseGuards(PrincipalGuard)
public create(@Principal() principal: User,
          @Param('siteId', ParseUUIDPipe) siteId: string,
          @Body(new BlockValidationPipe()) blockCreate: any): Promise<Block> {

    return this.blockService.create(principal.organization, siteId, blockCreate);

}

BlockValidationPipe(这应该将传入的数据对象转换为特定的块类型,然后对其进行验证,将传入的数据对象作为该类型返回):

@Injectable()
export class BlockValidationPipe implements PipeTransform<any> {
    async transform(value: any, { metatype }: ArgumentMetadata) {
        if (value.type) {
            if (value.type.id) {
                metatype = getBlockTypeFromId(value.type.id);
            }
        }

        if (!metatype || !this.toValidate(metatype)) {
            return value;
        }

                // MAGIC: ==========>
        let object = objectToBlockByType(value, value.type.id, metatype);

        const errors = await validate(object);
        if (errors.length > 0) {
            throw new BadRequestException(errors, 'Validation failed');
        }

        return object ? object : value;
    }


    private toValidate(metatype: Function): boolean {
        const types: Function[] = [String, Boolean, Number, Array, Object];
        return !types.includes(metatype);
    }
}

使用这个助手(但它可能无法完全按预期工作,还没有完全传递类型):

function castOrNull<C extends Block>(value: C, type): C | null {
    return value as typeof type;
}

export function objectToBlockByType(object, typeId, metatype) {
    switch(typeId) {
        case 'text':
            return castOrNull<TextBlock>(object, TextBlock);
        case 'avatar':
            return castOrNull<AvatarBlock>(object, AvatarBlock);
        case 'button':
            return castOrNull<ButtonBlock>(object, ButtonBlock);
                // etc....
        default:
            return castOrNull<Block>(object, Block);
    }
}

...这一切都应该只是给我一个合适的 Block 子类实例供控制器使用,但我不确定如何将这个特定的子类类型传递给底层服务调用以更新每个实体类型的特定块存储库。 使用泛型可以做到这一点吗?

例如,在 BlockService 中,但我应该将特定的块类型(TextBlock、ButtonBlock 等)传递给 repository.save() 方法,以便它将子类类型正确地序列化到它们各自的表中。 我假设这是可能的,但如果我在这里错了,请有人纠正我......

我正在尝试这样做,我将块数据作为其块父类型传递,然后尝试将其特定的类类型传递给保存,但它不起作用......

public async create(organization: Organization, siteId: string, blockCreate: Block): Promise<Block> {

    let blockType: Type<any> = getBlockTypeFromId(blockCreate.type.id);
    console.log("create block", typeof blockCreate, blockCreate.constructor.name, blockCreate, typeof blockType, blockType);

        ///
    let r = await this.blockRepository.save<typeof blockCreate>({

        organization: organization,
        site: await this.siteService.getByIdAndOrganization(siteId, organization),
        type: await this.blockTypeService.getById(blockCreate.type.id),
        ...blockCreate

    });

    //r.data = JSON.parse(r.data);
    return r;
}

这里的问题是“typeof blockCreate”总是返回“object”,我必须调用“blockCreate.constructor.name”来获取正确的子类块类型名称,但不能将其作为类型 T 传递。

所以我想知道......反正有没有将子类 Type T 参数从控制器助手(它应该在那里转换和验证子类型)一直返回到存储库,以便我可以将此类型 T 传递给保存(实体)调用...并正确提交? 或者有没有其他方法可以从对象实例本身获取这种类型 T,如果“typeof 块”没有返回特定的子类类型? 我认为在编译时不可能做前者......?

我真的只是试图让子类序列化和验证与一组控制器端点和服务层/存储库调用一起工作......我应该研究部分实体吗?

任何人都知道我可以寻求实现这一目标的任何方向?

让我们简单地设置两个基类/泛型类:

  • db/rest 服务类和
  • 一个 db/rest 控制器类,

每个具有以下泛型类型: <E> - 用于实体泛型类型。

然后您只需为您的特定实体扩展它们并提供实体。

请注意,本质上,整个过程只是围绕 TypeOrm 可用功能的几个通用包装类。

这是这个想法的骨架,但我对它们进行了测试,它们对我来说效果很好。 (代码带有注释)。

让我们从具有一些常见 db / REST 函数的通用服务类开始:

import { Repository, DeepPartial, SaveOptions } from "typeorm";
import { Injectable } from '@nestjs/common';


            /**
             * Provides common/general functionality for working with db data
             * via TypeOrm API.
             * 
             * see:
             *  https://github.com/typeorm/typeorm/blob/master/docs/repository-api.md
             * 
             * You can extend this service class for functionalities specific
             * to your given Entity.
             * 
             * A service is the work-horse for handling the tasks 
             * (such as fetching the data from data source / db)
             * delegated by a controller.
             * The service is injected in the controller who delegates the tasks
             * to the service for specific data sets / Entities / db tables.
             */
@Injectable()
export class DbGenService<E> {

            /**
             * @param repo 
             *    is TypeOrm repository for your given Entity <E>.
             *    (the intermediary object, which does all the work on the db end).
             */
  constructor(readonly repo: Repository<E>) {}

            /**
             * (AUX function to create entity object):
             * Creates a new entity/entities and copies all entity properties 
             * from given objects into their new entities.
             * Note that it copies only properties that are present in the entity schema.
             * @param obj 
             */
  async createE(obj): Promise<E[]> {
    return this.repo.create(obj);
  }
            /**
             * (AUX function to merge two entity objects, 1st can be set empty):
             * Merges multiple entities (or entity-like objects) into a given entity.
             *
             * @param mergeIntoEntity 
             *    the initial / source and 
             *    finally the target/resulting/merged entity
             *    Can be initilized with an empty object e.g:
             *    let e: E = {} as E;
             * @param entityLikes
             *    partial entity or an object looking like the entity
             */
  async mergeEs(mergeIntoEntity: E, ...entityLikes: DeepPartial<E>[]): Promise<E> {
    return this.repo.merge(mergeIntoEntity, ...entityLikes);
  }

            /**
             * Saves a given entity in the database.
             * If entity does not exist in the database,
             * then inserts, otherwise updates.
             */
  async saveRecord(recordEntity: E): Promise<E> {
    return await this.repo.save(recordEntity);
  }
            /**
             * Saves all given entities (array) in the database.
             * If entities do not exist in the database,
             * then inserts, otherwise updates.
             */
  async saveRecords<T extends DeepPartial<E>>(entities: T[], options?: SaveOptions): Promise<(T & E)[]> {
    return await this.repo.save(entities, options);
  }

            /**
             * Return all the records of the db table for this Entity
             */
  async getAllRecords(): Promise<E[]> {
    return await this.repo.find();
  } 

            /**
             * Return the record of the db table for this Entity
             * having 
             * @param id = id
             */
  async getRecordById(recID: number): Promise<E> {
    return await this.repo.findOne(recID);
  }


            /**
             * Deletes the records of the db table for this Entity
             * having query statement:
             * @param query = query
             */
  async deleteAllRecords(): Promise<void> {
    await this.repo.clear();
  }

            /**
             * deletes the record of the db table for this Entity
             * having 
             * @param id = id
             */
  async deleteRecord(id): Promise<void> {
    await this.repo.delete(id);
  }

  // ... + add your common db functions here 
  //      and match them with the generic controller ....
}

接下来,您编写一个通用控制器,它将工作负载委托给服务 - 匹配服务功能 - 如下所示:

import { DeepPartial } from 'typeorm';
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { DbGenService } from './db-gen.service';

            /**
             * General/base controller - handles basic HTTP requests of:
             *    Get, Query, Post, Body, Put, Param, Delete.
             * 
             * Provides general/base/shared db functionality 
             * (layed out in the service class: DbGenService<E> - via TypeOrm API)
             * to exteded controllers of this DbGenController class.
             * 
             * You can use this controller as a base class for your
             * specific controllers that share the same functionalities
             * with this controller.
             * 
             * Simply extend it like this:
             * 
             * @Controller('myRoute')
             * export class MyController extends DbGenController<MyEntity> { ... }
             * 
             * the extended router than handles requests such as
             * e.g:
             *    http://localhost:3000/myRoute
             *    http://localhost:3000/myRoute/1
             * 
             * 
             */
@Controller()
export class DbGenController<E> {

            /**
             * DbGenService is the class with the generic working functions
             * behind the controller
             */
  constructor(private dbGenService: DbGenService<E>) {}

            /**
             * Saves all given entities (array) in the database.
             * If entities do not exist in the database,
             * then inserts, otherwise updates.
             */
  @Post()
  async saveRecord(@Body() dto: DeepPartial<E>) {
              // create the Entity from the DTO
    let e: E[] = await this.dbGenService.createE(dto);
    // OR:
    // let e: E = {} as E;
    // e = await this.dbGenService.mergeEs(e, dto);
    const records = await this.dbGenService.saveRecords(e);
    return records;
  }

            /**
             * Return all the records of the db table for this Entity
             */
  @Get()
  async getAllRecords(): Promise<E[]> {
    const records = await this.dbGenService.getAllRecords();
    return records;
  }

            /**
             * Return the record of the db table for this Entity
             * having 
             * @param id = id
             */
  @Get(':id')
  async getRecordById(@Param('id') id): Promise<E> {
      const records = await this.dbGenService.getRecordById(id);
      return records;
  }

            /**
             * Return the record of the db table for this Entity
             * having 
             * @param id = id
             */
  @Get()
  async getRecordByFVs(@Param('id') id): Promise<E> {
      const records = await this.dbGenService.getRecordById(id);
      return records;
  }

            /**
             * Deletes all the records of the db table for this Entity
             */
  @Delete()
  async deleteAllRecords(): Promise<void> {
      const records = await this.dbGenService.deleteAllRecords();
      return records;
  }

            /**
             * Deletes the records of the db table for this Entity
             * having query statement:
             * @param query = query
             */
  @Delete()
  async deleteRecord(@Query() query): Promise<void> {
      const records = await this.dbGenService.deleteRecord(query.ID);
      return records;
  }

            /**
             * Deletes the record of the db table for this Entity
             * having 
             * @param id = id
             */
  @Delete(':id')
  deleteRecordById(@Param('id') id): Promise<void> {
    return this.dbGenService.deleteRecord(id);
  }

}

...现在是美丽/有趣的部分 - 将它们用于您想要的任何实体 - 例如 UsersEntity - 服务:

import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { DbGenService } from '../../generic/db-gen.service';
import { UsersEntity } from '../../../entities/users.entity';

            /**
             * Users db records service.
             * 
             * General doc:
             * ------------
             * A db service is the work-horse for handling the tasks
             * (such as fetching the data from data source / db)
             * delegated by a controller.
             * The service is injected in the controller.
             * 
             * This service extends the usege of the common/generic
             * db taks/functions of the service class: DbGenService<E>,
             * where <E> is the given Entity type, which we we pass to the
             * DbGenService instance, reflecting so exactly the Entity
             * of this extended class - in this case the: UsersEntity
             */
@Injectable()
export class UsersService<UsersEntity> extends DbGenService<UsersEntity> {

  constructor(@InjectRepository(UsersEntity) repo: Repository<UsersEntity>) {
    super(repo);
  }

}

现在是 UsersEntity - 控制器:

import { Controller } from '@nestjs/common';
import { appCfg } from '../../../../config/app-config.service';
import { DbGenController } from '../../generic/db-gen.controller';
import { UsersEntity } from '../../../entities/users.entity';
import { UsersService } from './users.service';

            /**
             * Controller - handles HTTP requests.
             * 
             * This controller handles routes of HTTP requests with suffix:
             *    /users
             * due to the decorator:
             *    @Controller('users')
             * e.g:
             *    http://localhost:3000/users
             *    http://localhost:3000/users/1
             * 
             * This service extends the usage of the common/generic
             * db controller class: DbGenController<E>,
             * where <E> is the given Entity type, which we we pass to the
             * DbGenController instance, reflecting so exactly the Entity
             * of this extended class - in this case the: UsersEntity
             */
@Controller('users')
export class UsersController extends DbGenController<UsersEntity> {

  constructor(private usersService: UsersService<UsersEntity>) {
    super(usersService);
  }

}

...当然,将它链接在一起:

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UsersEntity } from '../../../entities/users.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

            /**
             * UsersModule is used to export the UsersService,
             * so that other modules, specifically the AuthModule, 
             * can communicate with the database to perform 
             * its user authentication functions via an access to UsersService.
             */
@Module({
  imports: [TypeOrmModule.forFeature([UsersEntity])],
  controllers: [UsersController],
  providers: [UsersService]
})
export class UsersModule {}

与“UsersEntity”类似,您现在可以将放置在通用服务和通用控制器中的所有上述 REST 功能应用于任何其他实体,而无需在其控制器或服务中重写任何它。 而且,您仍然可以灵活地将特定的 REST/db 功能应用于每个实体控制器/服务的各个扩展类中。

现在,请记住,这只是一个基本的骨架设计,需要所有其他必需品,但是应该可以让您开始使用这种方法,这种方法可能适合一些,也可能不适合。

REST 示例的一些语法直接来自 NestJs 文档/网站。

(TS大师请随时提供改进,建议等。尤其是在装饰者周围,我很幸运的经历......)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM