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:
+1
-1
@@ -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",
|
||||
|
||||
+1065
-152
File diff suppressed because it is too large
Load Diff
+33
-6
@@ -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(
|
||||
|
||||
+2
-10
@@ -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<
|
||||
|
||||
+12
-5
@@ -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;
|
||||
};
|
||||
|
||||
+9
@@ -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>;
|
||||
Reference in New Issue
Block a user