mcpField
S
syncable-entity-runner-and-actions

syncable-entity-runner-and-actions

github.com/twentyhq開発60.0k55 回インストール今日更新
リポジトリ

syncable-entity-runner-and-actions について

Twenty で同期可能なエンティティの移行アクションハンドラーを実装する開発スキルです。ユニバーサルアクションの変換と、作成・更新・削除の処理を扱います。

主な機能

  1. 01作成・更新・削除ハンドラー
  2. 02ユニバーサルからフラットへの変換
  3. 03メタデータとワークスペーススキーマの実行
  4. 04リレーション識別子の解決

活用シーン

  1. 01新しいエンティティの移行ハンドラー実装
  2. 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 (型、キャッシュ、ビルダー) を完了した後。統合の前に必要です。


クイックスタート

このステップでは以下を作成します:

  1. 作成アクションハンドラー
  2. 更新アクションハンドラー
  3. 削除アクションハンドラー
  4. ユニバーサルからフラットへの変換ユーティリティ

主要なパターン: 各ハンドラーには 2 つのフェーズがあります:

  1. トランスパイル: ユニバーサルアクション → フラットアクション
  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 ルールを参照してください。

コメント