syncable-entity-types-and-constants
Syncable Entity: Types & Constants (Step 1/6)
Purpose: Define all types, entities, and register in central constants. This is the foundation - everything else depends on these types being correct.
When to use: First step when creating any new syncable entity. Must be completed before other steps.
Quick Start
This step creates:
- Metadata name constant (twenty-shared)
- TypeORM entity (extends
SyncableEntity) - Flat entity types
- Action types (universal + flat)
- Central constant registrations (5 constants)
Step 1: Add Metadata Name
File: packages/twenty-shared/src/metadata/all-metadata-name.constant.ts
export const ALL_METADATA_NAME = {
// ... existing entries
myEntity: 'myEntity',
} as const;
Step 2: Create TypeORM Entity
File: src/engine/metadata-modules/my-entity/entities/my-entity.entity.ts
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@Entity({ name: 'myEntity' })
export class MyEntityEntity extends SyncableEntity {
@Column({ type: 'varchar' })
name: string;
@Column({ type: 'varchar' })
label: string;
@Column({ type: 'boolean', default: true })
isCustom: boolean;
// Foreign key example (optional)
@Column({ type: 'uuid', nullable: true })
parentEntityId: string | null;
@ManyToOne(() => ParentEntityEntity, { nullable: true })
@JoinColumn({ name: 'parentEntityId' })
parentEntity: ParentEntityEntity | null;
// JSONB column example (optional)
@Column({ type: 'jsonb', nullable: true })
settings: Record<string, any> | null;
}
Key rules:
- Must extend
SyncableEntity(providesid,universalIdentifier,applicationId, etc.) - Must have
isCustomboolean column - Use
@Column({ type: 'jsonb' })for JSON data
Step 3: Define Flat Entity Types
File: src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type.ts
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
export type FlatMyEntity = FlatEntityFrom<MyEntityEntity>;
Maps file (if entity has indexed lookups):
// flat-my-entity-maps.type.ts
export type FlatMyEntityMaps = {
byId: Record<string, FlatMyEntity>;
byName: Record<string, FlatMyEntity>;
// Add other indexes as needed
};
Step 4: Define Editable Properties
File: src/engine/metadata-modules/flat-my-entity/constants/editable-flat-my-entity-properties.constant.ts
export const EDITABLE_FLAT_MY_ENTITY_PROPERTIES = [
'name',
'label',
'description',
'parentEntityId',
'settings',
] as const satisfies ReadonlyArray<keyof FlatMyEntity>;
Rule: Only include properties that can be updated (exclude id, createdAt, universalIdentifier, etc.)
Step 5: Define Action Types
File: src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type.ts
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
// Universal actions (used by builder/runner)
export type UniversalCreateMyEntityAction = {
type: 'create';
metadataName: 'myEntity';
universalFlatEntity: UniversalFlatMyEntity;
};
export type UniversalUpdateMyEntityAction = {
type: 'update';
metadataName: 'myEntity';
universalFlatEntity: UniversalFlatMyEntity;
universalUpdates: Partial<UniversalFlatMyEntity>;
};
export type UniversalDeleteMyEntityAction = {
type: 'delete';
metadataName: 'myEntity';
universalFlatEntity: UniversalFlatMyEntity;
};
// Flat actions (internal to runner)
export type FlatCreateMyEntityAction = {
type: 'create';
metadataName: 'myEntity';
flatEntity: FlatMyEntity;
};
export type FlatUpdateMyEntityAction = {
type: 'update';
metadataName: 'myEntity';
flatEntity: FlatMyEntity;
updates: Partial<FlatMyEntity>;
};
export type FlatDeleteMyEntityAction = {
type: 'delete';
metadataName: 'myEntity';
flatEntity: FlatMyEntity;
};
Step 6: Register in Central Constants
6a. AllFlatEntityTypesByMetadataName
File: src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts
export type AllFlatEntityTypesByMetadataName = {
// ... existing entries
myEntity: {
flatEntityMaps: FlatMyEntityMaps;
universalActions: {
create: UniversalCreateMyEntityAction;
update: UniversalUpdateMyEntityAction;
delete: UniversalDeleteMyEntityAction;
};
flatActions: {
create: FlatCreateMyEntityAction;
update: FlatUpdateMyEntityAction;
delete: FlatDeleteMyEntityAction;
};
flatEntity: FlatMyEntity;
universalFlatEntity: UniversalFlatMyEntity;
entity: MyEntityEntity;
};
};
6b. ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME
File: src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts
export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
// ... existing entries
myEntity: {
name: { toCompare: true },
label: { toCompare: true },
description: { toCompare: true },
parentEntityId: {
toCompare: true,
universalProperty: 'parentEntityUniversalIdentifier',
},
settings: {
toCompare: true,
toStringify: true,
universalProperty: 'universalSettings',
},
},
} as const;
Rules:
toCompare: true→ Editable property (checked for changes)toStringify: true→ JSONB/object property (needs JSON serialization)universalProperty→ Maps to universal version (for foreign keys & JSONB withSerializedRelation)
6c. ALL_ONE_TO_MANY_METADATA_RELATIONS
File: src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts
This constant is type-checked — values for metadataName, flatEntityForeignKeyAggregator, and universalFlatEntityForeignKeyAggregator are derived from entity type definitions. The aggregator names follow the pattern: remove trailing 's' from the relation property name, then append Ids or UniversalIdentifiers.
export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
// ... existing entries
myEntity: {
// If myEntity has a `childEntities: ChildEntityEntity[]` property:
childEntities: {
metadataName: 'childEntity',
flatEntityForeignKeyAggregator: 'childEntityIds',
universalFlatEntityForeignKeyAggregator: 'childEntityUniversalIdentifiers',
},
// null for relations to non-syncable entities
someNonSyncableRelation: null,
},
} as const;
6d. ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY
File: src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts
Low-level primitive constant. Only contains foreignKey — the column name ending in Id that stores the foreign key. Type-checked against entity properties.
export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
// ... existing entries
myEntity: {
workspace: null,
application: null,
parentEntity: {
foreignKey: 'parentEntityId',
},
},
} as const;
6e. ALL_MANY_TO_ONE_METADATA_RELATIONS
File: src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts
Derived from both ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY (for foreignKey type and universalForeignKey derivation) and ALL_ONE_TO_MANY_METADATA_RELATIONS (for inverseOneToManyProperty key constraint). This is the main constant consumed by utils and optimistic tooling.
export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
// ... existing entries
myEntity: {
workspace: null,
application: null,
parentEntity: {
metadataName: 'parentEntity',
foreignKey: 'parentEntityId',
inverseOneToManyProperty: 'myEntities', // key in ALL_ONE_TO_MANY_METADATA_RELATIONS['parentEntity'], or null if no inverse
isNullable: false,
universalForeignKey: 'parentEntityUniversalIdentifier',
},
},
} as const;
Derivation dependency graph:
ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY ALL_ONE_TO_MANY_METADATA_RELATIONS
(foreignKey only) (metadataName, aggregators)
│ │
│ FK type + universalFK derivation │ inverseOneToManyProperty keys
│ │
└────────────────┬───────────────────────┘
▼
ALL_MANY_TO_ONE_METADATA_RELATIONS
(metadataName, foreignKey, inverseOneToManyProperty,
isNullable, universalForeignKey)
Rules:
workspace: null,application: null— always present, always null (non-syncable relations)inverseOneToManyProperty— must be a key inALL_ONE_TO_MANY_METADATA_RELATIONS[targetMetadataName], ornullif the target entity doesn't expose an inverse one-to-many relationuniversalForeignKey— derived fromforeignKeyby replacing theIdsuffix withUniversalIdentifier- Optimistic utils resolve
flatEntityForeignKeyAggregator/universalFlatEntityForeignKeyAggregatorat runtime by looking upinverseOneToManyPropertyinALL_ONE_TO_MANY_METADATA_RELATIONS
Checklist
Before moving to Step 2:
- Metadata name added to
ALL_METADATA_NAME - TypeORM entity created (extends
SyncableEntity) -
isCustomcolumn added - Flat entity type defined
- Flat entity maps type defined (if needed)
- Editable properties constant defined
- Universal and flat action types defined
- Registered in
AllFlatEntityTypesByMetadataName - Registered in
ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME - Registered in
ALL_ONE_TO_MANY_METADATA_RELATIONS(if entity has one-to-many relations) - Registered in
ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY - Registered in
ALL_MANY_TO_ONE_METADATA_RELATIONS - TypeScript compiles without errors
Next Step
Once all types and constants are defined, proceed to: Syncable Entity: Cache & Transform (Step 2/6)
For complete workflow, see @creating-syncable-entity rule.