本文へ移動
cccskills
無料GitHub で公開

syncable-entity-cache-and-transform

Contributing to the Twenty codebase itself (twentyhq/twenty server internals), not for building apps on top of Twenty. Create cache services and transformation utilities for syncable entities in Twenty. Use when implementing entity-to-flat conversions, input DTO transpilation to universal flat entities, or cache recomputation for syncable entities.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md9.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Syncable Entity: Cache & Transform (Step 2/6)

Purpose: Create cache layer and transformation utilities to convert between different entity representations.

When to use: After completing Step 1 (Types & Constants). Required before building validators and action handlers.


Quick Start

This step creates:

  1. Cache service for flat entity maps
  2. Entity-to-flat conversion utility
  3. Input transform utils (DTO → Universal Flat Entity)

Key principle: Input transform utils must output universal flat entities (with universalIdentifier and foreign keys mapped to universal identifiers).


Step 1: Create Cache Service

File: 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])),
    };
  }
}

Critical rules:

  • Use @WorkspaceCache decorator with unique flatMapsKey
  • Always use withDeleted: true to include soft-deleted entities
  • Cache key pattern: flat{EntityName}Maps (camelCase)

Step 2: Entity-to-Flat Conversion

File: 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,
  };
};

Critical: universalIdentifier must be a new UUID generated with v4() (not entity.id)


Step 3: Input Transform Utils (DTO → Universal Flat Entity)

File: 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,
  };
};

Key steps:

  1. Generate IDs (id and universalIdentifier with v4())
  2. Extract foreign keys before sanitization
  3. Sanitize all string properties
  4. Build base flat entity
  5. Resolve foreign keys → universal identifiers

Step 4: Create Flat Entity Module

File: 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 {}

Rules:

  • Import entity with 'metadata' datasource
  • Export cache service for use in other modules

Common Patterns

Pattern: Foreign Key Resolution

// 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,
});

Pattern: JSONB with SerializedRelation

// 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,
});

Pattern: Update Transform

// 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;
};

Checklist

Before moving to Step 3:

  • Cache service created with @WorkspaceCache decorator
  • Cache uses withDeleted: true
  • Cache key follows flat{EntityName}Maps pattern
  • Entity-to-flat conversion implemented
  • universalIdentifier set correctly (generated with v4())
  • Create input transform implemented
  • Update input transform implemented (if needed)
  • Foreign keys extracted before sanitization
  • String properties sanitized
  • Foreign keys resolved to universal identifiers
  • Flat entity module created and exports cache service

Next Step

Once cache and transform utilities are complete, proceed to: Syncable Entity: Builder & Validation (Step 3/6)

For complete workflow, see @creating-syncable-entity rule.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when the user wants to create or scaffold a new Twenty app

日本語の概要は準備中です。原文の説明を表示しています。

twentyhq/twenty5.8万2026年10月10日 更新

Use when the user wants to add or modify Twenty app entities, including objects, layouts, logic functions, and front components inside an existing Twenty app.

日本語の概要は準備中です。原文の説明を表示しています。

twentyhq/twenty5.8万2026年10月10日 更新

Use when the user wants to manage or troubleshoot tooling, remotes, sync, build, deploy, logs, CI/CD, or operational workflows for an existing Twenty app.

日本語の概要は準備中です。原文の説明を表示しています。

twentyhq/twenty5.8万2026年10月10日 更新

Use when the user wants to prepare or verify a Twenty app for npm or marketplace publication, including README/About copy, package metadata, defineApplication marketplace metadata, logos, screenshots, and public assets.

日本語の概要は準備中です。原文の説明を表示しています。

twentyhq/twenty5.8万2026年10月10日 更新

qa-scout

無料

Browser QA of a PR against a running Twenty app, post-merge on main or pre-merge via the qa-scout label. Derives user-visible scenarios from the PR diff, executes them with the Playwright MCP browser, attests database effects over SQL, watches server and worker logs for swallowed errors, and writes a structured verdict plus a report. Invoked by ci-e2e-main.yaml after the deterministic e2e suite; also runnable locally against a dev stack.

日本語の概要は準備中です。原文の説明を表示しています。

twentyhq/twenty5.8万2026年10月10日 更新

Contributing to the Twenty codebase itself (twentyhq/twenty server internals), not for building apps on top of Twenty. Create validation logic and migration action builders for syncable entities in Twenty. Use when implementing business rule validation, uniqueness checks, foreign key validation, or building workspace migration actions for syncable entities. Validators never throw and never mutate.

日本語の概要は準備中です。原文の説明を表示しています。

twentyhq/twenty5.8万2026年10月10日 更新

twentyhq のスキルをすべて見る

このスキルの問題を報告する