mcpField
S
syncable-entity-integration

syncable-entity-integration

github.com/twentyhq开发60.0k52 次安装今天更新
代码仓库

关于 syncable-entity-integration

面向在 NestJS 中添加可同步实体的集成步骤指南,供完成前置步骤的开发者使用。说明模块注册、服务与解析器分层,以及 GraphQL 异常处理。

主要功能

  1. 01注册构建器、验证器和操作处理器
  2. 02创建返回扁平实体的服务层
  3. 03将扁平实体转换为 DTO
  4. 04通过拦截器格式化 GraphQL 异常

适用场景

  1. 01将新实体接入工作区迁移模块
  2. 02为 GraphQL API 添加实体服务与解析器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/twentyhq/twenty --skill syncable-entity-integration

安装后在 Claude Code 中输入/syncable-entity-integration确认已加载。

详细介绍

Syncable Entity:集成(第 5/6 步)

目的:将所有内容串联起来,在模块中注册,创建服务和解析器。

何时使用:完成第 1-4 步(所有步骤)之后。测试之前必需。


快速开始

本步骤:

  1. 在 3 个 NestJS 模块中注册服务
  2. 创建服务层(返回扁平实体)
  3. 创建解析器层(将扁平实体 → DTO)
  4. 使用异常拦截器处理 GraphQL

关键原则:服务返回扁平实体,解析器将扁平实体转译为 DTO。


第 1 步:在 Builder 模块中注册

文件:src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts

import { WorkspaceMigrationMyEntityActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/workspace-migration-my-entity-actions-builder.service';

@Module({
  imports: [
    // ... existing imports
  ],
  providers: [
    // ... existing providers
    WorkspaceMigrationMyEntityActionsBuilderService,
  ],
  exports: [
    // ... existing exports
    WorkspaceMigrationMyEntityActionsBuilderService,
  ],
})
export class WorkspaceMigrationBuilderModule {}

重要:同时添加到 providers 和 exports(builder 需要导出以供 orchestrator 使用)。


第 2 步:在 Validators 模块中注册

文件:src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts

import { FlatMyEntityValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service';

@Module({
  imports: [
    // ... existing imports
  ],
  providers: [
    // ... existing providers
    FlatMyEntityValidatorService,
  ],
  exports: [
    // ... existing exports
    FlatMyEntityValidatorService,
  ],
})
export class WorkspaceMigrationBuilderValidatorsModule {}

第 3 步:注册 Action Handlers

文件:src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts

import { CreateMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service';
import { UpdateMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service';
import { DeleteMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service';

@Module({
  imports: [
    // ... existing imports
  ],
  providers: [
    // ... existing providers
    CreateMyEntityActionHandlerService,
    UpdateMyEntityActionHandlerService,
    DeleteMyEntityActionHandlerService,
  ],
  exports: [
    // ... existing exports (action handlers typically not exported)
  ],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}

注意:action handlers 通常只放在 providers 中,而不是 exports。


第 4 步:创建服务层

文件:src/engine/metadata-modules/my-entity/my-entity.service.ts

import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';

import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { fromCreateMyEntityInputToUniversalFlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/utils/from-create-my-entity-input-to-universal-flat-my-entity.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';

@Injectable()
export class MyEntityService {
  constructor(
    private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
    private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
  ) {}

  async create(input: CreateMyEntityInput, workspaceId: string): Promise<FlatMyEntity> {
    // 1. Transform input to universal flat entity
    const universalFlatMyEntityToCreate = fromCreateMyEntityInputToUniversalFlatMyEntity({
      input,
      workspaceId,
    });

    // 2. Validate, build, and run
    const result =
      await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
        {
          allFlatEntityOperationByMetadataName: {
            myEntity: {
              flatEntityToCreate: [universalFlatMyEntityToCreate],
              flatEntityToDelete: [],
              flatEntityToUpdate: [],
            },
          },
          workspaceId,
          isSystemBuild: false,
        },
      );

    // 3. Throw if validation failed
    if (isDefined(result)) {
      throw new WorkspaceMigrationBuilderException(
        result,
        'Validation errors occurred while creating entity',
      );
    }

    // 4. Return freshly cached flat entity
    const { flatMyEntityMaps } =
      await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
        {
          workspaceId,
          flatMapsKeys: ['flatMyEntityMaps'],
        },
      );

    return findFlatEntityByIdInFlatEntityMapsOrThrow({
      flatEntityId: universalFlatMyEntityToCreate.id,
      flatEntityMaps: flatMyEntityMaps,
    });
  }
}

服务模式:

  1. 将输入转换为通用扁平实体
  2. 调用 validateBuildAndRunWorkspaceMigration
  3. 如果验证出错则抛出异常
  4. 返回扁平实体(不是 DTO)

第 5 步:创建解析器层

文件:src/engine/metadata-modules/my-entity/my-entity.resolver.ts

import { UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';

import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { MyEntityService } from 'src/engine/metadata-modules/my-entity/my-entity.service';
import { fromFlatMyEntityToMyEntityDto } from 'src/engine/metadata-modules/my-entity/utils/from-flat-my-entity-to-my-entity-dto.util';

@Resolver(() => MyEntityDto)
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
export class MyEntityResolver {
  constructor(private readonly myEntityService: MyEntityService) {}

  @Mutation(() => MyEntityDto)
  async createMyEntity(
    @Args('input') input: CreateMyEntityInput,
    @Workspace() { id: workspaceId }: Workspace,
  ): Promise<MyEntityDto> {
    // Service returns flat entity
    const flatMyEntity = await this.myEntityService.create(input, workspaceId);
    
    // Resolver converts flat entity to DTO
    return fromFlatMyEntityToMyEntityDto(flatMyEntity);
  }

  @Mutation(() => MyEntityDto)
  async updateMyEntity(
    @Args('id') id: string,
    @Args('input') input: UpdateMyEntityInput,
    @Workspace() { id: workspaceId }: Workspace,
  ): Promise<MyEntityDto> {
    const flatMyEntity = await this.myEntityService.update(id, input, workspaceId);
    return fromFlatMyEntityToMyEntityDto(flatMyEntity);
  }

  @Mutation(() => Boolean)
  async deleteMyEntity(
    @Args('id') id: string,
    @Workspace() { id: workspaceId }: Workspace,
  ) {
    await this.myEntityService.delete(id, workspaceId);
    return true;
  }
}

解析器职责:

  • 从服务接收扁平实体
  • 使用转换工具将扁平实体 → DTO
  • 将 DTO 返回给 GraphQL API
  • 使用异常拦截器进行错误格式化

第 6 步:扁平实体到 DTO 的转换

文件:src/engine/metadata-modules/my-entity/utils/from-flat-my-entity-to-my-entity-dto.util.ts

import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';

export const fromFlatMyEntityToMyEntityDto = (
  flatMyEntity: FlatMyEntity,
): MyEntityDto => {
  return {
    id: flatMyEntity.id,
    name: flatMyEntity.name,
    label: flatMyEntity.label,
    description: flatMyEntity.description,
    isCustom: flatMyEntity.isCustom,
    createdAt: flatMyEntity.createdAt,
    updatedAt: flatMyEntity.updatedAt,
    // Convert foreign key IDs to relation objects if needed
    // parentEntity: flatMyEntity.parentEntityId ? { id: flatMyEntity.parentEntityId } : null,
  };
};

各层职责

层输入输出职责
服务输入 DTO扁平实体业务逻辑、验证编排
解析器服务结果DTO扁平实体 → DTO 转换、GraphQL 暴露

服务层:

  • 在内部使用扁平实体
  • 返回 FlatMyEntity 类型
  • 不感知 DTO 或 GraphQL 类型

解析器层:

  • 从服务接收扁平实体
  • 将扁平实体转换为 DTO
  • 将 DTO 返回给 GraphQL API

异常拦截器

WorkspaceMigrationGraphqlApiExceptionInterceptor 自动处理:

  1. FlatEntityMapsException → 转换为 GraphQL 错误(NotFoundError 等)
  2. WorkspaceMigrationBuilderException → 使用 i18n 格式化验证错误
  3. WorkspaceMigrationRunnerException → 格式化 runner 错误

它的作用:

  • 捕获异常并为 API 响应进行格式化
  • 根据用户区域设置翻译错误消息
  • 确保前端获得一致的错误结构

检查清单

在进入第 6 步(测试)之前:

  • Builder 已在 builder 模块中注册(providers + exports)
  • Validator 已在 validators 模块中注册(providers + exports)
  • 所有 3 个 action handlers 已在 action handlers 模块中注册(providers)
  • 已创建服务层
  • 服务返回扁平实体(不是 DTO)
  • 已创建解析器层
  • 解析器使用异常拦截器
  • 解析器将扁平实体 → DTO
  • 已创建扁平实体到 DTO 的转换工具

下一步

集成完成后,继续执行(强制): Syncable Entity:集成测试(第 6/6 步)

完整工作流请参阅 @creating-syncable-entity 规则。

评论

支持 Markdown