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.
67
81%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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.
This step creates:
Key principle: Input transform utils must output universal flat entities (with universalIdentifier and foreign keys mapped to universal identifiers).
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:
@WorkspaceCache decorator with unique flatMapsKeywithDeleted: true to include soft-deleted entitiesflat{EntityName}Maps (camelCase)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)
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:
id and universalIdentifier with v4())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:
'metadata' datasource// 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,
});// 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;
};Before moving to Step 3:
@WorkspaceCache decoratorwithDeleted: trueflat{EntityName}Maps patternuniversalIdentifier set correctly (generated with v4())Once cache and transform utilities are complete, proceed to: Syncable Entity: Builder & Validation (Step 3/6)
For complete workflow, see @creating-syncable-entity rule.
bb4e427
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.