Scalar and universal flat entity transpilers (#17891)

# Introduction

## Use `flatEntityTranspilers.toScalarFlatEntity` in create action
handler

**Changes:**
- Modified
`BaseWorkspaceMigrationRunnerActionHandlerService.insertFlatEntitiesInRepository()`
to transform flat entities using `toScalarFlatEntity()` before database
insertion

**What it does:**
Strips out TypeORM relation objects and metadata-only properties,
ensuring only scalar values (primitives, IDs, dates) are inserted into
the database.

**Benefits:**
- **Type Safety:** Prevents accidental insertion of nested objects that
TypeORM can't persist
- **Consistency:** All 17+ create action handlers automatically benefit
from proper data transformation
- **Single Source of Truth:** Centralized logic for what constitutes a
database-insertable entity
- **Prevents Errors:** Uses entity configuration schema to ensure only
valid properties are included

## Usage
```ts
  protected async insertFlatEntitiesInRepository({
    flatEntities,
    queryRunner,
  }: {
    queryRunner: QueryRunner;
    flatEntities: MetadataFlatEntity<TMetadataName>[];
  }) {
    const metadataEntity =
      ALL_METADATA_ENTITY_BY_METADATA_NAME[this.metadataName];
    const repository = queryRunner.manager.getRepository(metadataEntity);
    const scalarFlatEntities = flatEntities.map((flatEntity) =>
      flatEntityTranspilers.toScalarFlatEntity({
        flatEntity,
        metadataName: this.metadataName,
      }),
    );

    await repository.insert(scalarFlatEntities);
  }
```

## Upcoming refactor
About to completely split the
`packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface.ts`
into three dedicated boilerplate one for each action type `create`
`delete` `update` will provide a better interfacing and typing + will
allow not requiring the user to provide the metadata execute handler as
required
This commit is contained in:
Paul Rastoin
2026-02-12 18:06:55 +01:00
committed by GitHub
parent 8ffc554c9a
commit f6b7ab2251
37 changed files with 1400 additions and 428 deletions
@@ -1,7 +1,10 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Equal, type Expect } from 'twenty-shared/testing';
import { type ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
import {
type ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME,
type MetadataEntityComparablePropertyName,
} from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
type ExtractPropertyToCompare<
@@ -14,9 +17,12 @@ type ExtractPropertyToCompare<
export type MetadataUniversalFlatEntityPropertiesToCompare<
T extends AllMetadataName,
MetadataConfig = (typeof ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME)[T],
TComparedKeys extends
keyof MetadataConfig = MetadataEntityComparablePropertyName<T> &
keyof MetadataConfig,
> = {
[P in keyof MetadataConfig]: ExtractPropertyToCompare<MetadataConfig, P>;
}[keyof MetadataConfig] &
[P in TComparedKeys]: ExtractPropertyToCompare<MetadataConfig, P>;
}[TComparedKeys] &
keyof MetadataUniversalFlatEntity<T>;
// eslint-disable-next-line unused-imports/no-unused-vars
@@ -2,12 +2,14 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
type ExtractKeysWithToStringify<T> = {
[K in keyof T]: T[K] extends { toStringify: true } ? K : never;
type ExtractComparableKeysWithToStringify<T> = {
[K in keyof T]: T[K] extends { toCompare: true; toStringify: true }
? K
: never;
}[keyof T];
export type MetadataUniversalFlatEntityPropertiesToStringify<
T extends AllMetadataName,
> = ExtractKeysWithToStringify<
> = ExtractComparableKeysWithToStringify<
(typeof ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME)[T]
>;
@@ -3,7 +3,7 @@ import { type IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
// Note: IndexFieldMetadataEntity is just in between a SyncableEntity and a jsonb we should decide one of both
// Note: IndexFieldMetadataEntity is just in between a SyncableEntity and a jsonb we should decide one of both https://github.com/twentyhq/core-team-issues/issues/2227
export type UniversalFlatIndexFieldMetadata = Omit<
IndexFieldMetadataEntity,
| 'indexMetadataId'
@@ -22,7 +22,7 @@ export function transformUniversalFlatEntityForComparison<
if (
propertiesToStringify.includes(
propertyToCompare as MetadataUniversalFlatEntityPropertiesToStringify<T>,
propertyToCompare as string as MetadataUniversalFlatEntityPropertiesToStringify<T>,
)
) {
const orderedValue = orderObjectProperties(currentValue);