본문으로 건너뛰기
cccskills
무료GitHub에 공개

syncable-entity-runner-and-actions

Implement database action handlers for syncable entities in Twenty server internals. It covers create, update, and delete operations, with conversion from universal identifiers to database IDs before each action is executed.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

원문원문 설명 보기

Contributing to the Twenty codebase itself (twentyhq/twenty server internals), not for building apps on top of Twenty. Implement action handlers for executing workspace migrations in Twenty. Use when creating database operations for syncable entities, implementing universal-to-flat entity transpilation, or handling create/update/delete actions in the runner layer.

설치 방법 보기

이럴 때 유용합니다

  • Implement create, update, and delete handlers for a new entity
  • Convert universal relation identifiers into database IDs
  • Check runner implementation before the integration step

포함된 파일(1)

  • SKILL.md12.2 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Syncable Entity: Runner & Actions (Step 4/6)

Purpose: Execute migration actions against the database with proper transpilation from universal to flat entities.

When to use: After completing Steps 1-3 (Types, Cache, Builder). Required before integration.


Quick Start

This step creates:

  1. Create action handler
  2. Update action handler
  3. Delete action handler
  4. Universal-to-flat conversion utilities

Key pattern: Each handler has two phases:

  1. Transpilation: Universal action → Flat action
  2. Execution: Flat action → Database operation

Step 1: Create Action Handler

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

Key helper methods:

  • transpileUniversalActionToFlatAction: Converts universal → flat
  • insertFlatEntitiesInRepository: Base class helper for inserts
  • executeForMetadata: Metadata database operations
  • executeForWorkspaceSchema: Workspace schema changes (if needed)

Step 2: Update Action Handler

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

Update-specific helper:

  • resolveUniversalUpdateRelationIdentifiersToIds: Maps universal identifiers back to regular IDs in the updates object

Step 3: Delete Action Handler

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

Delete-specific helper:

  • transpileUniversalDeleteActionToFlatDeleteAction: Base class helper that handles standard delete transpilation

Step 4: Universal-to-Flat Conversion

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

Key utility:

  • resolveUniversalRelationIdentifiersToIds: Maps universal identifiers → regular IDs (reverse of resolveEntityRelationUniversalIdentifiers)

Action Handler Patterns

Pattern: Create Handler

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

Pattern: Update Handler

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

Pattern: Delete Handler

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

Checklist

Before moving to Step 5:

  • Create action handler implemented
  • Update action handler implemented
  • Delete action handler implemented
  • All handlers extend appropriate base class
  • transpileUniversalActionToFlatAction implemented in all handlers
  • executeForMetadata implemented in all handlers
  • executeForWorkspaceSchema implemented (or returns empty)
  • Universal-to-flat conversion utility created
  • Create handler uses insertFlatEntitiesInRepository
  • Update handler uses resolveUniversalUpdateRelationIdentifiersToIds
  • Delete handler uses transpileUniversalDeleteActionToFlatDeleteAction
  • Delete handler uses hard delete (delete())

Next Step

Once action handlers are complete, proceed to: Syncable Entity: Integration (Step 5/6)

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

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Create a new app that extends a running Twenty instance. Guide project naming and instance selection, then use the scaffolder to install dependencies, authenticate, initialize Git, and complete the initial sync and installation.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Start a new Twenty app project
  • Scaffold a CRM extension or integration
  • Create an app connected to an existing Twenty instance
twentyhq/twenty5.9만2026년 10월 19일 업데이트

Extend an existing Twenty app with data fields, layouts, custom screens, automated logic, workflows, and permissions. The skill guides code organization, entity creation, type checking, linting, and syncing definitions to the active remote.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Add custom fields and objects to an existing Twenty app
  • Build custom screens and standalone pages
  • Run logic when records are created
twentyhq/twenty5.9만2026년 10월 19일 업데이트

Operate an existing Twenty app by syncing changes, managing server connections, building and deploying releases, and inspecting function logs. It also guides troubleshooting and CI/CD, the workflows used to automate testing and deployment.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Sync app changes to a Twenty server
  • Prepare and deploy an app update
  • Investigate failed builds or deployments
twentyhq/twenty5.9만2026년 10월 19일 업데이트

Prepare an existing Twenty app for npm or marketplace publication by reviewing its README, listing details, package metadata, logos, and screenshots. The workflow includes checking publication settings, validating the build, and running the publishing command.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Prepare a Twenty app marketplace listing
  • Write or review README and About text
  • Check logos, screenshots, and public assets
twentyhq/twenty5.9만2026년 10월 19일 업데이트

qa-scout

무료

Check a Twenty pull request through real browser scenarios derived from its changes. Verify database effects and server logs alongside visible results, then produce screenshots, a structured verdict, and a report of any failures or uncertainty.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Validate user flows before or after merging a Twenty pull request
  • Check database writes and timeline activity after saving records
  • Investigate backend errors hidden behind a successful screen
twentyhq/twenty5.9만2026년 10월 19일 업데이트

Implement validation and migration action builders for syncable entities, data types handled by Twenty's synchronization system. This skill covers required fields, uniqueness, reference checks, and orchestration wiring, with validators that return errors without throwing exceptions or changing data.

한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.

  • Add required-field and duplicate checks
  • Validate references to parent entities
  • Build create, update, and delete migration actions
twentyhq/twenty5.9만2026년 10월 19일 업데이트

twentyhq의 스킬 모두 보기

이 스킬의 문제 신고