syncable-entity-cache-and-transform

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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "syncable-entity-cache-and-transform" with this command: npx skills add twentyhq/twenty/twentyhq-twenty-syncable-entity-cache-and-transform

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:

  • Cache service for flat entity maps

  • Entity-to-flat conversion utility

  • 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:

  • Generate IDs (id and universalIdentifier with v4() )

  • Extract foreign keys before sanitization

  • Sanitize all string properties

  • Build base flat entity

  • 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

syncable-entity-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

syncable-entity-types-and-constants

No summary provided by upstream source.

Repository SourceNeeds Review
General

syncable-entity-runner-and-actions

No summary provided by upstream source.

Repository SourceNeeds Review