关于 syncable-entity-integration
面向在 NestJS 中添加可同步实体的集成步骤指南,供完成前置步骤的开发者使用。说明模块注册、服务与解析器分层,以及 GraphQL 异常处理。
主要功能
- 01注册构建器、验证器和操作处理器
- 02创建返回扁平实体的服务层
- 03将扁平实体转换为 DTO
- 04通过拦截器格式化 GraphQL 异常
适用场景
- 01将新实体接入工作区迁移模块
- 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 步(所有步骤)之后。测试之前必需。
快速开始
本步骤:
- 在 3 个 NestJS 模块中注册服务
- 创建服务层(返回扁平实体)
- 创建解析器层(将扁平实体 → DTO)
- 使用异常拦截器处理 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,
});
}
}
服务模式:
- 将输入转换为通用扁平实体
- 调用
validateBuildAndRunWorkspaceMigration - 如果验证出错则抛出异常
- 返回扁平实体(不是 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 自动处理:
FlatEntityMapsException→ 转换为 GraphQL 错误(NotFoundError 等)WorkspaceMigrationBuilderException→ 使用 i18n 格式化验证错误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 规则。