TypeORM + nest js: Как сериализовать + классы дочерних сущностей CRUD с использованием родительских обобщений, с одним контроллером, хранилищем, проверкой на подтипы? - PullRequest
0 голосов
/ 28 февраля 2020

Мне было трудно заставить дочерние объекты автоматически работать с 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;

}

Я сделал каждый тип блока своим собственным объектом, чтобы столбцы могли сериализоваться в базу данных должным образом и иметь проверки для каждого свойства.

Итак ... I У меня более 10 типов блоков, и я пытаюсь избежать отдельного контроллера и конечных точек для CRUD каждого типа блока. Мне бы просто хотелось, чтобы один BlockController, одна конечная точка / block, POSTing создавались, и PUT on / block /: id для обновления, где он мог бы выводить тип блока из параметра body 'type' запроса.

Проблема в том, что в запросе последний параметр @Body () не будет проверен (запрос не будет проходить через go), если я не использую тип 'any' ... потому что каждый пользовательский тип блока передает свои дополнительные / Пользовательские свойства. В противном случае мне пришлось бы использовать каждый указанный c блочный дочерний класс в качестве типа параметра, требуя настраиваемые методы для каждого типа.

Чтобы добиться этого, я пытаюсь использовать пользовательский валидатор Pipe и дженерики, где я может смотреть на входящий параметр тела 'type' и приводить или создавать входящие данные в качестве заданного c Тип блока.

Обработчик контроллера:

@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 (это предполагается чтобы преобразовать входящий объект данных в указанный тип блока c, а затем проверить его, вернуть входящий объект данных в качестве этого типа):

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

... Это все, что должно дать мне правильное создание экземпляров блочного подкласса для использования контроллером, но я не уверен, как передать этот указанный тип подкласса c базовым сервисным вызовам для обновления указанных c блочных репозиториев для каждого типа сущности. Можно ли это сделать с помощью обобщений?

Для экземпляра в BlockService, но я должен передать указанный тип блока c (TextBlock, ButtonBlock и т. Д. c) в метод repository.save (), так что он будет сериализовать типы подклассов в соответствующие таблицы. Я предполагаю, что это возможно сделать, но кто-то, пожалуйста, исправьте меня, если я здесь не прав ...

Я пытаюсь сделать это, где я передаю данные блока как его родительский тип блока и пытаюсь чтобы затем получить его специфицированный c тип класса для передачи, чтобы сохранить, но он не работает ...

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 всегда возвращает объект, мне нужно вызвать 'blockCreate.constructor.name', чтобы получить правильное имя типа блока подкласса, но не может передать его как тип T.

Так что мне интересно ... есть ли способ вернуть подкласс Type T Параметр весь путь от помощника контроллера (где он должен приводить и проверять подтип) в хранилище, чтобы я мог передать этот тип T вызову save (entity) ... и правильно ли это зафиксировать? Или есть какой-то другой способ получить этот тип T из самого экземпляра объекта, если 'typeof block' не возвращает указанный тип подкласса c? Я не думаю, что это возможно сделать во время компиляции ...?

Я просто пытаюсь получить сериализацию и валидацию подкласса, работая только с одним набором конечных точек контроллера и сервисным уровнем / хранилище вызывает ... Должен ли я искать Частичные сущности?

Кто-нибудь знает о каком-либо направлении, которое я могу найти, чтобы достичь sh этого?

1 Ответ

1 голос
/ 07 марта 2020

Позволяет просто установить два базовых / обобщенных c класса:

  • класс обслуживания db / rest и
  • класс контроллера db / rest,

каждый с типом generi c: - для типа Entity generi c.

Затем вы просто расширяете их для указанных вами c сущностей и предоставляете сущность.

Обратите внимание, что по сути, все это всего лишь пара обобщенных c классов обтекания вокруг функциональность, доступная от TypeOrm.

Вот скелеты идеи, но я проверил их, и они отлично работали для меня. (Код поставляется с комментариями).

Давайте начнем с обобщенного c класса обслуживания с некоторыми распространенными функциями 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 ....
}

Далее вы пишете контроллер generi c это делегирует рабочую нагрузку службе - в соответствии с функциями службы - что-то вроде этого:

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 - service :

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, которые вы помещаете в службу generi c и контроллер generi c, к любому другому объекту без перезаписи какого-либо из них внутри своих контроллеров или служб. И все же у вас будет гибкость в применении определенных c REST / db функциональных возможностей к каждому контроллеру / сервису сущностей внутри их отдельных расширенных классов.

Теперь, помните, это всего лишь базовый c, скелет дизайн и нуждается во всех других основах, но вы должны начать с такого подхода, который опять-таки может соответствовать некоторым, а некоторые нет.

Некоторые синтаксис примеров REST взят непосредственно из Nest Js docs / website.

(TS-гуру, пожалуйста, не стесняйтесь предоставлять улучшения, предложения и т. Д. c. Особенно в отношении декораторов где мне повезло испытать ...)

...