Introduce UniversalFlatEntityFrom (#17367)

# Introduction

Creating a `UniversalFlatEntityFrom` that strips out all the relation
and foreignKey properties in order to replace them with
`UniversalIdentifier` suffix

This data type will be major for the workspace migration workspace
agnostic refactor

## Chore
- renamed `flat-entity.type` to `flat-entity-from.type.ts` ( more
accurate to exported module )
- create static test type over the field metadata entity on quite
complex utils as both coverage and documentation

## Example
Here's an example of a `UniversalFlatEntityFrom<FieldMetadataEntity>`

```ts
const universalFlatFieldMetadata: UniversalFlatFieldMetadata<FieldMetadataType.RELATION> = {
  // Base properties (from FieldMetadataEntity, excluding relations and applicationId)
  universalIdentifier: '550e8400-e29b-41d4-a716-446655440001',
  applicationUniversalIdentifier: '5800681c-088e-4e2b-9fc3-bcf6e8ec2051',
  type: FieldMetadataType.RELATION,
  name: 'firstName',
  label: 'First Name',
  defaultValue: null,
  description: 'The first name of the person',
  icon: 'IconUser',
  standardOverrides: null,
  options: null,
  settings: {
    relationType: RelationType.ONE_TO_MANY,
  },
  isCustom: false,
  isActive: true,
  isSystem: false,
  isUIReadOnly: false,
  isNullable: true,
  isUnique: false,
  isLabelSyncedWithName: true,
  morphId: null,

  // Date properties cast to string
  createdAt: '2024-01-15T10:30:00.000Z',
  updatedAt: '2024-01-15T10:30:00.000Z',

  // ManyToOne relation universal identifiers (from FieldMetadataEntity relations)
  relationTargetFieldMetadataUniversalIdentifier:
    '550e8400-e29b-41d4-a716-446655440012',
  relationTargetObjectMetadataUniversalIdentifier:
    '550e8400-e29b-41d4-a716-446655440013',

  // Join column universal identifiers (foreignKey -> universalIdentifier)
  objectMetadataUniversalIdentifier: '550e8400-e29b-41d4-a716-446655440010',

  // OneToMany relation universal identifiers (array of related entity identifiers)
  viewFieldUniversalIdentifiers: [
    '550e8400-e29b-41d4-a716-446655440020',
    '550e8400-e29b-41d4-a716-446655440021',
  ],
  viewFilterUniversalIdentifiers: ['550e8400-e29b-41d4-a716-446655440030'],
  kanbanAggregateOperationViewUniversalIdentifiers: [],
  calendarViewUniversalIdentifiers: [],
  mainGroupByFieldMetadataViewUniversalIdentifiers: [],
};
```

## Settings
Will hop on the settings typing next. Might not be dynamic but
declarative though
This commit is contained in:
Paul Rastoin
2026-01-22 18:30:19 +00:00
committed by GitHub
parent fd43eeda41
commit 4c93ab5259
60 changed files with 408 additions and 52 deletions
@@ -1,5 +1,5 @@
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
export const createEmptyFlatEntityMaps = () =>
({
@@ -2,7 +2,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type WorkspaceMigrationActionType } from 'src/engine/metadata-modules/flat-entity/types/metadata-workspace-migration-action.type';
import { type WorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
@@ -0,0 +1,21 @@
import { type Expect, type Equal } from 'twenty-shared/testing';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type CastRecordTypeOrmDatePropertiesToString } from 'src/engine/metadata-modules/flat-entity/types/cast-record-typeorm-date-properties-to-string.type';
type FieldMetadataDateProperties =
CastRecordTypeOrmDatePropertiesToString<FieldMetadataEntity>;
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
// FieldMetadataEntity has createdAt and updatedAt Date properties
Expect<
Equal<
FieldMetadataDateProperties,
{
createdAt: string;
updatedAt: string;
}
>
>,
];
@@ -0,0 +1,27 @@
import { type Expect, type Equal } from 'twenty-shared/testing';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
type FieldMetadataManyToOneRelations =
ExtractEntityManyToOneEntityRelationProperties<FieldMetadataEntity>;
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
// FieldMetadataEntity ManyToOne relations:
// - object (ObjectMetadataEntity)
// - workspace (WorkspaceEntity)
// - application (ApplicationEntity)
// - relationTargetFieldMetadata (FieldMetadataEntity)
// - relationTargetObjectMetadata (ObjectMetadataEntity)
Expect<
Equal<
FieldMetadataManyToOneRelations,
| 'object'
| 'workspace'
| 'application'
| 'relationTargetFieldMetadata'
| 'relationTargetObjectMetadata'
>
>,
];
@@ -0,0 +1,45 @@
import { type Equal, type Expect } from 'twenty-shared/testing';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
type FieldMetadataOneToManyRelations =
ExtractEntityOneToManyEntityRelationProperties<FieldMetadataEntity>;
type FieldMetadataOneToManySyncableRelations =
ExtractEntityOneToManyEntityRelationProperties<
FieldMetadataEntity,
SyncableEntity
>;
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
// FieldMetadataEntity OneToMany relations (all targets):
// - indexFieldMetadatas, fieldPermissions, viewFields, viewFilters,
// - kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews
Expect<
Equal<
FieldMetadataOneToManyRelations,
| 'indexFieldMetadatas'
| 'fieldPermissions'
| 'viewFields'
| 'viewFilters'
| 'kanbanAggregateOperationViews'
| 'calendarViews'
| 'mainGroupByFieldMetadataViews'
>
>,
// When filtered by SyncableEntity target, only syncable relations are included
Expect<
Equal<
FieldMetadataOneToManySyncableRelations,
| 'viewFields'
| 'viewFilters'
| 'kanbanAggregateOperationViews'
| 'calendarViews'
| 'mainGroupByFieldMetadataViews'
>
>,
];
@@ -0,0 +1,31 @@
import { type Expect, type Equal } from 'twenty-shared/testing';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
type FieldMetadataRelatedProperties =
ExtractEntityRelatedEntityProperties<FieldMetadataEntity>;
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
// FieldMetadataEntity has both ManyToOne and OneToMany relations
// ManyToOne: object, workspace, application, relationTargetFieldMetadata, relationTargetObjectMetadata
// OneToMany: indexFieldMetadatas, fieldPermissions, viewFields, viewFilters, kanbanAggregateOperationViews, calendarViews, mainGroupByFieldMetadataViews
Expect<
Equal<
FieldMetadataRelatedProperties,
| 'object'
| 'workspace'
| 'application'
| 'relationTargetFieldMetadata'
| 'relationTargetObjectMetadata'
| 'indexFieldMetadatas'
| 'fieldPermissions'
| 'viewFields'
| 'viewFilters'
| 'kanbanAggregateOperationViews'
| 'calendarViews'
| 'mainGroupByFieldMetadataViews'
>
>,
];
@@ -0,0 +1,18 @@
import { type Equal, type Expect } from 'twenty-shared/testing';
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
type FieldMetadataJoinColumns = MetadataManyToOneJoinColumn<'fieldMetadata'>;
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
// fieldMetadata foreign keys from ALL_METADATA_RELATIONS
Expect<
Equal<
FieldMetadataJoinColumns,
| 'objectMetadataId'
| 'relationTargetFieldMetadataId'
| 'relationTargetObjectMetadataId'
>
>,
];
@@ -15,8 +15,6 @@ export type SyncableFlatEntity = NonNullableProperties<
export type FlatEntityFrom<TEntity> = Omit<
TEntity,
| ExtractEntityRelatedEntityProperties<TEntity>
| 'application'
| 'workspace'
| keyof CastRecordTypeOrmDatePropertiesToString<TEntity>
> &
CastRecordTypeOrmDatePropertiesToString<TEntity> & {
@@ -1,4 +1,4 @@
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
export type FlatEntityMaps<T extends SyncableFlatEntity> = {
byId: Partial<Record<string, T>>;
@@ -1,5 +1,8 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import { type FieldMetadataType } from 'twenty-shared/types';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { type AllFlatEntityTypesByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
@@ -10,3 +13,17 @@ export type FromMetadataEntityToMetadataName<T extends SyncableEntity> = {
? P
: never;
}[AllMetadataName];
// metadata name lookups works with the base entity type, not with narrowed variants.
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
Expect<
[never] extends [
FromMetadataEntityToMetadataName<
FieldMetadataEntity<FieldMetadataType.RELATION>
>,
]
? true
: false
>,
];
@@ -4,8 +4,8 @@ import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
type AddFlatEntityToFlatEntityMapsOrThrowArgs<T extends SyncableFlatEntity> = {
flatEntity: T;
@@ -5,8 +5,8 @@ import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
export type DeleteFlatEntityFromFlatEntityMapsOrThrowArgs<
T extends SyncableFlatEntity,
@@ -1,7 +1,7 @@
import { isDefined } from 'twenty-shared/utils';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
export const findFlatEntitiesByApplicationId = <T extends SyncableFlatEntity>({
applicationId,
@@ -5,8 +5,8 @@ import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
export type FindFlatEntityByIdInFlatEntityMapsOrThrowArgs<
T extends SyncableFlatEntity,
@@ -1,4 +1,4 @@
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import {
findFlatEntityByIdInFlatEntityMapsOrThrow,
type FindFlatEntityByIdInFlatEntityMapsOrThrowArgs,
@@ -4,8 +4,8 @@ import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
export type FindFlatEntityByUniversalIdentifierOrThrowArgs<
@@ -1,7 +1,7 @@
import { isDefined } from 'twenty-shared/utils';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
export const findFlatEntityByUniversalIdentifier = <
T extends SyncableFlatEntity,
@@ -1,7 +1,7 @@
import { isDefined } from 'twenty-shared/utils';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util';
export type FindManyFlatEntityByIdInFlatEntityMapsOrThrowArgs<
@@ -1,5 +1,5 @@
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { findFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entities-by-application-id.util';
import { getSubFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-flat-entity-maps-or-throw.util';
@@ -1,6 +1,6 @@
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
@@ -1,5 +1,5 @@
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
@@ -1,4 +1,4 @@
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { orderObjectProperties } from 'src/engine/metadata-modules/flat-entity/utils/order-object-properties.util';
export function transformFlatEntityForComparison<