syncable-entity-runner-and-actions について
Twenty で同期可能なエンティティの移行アクションハンドラーを実装する開発スキルです。ユニバーサルアクションの変換と、作成・更新・削除の処理を扱います。
主な機能
- 01作成・更新・削除ハンドラー
- 02ユニバーサルからフラットへの変換
- 03メタデータとワークスペーススキーマの実行
- 04リレーション識別子の解決
活用シーン
- 01新しいエンティティの移行ハンドラー実装
- 02更新時のユニバーサルなリレーション識別子のマッピング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ twentyhq/ twenty --skill syncable-entity-runner-and-actionsインストール後、Claude Code で/syncable-entity-runner-and-actionsを入力して読み込まれたか確認します。
詳細
同期可能エンティティ: ランナーとアクション (ステップ 4/6)
目的: ユニバーサルエンティティからフラットエンティティへの適切なトランスパイルを行いながら、データベースに対してマイグレーションアクションを実行します。
使用タイミング: ステップ 1〜3 (型、キャッシュ、ビルダー) を完了した後。統合の前に必要です。
クイックスタート
このステップでは以下を作成します:
- 作成アクションハンドラー
- 更新アクションハンドラー
- 削除アクションハンドラー
- ユニバーサルからフラットへの変換ユーティリティ
主要なパターン: 各ハンドラーには 2 つのフェーズがあります:
- トランスパイル: ユニバーサルアクション → フラットアクション
- 実行: フラットアクション → データベース操作
ステップ 1: 作成アクションハンドラー
ファイル: src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceCreateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-create-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
import {
type UniversalCreateMyEntityAction,
type FlatCreateMyEntityAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type';
@Injectable()
export class CreateMyEntityActionHandlerService extends WorkspaceCreateActionHandlerService<
'myEntity',
UniversalCreateMyEntityAction,
FlatCreateMyEntityAction
> {
constructor(
@InjectRepository(MyEntityEntity, 'metadata')
private readonly myEntityRepository: Repository<MyEntityEntity>,
) {
super();
}
// Phase 1: Transpile universal action to flat action
protected transpileUniversalActionToFlatAction(
universalAction: UniversalCreateMyEntityAction,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatCreateMyEntityAction {
return {
type: 'create',
metadataName: 'myEntity',
flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
universalAction.universalFlatEntity,
flatEntityMaps,
),
};
}
// Phase 2: Execute flat action against database
protected async executeForMetadata(
flatActions: FlatCreateMyEntityAction[],
): Promise<void> {
const flatEntities = flatActions.map((action) => action.flatEntity);
await this.insertFlatEntitiesInRepository({
repository: this.myEntityRepository,
flatEntities,
});
}
protected async executeForWorkspaceSchema(): Promise<void> {
// No workspace schema changes needed for metadata-only entity
return;
}
}
主要なヘルパーメソッド:
transpileUniversalActionToFlatAction: ユニバーサル → フラットに変換しますinsertFlatEntitiesInRepository: 挿入用の基底クラスヘルパーexecuteForMetadata: メタデータデータベース操作executeForWorkspaceSchema: ワークスペーススキーマの変更 (必要な場合)
ステップ 2: 更新アクションハンドラー
ファイル: src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceUpdateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-update-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
@Injectable()
export class UpdateMyEntityActionHandlerService extends WorkspaceUpdateActionHandlerService<
'myEntity',
UniversalUpdateMyEntityAction,
FlatUpdateMyEntityAction
> {
constructor(
@InjectRepository(MyEntityEntity, 'metadata')
private readonly myEntityRepository: Repository<MyEntityEntity>,
) {
super();
}
protected transpileUniversalActionToFlatAction(
universalAction: UniversalUpdateMyEntityAction,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatUpdateMyEntityAction {
const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
universalAction.universalFlatEntity,
flatEntityMaps,
);
// Resolve universal foreign keys in updates to regular IDs
const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'myEntity',
universalUpdates: universalAction.universalUpdates,
flatEntityMaps,
});
return {
type: 'update',
metadataName: 'myEntity',
flatEntity,
updates: flatUpdates,
};
}
protected async executeForMetadata(
flatActions: FlatUpdateMyEntityAction[],
): Promise<void> {
for (const action of flatActions) {
await this.myEntityRepository.update(
{ id: action.flatEntity.id },
action.updates,
);
}
}
protected async executeForWorkspaceSchema(): Promise<void> {
return;
}
}
更新専用のヘルパー:
resolveUniversalUpdateRelationIdentifiersToIds: updates オブジェクト内のユニバーサル識別子を通常の ID にマッピングし直します
ステップ 3: 削除アクションハンドラー
ファイル: src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceDeleteActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-delete-action-handler.service';
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
@Injectable()
export class DeleteMyEntityActionHandlerService extends WorkspaceDeleteActionHandlerService<
'myEntity',
UniversalDeleteMyEntityAction,
FlatDeleteMyEntityAction
> {
constructor(
@InjectRepository(MyEntityEntity, 'metadata')
private readonly myEntityRepository: Repository<MyEntityEntity>,
) {
super();
}
protected transpileUniversalActionToFlatAction(
universalAction: UniversalDeleteMyEntityAction,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatDeleteMyEntityAction {
// Use base class helper for delete transpilation
return this.transpileUniversalDeleteActionToFlatDeleteAction({
universalAction,
flatEntityMaps,
fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
});
}
protected async executeForMetadata(
flatActions: FlatDeleteMyEntityAction[],
): Promise<void> {
const ids = flatActions.map((action) => action.flatEntity.id);
await this.myEntityRepository.delete(ids);
}
protected async executeForWorkspaceSchema(): Promise<void> {
return;
}
}
削除専用のヘルパー:
transpileUniversalDeleteActionToFlatDeleteAction: 標準的な削除のトランスパイルを処理する基底クラスヘルパー
ステップ 4: ユニバーサルからフラットへの変換
ファイル: src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util.ts
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';
export const fromUniversalFlatMyEntityToFlatMyEntity = (
universalFlatMyEntity: UniversalFlatMyEntity,
flatEntityMaps: AllFlatEntityMapsByMetadataName,
): FlatMyEntity => {
// Resolve universal foreign keys back to regular IDs
return resolveUniversalRelationIdentifiersToIds({
metadataName: 'myEntity',
universalFlatEntity: universalFlatMyEntity,
flatEntityMaps,
}) as FlatMyEntity;
};
主要なユーティリティ:
resolveUniversalRelationIdentifiersToIds: ユニバーサル識別子 → 通常の ID にマッピングします (resolveEntityRelationUniversalIdentifiersの逆)
アクションハンドラーのパターン
パターン: 作成ハンドラー
// 1. Transpile: Universal → Flat
protected transpileUniversalActionToFlatAction(
universalAction,
flatEntityMaps,
) {
return {
type: 'create',
metadataName: 'myEntity',
flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
universalAction.universalFlatEntity,
flatEntityMaps,
),
};
}
// 2. Execute: Flat → Database
protected async executeForMetadata(flatActions) {
await this.insertFlatEntitiesInRepository({
repository: this.myEntityRepository,
flatEntities: flatActions.map(a => a.flatEntity),
});
}
パターン: 更新ハンドラー
// Transpile with update-specific resolution
protected transpileUniversalActionToFlatAction(
universalAction,
flatEntityMaps,
) {
const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
universalAction.universalFlatEntity,
flatEntityMaps,
);
const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'myEntity',
universalUpdates: universalAction.universalUpdates,
flatEntityMaps,
});
return { type: 'update', metadataName: 'myEntity', flatEntity, updates: flatUpdates };
}
パターン: 削除ハンドラー
// Use base class helper
protected transpileUniversalActionToFlatAction(
universalAction,
flatEntityMaps,
) {
return this.transpileUniversalDeleteActionToFlatDeleteAction({
universalAction,
flatEntityMaps,
fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
});
}
// Delete
protected async executeForMetadata(flatActions) {
const ids = flatActions.map(a => a.flatEntity.id);
await this.myEntityRepository.delete(ids);
}
チェックリスト
ステップ 5 に進む前に:
- 作成アクションハンドラーを実装済み
- 更新アクションハンドラーを実装済み
- 削除アクションハンドラーを実装済み
- すべてのハンドラーが適切な基底クラスを継承している
- すべてのハンドラーで
transpileUniversalActionToFlatActionを実装済み - すべてのハンドラーで
executeForMetadataを実装済み -
executeForWorkspaceSchemaを実装済み (または空を返す) - ユニバーサルからフラットへの変換ユーティリティを作成済み
- 作成ハンドラーが
insertFlatEntitiesInRepositoryを使用している - 更新ハンドラーが
resolveUniversalUpdateRelationIdentifiersToIdsを使用している - 削除ハンドラーが
transpileUniversalDeleteActionToFlatDeleteActionを使用している - 削除ハンドラーがハード削除 (
delete()) を使用している
次のステップ
アクションハンドラーが完成したら、次に進みます: 同期可能エンティティ: 統合 (ステップ 5/6)
ワークフロー全体については、@creating-syncable-entity ルールを参照してください。