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
@@ -66,9 +66,9 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
"propertiesToCompare": [
"indexType",
"indexWhereClause",
"universalFlatIndexFieldMetadatas",
"isUnique",
"name",
"universalFlatIndexFieldMetadatas",
],
"propertiesToStringify": [
"universalFlatIndexFieldMetadatas",
@@ -4,9 +4,31 @@ import {
} from 'twenty-shared/metadata';
import { 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 MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { type MetadataUniversalFlatEntityPropertiesToCompare } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-properties-to-compare.type';
import { type MetadataUniversalFlatEntityPropertiesToStringify } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-properties-to-stringify.type';
type PropertyConfiguration = {
universalProperty: string | undefined;
toStringify: boolean;
toCompare: boolean;
};
// TODO remove once https://github.com/twentyhq/core-team-issues/issues/2227 has been resolved
const EXTRA_PROPERTIES_TO_COMPARE = {
index: {
flatIndexFieldMetadatas: {
toStringify: true,
universalProperty: 'universalFlatIndexFieldMetadatas',
toCompare: true,
},
},
} as const satisfies {
[P in AllMetadataName]?: Partial<
Record<keyof MetadataFlatEntity<P>, PropertyConfiguration>
>;
};
type UniversalFlatEntityPropertiesToCompareAndStringify<
T extends AllMetadataName,
> = {
@@ -18,18 +40,23 @@ const computeUniversalFlatEntityPropertiesToCompareAndStringify = <
>(
metadataName: T,
): UniversalFlatEntityPropertiesToCompareAndStringify<T> => {
const entries = Object.entries(
ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME[metadataName],
) as [
string,
{ universalProperty: string | undefined; toStringify: boolean },
][];
const entries = Object.entries({
...ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME[metadataName],
...EXTRA_PROPERTIES_TO_COMPARE[
metadataName as keyof typeof EXTRA_PROPERTIES_TO_COMPARE
],
}) as [string, PropertyConfiguration][];
const accumulator: UniversalFlatEntityPropertiesToCompareAndStringify<T> = {
propertiesToCompare: [],
propertiesToStringify: [],
};
for (const [property, configuration] of entries) {
if (!configuration.toCompare) {
continue;
}
const comparedProperty = configuration.universalProperty ?? property;
accumulator.propertiesToCompare.push(
@@ -1,9 +1,8 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type AddSuffixToEntityOneToManyProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type';
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
import { type ScalarFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/scalar-flat-entity.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type UniversalFlatEntityExtraProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
@@ -14,18 +13,11 @@ export type SyncableFlatEntity = Omit<
id: string;
};
type AtomicFlatEntity<TEntity> = Omit<
TEntity,
| ExtractEntityRelatedEntityProperties<TEntity>
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
>;
export type FlatEntityFrom<
TEntity,
// Required to be passed for narrowed type
TMetadataName extends AllMetadataName | undefined = undefined,
> = AtomicFlatEntity<TEntity> &
CastRecordTypeOrmDatePropertiesToString<TEntity> &
> = ScalarFlatEntity<TEntity> &
AddSuffixToEntityOneToManyProperties<TEntity, 'ids'> &
(TEntity extends SyncableEntity
? UniversalFlatEntityExtraProperties<
@@ -1,6 +1,9 @@
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';
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 MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
type IsUniversalMappedProperty<
@@ -13,15 +16,19 @@ type IsUniversalMappedProperty<
export type FlatEntityUpdate<
T extends AllMetadataName,
MetadataPropertyConfig = (typeof ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME)[T],
TComparedKeys extends
keyof MetadataPropertyConfig = MetadataEntityComparablePropertyName<T> &
keyof MetadataPropertyConfig,
> = Partial<
Pick<
MetadataFlatEntity<T>,
Extract<keyof MetadataFlatEntity<T>, keyof MetadataPropertyConfig>
Extract<keyof MetadataFlatEntity<T>, TComparedKeys>
>
> & {
[K in keyof MetadataPropertyConfig as [
never,
] extends IsUniversalMappedProperty<MetadataPropertyConfig, K>
[K in TComparedKeys as [never] extends IsUniversalMappedProperty<
MetadataPropertyConfig,
K
>
? never
: IsUniversalMappedProperty<MetadataPropertyConfig, K>]?: never;
};
@@ -0,0 +1,9 @@
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
export type ScalarFlatEntity<TEntity> = Omit<
TEntity,
| ExtractEntityRelatedEntityProperties<TEntity>
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity>;