关于 syncable-entity-cache-and-transform
用于实现 Twenty 可同步实体的缓存与转换层,按步骤指导开发者缓存扁平实体映射、转换实体,并将输入处理为通用扁平实体,为后续验证和操作处理程序做准备。
主要功能
- 01创建工作区缓存的实体映射
- 02将实体转换为扁平表示
- 03转换创建和更新输入
- 04将关联解析为通用标识符
- 05清理字符串属性
适用场景
- 01为新实体实现缓存层
- 02将实体记录转换为扁平映射
- 03为工作区迁移准备输入
- 04在转换期间解析外键
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ twentyhq/ twenty --skill syncable-entity-cache-and-transform安装后在 Claude Code 中输入/syncable-entity-cache-and-transform确认已加载。
详细介绍
可同步实体:缓存与转换(第 2/6 步)
用途:创建缓存层与转换工具,用于在不同实体表示形式之间转换。
使用时机:完成第 1 步(类型与常量)之后。在构建校验器和操作处理器之前必需。
快速开始
本步骤创建:
- 用于扁平实体映射的缓存服务
- 实体到扁平的转换工具
- 输入转换工具(DTO → 通用扁平实体)
核心原则:输入转换工具必须输出通用扁平实体(带有 universalIdentifier,且外键映射为通用标识符)。
第 1 步:创建缓存服务
文件:src/engine/metadata-modules/flat-my-entity/services/flat-my-entity-cache.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { WorkspaceCache } from 'src/engine/twenty-orm/decorators/workspace-cache.decorator';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { type FlatMyEntityMaps } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity-maps.type';
import { fromMyEntityEntityToFlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/utils/from-my-entity-entity-to-flat-my-entity.util';
@Injectable()
export class FlatMyEntityCacheService {
constructor(
@InjectRepository(MyEntityEntity, 'metadata')
private readonly myEntityRepository: Repository<MyEntityEntity>,
) {}
@WorkspaceCache({ flatMapsKey: 'flatMyEntityMaps' })
async getFlatMyEntityMaps(): Promise<FlatMyEntityMaps> {
const myEntities = await this.myEntityRepository.find({
withDeleted: true, // CRITICAL: Include soft-deleted entities
});
const flatMyEntities = myEntities.map((entity) =>
fromMyEntityEntityToFlatMyEntity(entity),
);
return {
byId: Object.fromEntries(flatMyEntities.map((e) => [e.id, e])),
byName: Object.fromEntries(flatMyEntities.map((e) => [e.name, e])),
};
}
}
关键规则:
- 使用带有唯一
flatMapsKey的@WorkspaceCache装饰器 - 始终使用
withDeleted: true以包含软删除的实体 - 缓存键模式:
flat{EntityName}Maps(camelCase)
第 2 步:实体到扁平的转换
文件:src/engine/metadata-modules/flat-my-entity/utils/from-my-entity-entity-to-flat-my-entity.util.ts
import { v4 } from 'uuid';
import { type MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
export const fromMyEntityEntityToFlatMyEntity = (
entity: MyEntityEntity,
): FlatMyEntity => {
return {
id: entity.id,
// Critical: generate a new UUID for universalIdentifier
universalIdentifier: v4(),
workspaceId: entity.workspaceId,
applicationId: entity.applicationId,
name: entity.name,
label: entity.label,
description: entity.description,
isCustom: entity.isCustom,
parentEntityId: entity.parentEntityId,
settings: entity.settings,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
deletedAt: entity.deletedAt?.toISOString() ?? null,
};
};
关键:universalIdentifier 必须是用 v4() 生成的新 UUID(而不是 entity.id)
第 3 步:输入转换工具(DTO → 通用扁平实体)
文件:src/engine/metadata-modules/flat-my-entity/utils/from-create-my-entity-input-to-universal-flat-my-entity.util.ts
import { v4 } from 'uuid';
import { sanitizeString } from 'twenty-shared/string';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';
export const fromCreateMyEntityInputToUniversalFlatMyEntity = ({
input,
workspaceId,
flatEntityMaps,
}: {
input: CreateMyEntityInput;
workspaceId: string;
flatEntityMaps?: AllFlatEntityMapsByMetadataName;
}): UniversalFlatMyEntity => {
const id = v4();
const universalIdentifier = v4();
// 1. Extract foreign key IDs BEFORE sanitization
const parentEntityId = input.parentEntityId ?? null;
// 2. Sanitize string properties
const name = sanitizeString(input.name);
const label = sanitizeString(input.label);
const description = input.description ? sanitizeString(input.description) : null;
// 3. Build base flat entity
const baseFlatEntity = {
id,
universalIdentifier,
workspaceId,
applicationId: null,
name,
label,
description,
isCustom: true,
parentEntityId,
settings: input.settings ?? null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
// 4. Resolve foreign keys to universal identifiers (if flatEntityMaps provided)
if (flatEntityMaps) {
return resolveEntityRelationUniversalIdentifiers({
metadataName: 'myEntity',
flatEntity: baseFlatEntity,
flatEntityMaps,
});
}
// 5. Return with null universal foreign keys if no maps
return {
...baseFlatEntity,
parentEntityUniversalIdentifier: null,
};
};
关键步骤:
- 生成 ID(
id和universalIdentifier使用v4()) - 在清理之前提取外键
- 清理所有字符串属性
- 构建基础扁平实体
- 将外键解析为通用标识符
第 4 步:创建扁平实体模块
文件:src/engine/metadata-modules/flat-my-entity/flat-my-entity.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { FlatMyEntityCacheService } from 'src/engine/metadata-modules/flat-my-entity/services/flat-my-entity-cache.service';
@Module({
imports: [TypeOrmModule.forFeature([MyEntityEntity], 'metadata')],
providers: [FlatMyEntityCacheService],
exports: [FlatMyEntityCacheService],
})
export class FlatMyEntityModule {}
规则:
- 使用
'metadata'数据源导入实体 - 导出缓存服务以供其他模块使用
常见模式
模式:外键解析
// Extract foreign keys BEFORE sanitization
const parentEntityId = input.parentEntityId ?? null;
// After building base entity, resolve to universal identifiers
const universalFlatEntity = resolveEntityRelationUniversalIdentifiers({
metadataName: 'myEntity',
flatEntity: baseFlatEntity,
flatEntityMaps,
});
模式:使用 SerializedRelation 的 JSONB
// For JSONB properties containing foreign keys
const settings = input.settings
? {
...input.settings,
fieldMetadataId: input.settings.fieldMetadataId,
}
: null;
// After resolution, JSONB foreign keys become universal identifiers
return resolveEntityRelationUniversalIdentifiers({
metadataName: 'myEntity',
flatEntity: { ...baseFlatEntity, settings },
flatEntityMaps,
});
模式:更新转换
// from-update-my-entity-input-to-universal-flat-my-entity-updates.util.ts
export const fromUpdateMyEntityInputToUniversalFlatMyEntityUpdates = ({
input,
flatEntityMaps,
}: {
input: UpdateMyEntityInput;
flatEntityMaps?: AllFlatEntityMapsByMetadataName;
}): Partial<UniversalFlatMyEntity> => {
const updates: Partial<UniversalFlatMyEntity> = {};
if (input.name !== undefined) {
updates.name = sanitizeString(input.name);
}
if (input.parentEntityId !== undefined) {
updates.parentEntityId = input.parentEntityId;
}
updates.updatedAt = new Date().toISOString();
// Resolve foreign keys if maps provided
if (flatEntityMaps) {
return resolveEntityRelationUniversalIdentifiers({
metadataName: 'myEntity',
flatEntity: updates as any,
flatEntityMaps,
});
}
return updates;
};
检查清单
在进入第 3 步之前:
- 已使用
@WorkspaceCache装饰器创建缓存服务 - 缓存使用
withDeleted: true - 缓存键遵循
flat{EntityName}Maps模式 - 已实现实体到扁平的转换
-
universalIdentifier设置正确(使用v4()生成) - 已实现创建输入转换
- 已实现更新输入转换(如需要)
- 在清理之前提取外键
- 字符串属性已清理
- 外键已解析为通用标识符
- 已创建扁平实体模块并导出缓存服务
下一步
缓存与转换工具完成后,请继续: 可同步实体:构建器与校验(第 3/6 步)
完整工作流请参见 @creating-syncable-entity 规则。