mcpField
S
syncable-entity-cache-and-transform

syncable-entity-cache-and-transform

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

syncable-entity-cache-and-transform について

Twenty の同期可能なエンティティにキャッシュと変換レイヤーを実装するための手順を示すスキルです。フラットなエンティティマップのキャッシュ、エンティティ変換、入力からユニバーサルなフラットエンティティへの変換を扱い、検証やアクションハンドラーの実装に先立って利用します。

主な機能

  1. 01ワークスペースキャッシュ付きエンティティマップの作成
  2. 02エンティティのフラット表現への変換
  3. 03作成・更新入力の変換
  4. 04関連をユニバーサル識別子に解決
  5. 05文字列プロパティのサニタイズ

活用シーン

  1. 01新しいエンティティのキャッシュ層実装
  2. 02エンティティレコードのフラットマップ化
  3. 03ワークスペース移行用入力の準備
  4. 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 (型と定数) の完了後。バリデータとアクションハンドラを構築する前に必要です。


クイックスタート

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

  1. フラットなエンティティマップ用のキャッシュサービス
  2. エンティティからフラットへの変換ユーティリティ
  3. 入力変換ユーティリティ (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,
  };
};

主要なステップ:

  1. ID を生成する (id と universalIdentifier を v4() で)
  2. サニタイズの前に外部キーを抽出する
  3. すべての文字列プロパティをサニタイズする
  4. ベースのフラットエンティティを構築する
  5. 外部キーを解決してユニバーサル識別子にする

ステップ 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 ルールを参照してください。

コメント