Fix and refactor all metadata relation (#17978)

# Introduction
The initial motivation was that in the workspace migration create action
some universal foreign key aggregators weren't correctly deleted before
returned due to constant missconfiguration
<img width="2300" height="972" alt="image"
src="https://github.com/user-attachments/assets/9401eb02-2bb2-4e69-9c5f-9a354ff61079"
/>
It also meant that under the hood some optimistic behavior wasn't
correctly rendered for some aggregators

## Solution
Refactored the `ALL_METADATA_RELATIONS` as follows:

This way we can infer the FK and transpile it to a universalFK, also the
aggregators are one to one instead of one versus all available
Making the only manual configuration to be defined the `foreignKey` and
`inverseOneToManyProperty`

```
┌──────────────────────────────────────┐      ┌─────────────────────────────────────────────┐
│  ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY│      │      ALL_ONE_TO_MANY_METADATA_RELATIONS     │
│──────────────────────────────────────│      │─────────────────────────────────────────────│
│  Derived from: Entity types          │      │  Derived from: Entity types                 │
│                                      │      │                                             │
│  Provides:                           │      │  Provides:                                  │
│   • foreignKey                       │      │   • metadataName                            │
│                                      │      │   • flatEntityForeignKeyAggregator          │
│  Standalone low-level primitive      │      │   • universalFlatEntityForeignKeyAggregator │
└──────────────┬───────────────────────┘      └──────────────┬──────────────────────────────┘
               │                                             │
               │ foreignKey type +                           │ inverseOneToManyProperty
               │ universalForeignKey derivation              │ keys (type constraint)
               │                                             │
               ▼                                             ▼
       ┌───────────────────────────────────────────────────────────────┐
       │              ALL_MANY_TO_ONE_METADATA_RELATIONS              │
       │───────────────────────────────────────────────────────────────│
       │  Derived from:                                               │
       │   • Entity types (metadataName, isNullable)                  │
       │   • ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY (FK → universalFK)  │
       │   • ALL_ONE_TO_MANY_METADATA_RELATIONS (inverse keys)        │
       │                                                              │
       │  Provides:                                                   │
       │   • metadataName                                             │
       │   • foreignKey (replicated from FK constant)                 │
       │   • inverseOneToManyProperty                                 │
       │   • isNullable                                               │
       │   • universalForeignKey                                      │
       └──────────────────────────┬────────────────────────────────────┘
                                  │
               ┌──────────────────┼──────────────────┐
               │                  │                  │
               ▼                  ▼                  ▼
   ┌───────────────────┐ ┌────────────────┐ ┌──────────────────────┐
   │  Type consumers   │ │  Atomic utils  │ │  Optimistic utils    │
   │───────────────────│ │────────────────│ │──────────────────────│
   │ • JoinColumn      │ │ • resolve-*    │ │ • add/delete flat    │
   │ • RelatedNames    │ │ • get-*        │ │   entity maps        │
   │ • UniversalFlat   │ │                │ │ • add/delete         │
   │   EntityFrom      │ │                │ │   universal flat     │
   │                   │ │                │ │   entity maps        │
   └───────────────────┘ └────────────────┘ │                      │
                                            │  (bridge via         │
                                            │   inverseOneToMany   │
                                            │   Property →         │
                                            │   ONE_TO_MANY for    │
                                            │   aggregator lookup) │
                                            └──────────────────────┘
```

### Previously
```
┌─────────────────────────────────────────────────────────────────────┐
│                       ALL_METADATA_RELATIONS                       │
│─────────────────────────────────────────────────────────────────────│
│  Derived from: Entity types                                        │
│                                                                    │
│  Structure: { [metadataName]: { manyToOne: {...}, oneToMany: {...},│
│               serializedRelations?: {...} } }                      │
│                                                                    │
│  manyToOne provides:                                               │
│   • metadataName                                                   │
│   • foreignKey                                                     │
│   • flatEntityForeignKeyAggregator (nullable, often wrong/null)    │
│   • isNullable                                                     │
│                                                                    │
│  oneToMany provides:                                               │
│   • metadataName                                                   │
│                                                                    │
│  Monolithic single source of truth                                 │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
                           │ manyToOne entries transformed via
                           │ ToUniversalMetadataManyToOneRelationConfiguration
                           │
                           ▼
┌─────────────────────────────────────────────────────────────────────┐
│                  ALL_UNIVERSAL_METADATA_RELATIONS                   │
│─────────────────────────────────────────────────────────────────────│
│  Derived from: ALL_METADATA_RELATIONS (type-level transform)       │
│                                                                    │
│  Structure: { [metadataName]: { manyToOne: {...}, oneToMany: {...} │
│  } }                                                               │
│                                                                    │
│  manyToOne provides:                                               │
│   • metadataName                                                   │
│   • foreignKey                                                     │
│   • universalForeignKey (derived: FK → replace Id → UniversalId)   │
│   • universalFlatEntityForeignKeyAggregator (derived from          │
│     flatEntityForeignKeyAggregator → replace Ids → UniversalIds)   │
│   • isNullable                                                     │
│                                                                    │
│  oneToMany: passthrough from ALL_METADATA_RELATIONS                │
│                                                                    │
│  Duplicated monolith with universal key transforms                 │
└──────────────────────────┬──────────────────────────────────────────┘
                           │
        ┌──────────────────┼──────────────────────┐
        │                  │                      │
        ▼                  ▼                      ▼
┌───────────────┐ ┌────────────────────┐ ┌──────────────────────┐
│ Type consumers│ │   Atomic utils     │ │  Optimistic utils    │
│───────────────│ │────────────────────│ │──────────────────────│
│ • JoinColumn  │ │ • resolve-entity-  │ │ • add/delete flat    │
│ • RelatedNames│ │   relation-univ-id │ │   entity maps        │
│ • Universal   │ │   (ALL_METADATA_   │ │   (ALL_METADATA_     │
│   FlatEntity  │ │    RELATIONS       │ │    RELATIONS         │
│   From        │ │    .manyToOne)     │ │    .manyToOne)       │
│               │ │                    │ │                      │
│ Mixed usage   │ │ • resolve-univ-    │ │ • add/delete univ    │
│ of both       │ │   relation-ids     │ │   flat entity maps   │
│ constants     │ │   (ALL_UNIVERSAL_  │ │   (ALL_UNIVERSAL_    │
│               │ │    METADATA_REL    │ │    METADATA_REL      │
│               │ │    .manyToOne)     │ │    .manyToOne)       │
│               │ │                    │ │                      │
│               │ │ • resolve-univ-    │ │ universalFlatEntity  │
│               │ │   update-rel-ids   │ │ ForeignKeyAggregator │
│               │ │   (ALL_UNIVERSAL_  │ │ read directly from   │
│               │ │    METADATA_REL    │ │ the constant         │
│               │ │    .manyToOne)     │ │                      │
│               │ │                    │ │                      │
│               │ │ • regex hack:      │ │                      │
│               │ │   foreignKey       │ │                      │
│               │ │   .replace(/Id$/,  │ │                      │
│               │ │   'UniversalId')   │ │                      │
└───────────────┘ └────────────────────┘ └──────────────────────┘
```
This commit is contained in:
Paul Rastoin
2026-02-17 11:13:54 +01:00
committed by GitHub
parent 8244610bdc
commit 5544b5dcfe
36 changed files with 1279 additions and 1345 deletions
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with children first (most manyToOne relations first) 1`] = `
[
@@ -23,7 +23,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
"webhook",
"view",
"fieldMetadata",
"role",
"objectMetadata",
"role",
]
`;
@@ -1,7 +1,8 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
import { ALL_ONE_TO_MANY_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
@@ -45,29 +46,46 @@ export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow
selfFlatEntityMaps.universalIdentifierById[flatEntity.id] =
flatEntity.universalIdentifier;
const idBasedManyToOneRelations = Object.values(
ALL_METADATA_RELATIONS[metadataName].manyToOne,
) as Array<{
metadataName: AllMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<AllMetadataName>;
foreignKey: keyof MetadataFlatEntity<T>;
} | null>;
const manyToOneRelations = ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName];
for (const idBasedRelation of idBasedManyToOneRelations) {
if (!isDefined(idBasedRelation)) {
for (const relationPropertyName of Object.keys(manyToOneRelations)) {
const relation = manyToOneRelations[
relationPropertyName as keyof typeof manyToOneRelations
] as {
metadataName: AllMetadataName;
foreignKey: string;
inverseOneToManyProperty: string | null;
} | null;
if (!isDefined(relation)) {
continue;
}
const {
metadataName: relatedMetadataName,
flatEntityForeignKeyAggregator,
foreignKey,
} = idBasedRelation;
inverseOneToManyProperty,
} = relation;
if (!isDefined(flatEntityForeignKeyAggregator)) {
if (!isDefined(inverseOneToManyProperty)) {
continue;
}
const oneToManyRelations =
ALL_ONE_TO_MANY_METADATA_RELATIONS[relatedMetadataName];
const inverseRelation = oneToManyRelations[
inverseOneToManyProperty as keyof typeof oneToManyRelations
] as {
flatEntityForeignKeyAggregator: string;
} | null;
if (!isDefined(inverseRelation)) {
continue;
}
const { flatEntityForeignKeyAggregator } = inverseRelation;
const relatedFlatEntityMapsKey =
getMetadataFlatEntityMapsKey(relatedMetadataName);
@@ -75,9 +93,9 @@ export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow
relatedFlatEntityMapsKey as MetadataRelatedFlatEntityMapsKeys<T>
] as FlatEntityMaps<MetadataFlatEntity<typeof relatedMetadataName>>;
const flatEntityRelatedEntityForeignKeyValue = flatEntity[foreignKey] as
| string
| undefined;
const flatEntityRelatedEntityForeignKeyValue = (
flatEntity as unknown as Record<string, string | undefined>
)[foreignKey];
if (!isDefined(flatEntityRelatedEntityForeignKeyValue)) {
continue;
@@ -103,9 +121,9 @@ export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow
const updatedRelatedEntity = {
...relatedFlatEntity,
[flatEntityForeignKeyAggregator]: [
...(relatedFlatEntity[
...((relatedFlatEntity as unknown as Record<string, string[]>)[
flatEntityForeignKeyAggregator
] as unknown as string[]),
] ?? []),
flatEntity.id,
],
};
@@ -1,7 +1,8 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
import { ALL_ONE_TO_MANY_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
@@ -38,29 +39,46 @@ export const deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOr
},
);
const idBasedManyToOneRelations = Object.values(
ALL_METADATA_RELATIONS[metadataName].manyToOne,
) as Array<{
metadataName: AllMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<AllMetadataName>;
foreignKey: keyof MetadataFlatEntity<T>;
} | null>;
const manyToOneRelations = ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName];
for (const idBasedRelation of idBasedManyToOneRelations) {
if (!isDefined(idBasedRelation)) {
for (const relationPropertyName of Object.keys(manyToOneRelations)) {
const relation = manyToOneRelations[
relationPropertyName as keyof typeof manyToOneRelations
] as {
metadataName: AllMetadataName;
foreignKey: string;
inverseOneToManyProperty: string | null;
} | null;
if (!isDefined(relation)) {
continue;
}
const {
metadataName: relatedMetadataName,
flatEntityForeignKeyAggregator,
foreignKey,
} = idBasedRelation;
inverseOneToManyProperty,
} = relation;
if (!isDefined(flatEntityForeignKeyAggregator)) {
if (!isDefined(inverseOneToManyProperty)) {
continue;
}
const oneToManyRelations =
ALL_ONE_TO_MANY_METADATA_RELATIONS[relatedMetadataName];
const inverseRelation = oneToManyRelations[
inverseOneToManyProperty as keyof typeof oneToManyRelations
] as {
flatEntityForeignKeyAggregator: string;
} | null;
if (!isDefined(inverseRelation)) {
continue;
}
const { flatEntityForeignKeyAggregator } = inverseRelation;
const relatedFlatEntityMapsKey =
getMetadataFlatEntityMapsKey(relatedMetadataName);
@@ -68,9 +86,9 @@ export const deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOr
relatedFlatEntityMapsKey as MetadataRelatedFlatEntityMapsKeys<T>
] as FlatEntityMaps<MetadataFlatEntity<typeof relatedMetadataName>>;
const flatEntityRelatedEntityForeignKeyValue = flatEntity[foreignKey] as
| string
| undefined;
const flatEntityRelatedEntityForeignKeyValue = (
flatEntity as unknown as Record<string, string | undefined>
)[foreignKey];
if (!isDefined(flatEntityRelatedEntityForeignKeyValue)) {
continue;
@@ -100,9 +118,9 @@ export const deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOr
const updatedRelatedEntity = {
...relatedFlatEntity,
[flatEntityForeignKeyAggregator]: (
relatedFlatEntity[
(relatedFlatEntity as unknown as Record<string, string[]>)[
flatEntityForeignKeyAggregator
] as unknown as string[]
] ?? []
).filter((id) => id !== flatEntity.id),
};
@@ -1,17 +1,16 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
import { ALL_ONE_TO_MANY_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant';
export const getMetadataEntityRelationProperties = <T extends AllMetadataName>(
metadataName: T,
) => {
const relationProperties = ALL_METADATA_RELATIONS[metadataName];
return [
...Object.keys(relationProperties.manyToOne),
...Object.keys(relationProperties.oneToMany),
...Object.keys(ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName]),
...Object.keys(ALL_ONE_TO_MANY_METADATA_RELATIONS[metadataName]),
] as (
| keyof (typeof ALL_METADATA_RELATIONS)[T]['manyToOne']
| keyof (typeof ALL_METADATA_RELATIONS)[T]['oneToMany']
| keyof (typeof ALL_MANY_TO_ONE_METADATA_RELATIONS)[T]
| keyof (typeof ALL_ONE_TO_MANY_METADATA_RELATIONS)[T]
)[];
};
@@ -1,14 +1,12 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
import { type MetadataManyToOneRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-related-metadata-names.type';
export const getMetadataManyToOneRelatedNames = <T extends AllMetadataName>(
metadataName: T,
): MetadataManyToOneRelatedMetadataNames<T>[] => {
const relations = ALL_METADATA_RELATIONS[metadataName];
return Object.values(relations.manyToOne)
return Object.values(ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName])
.filter((relation) => relation !== null)
.map(
(relation) => relation.metadataName,
@@ -1,14 +1,12 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { ALL_ONE_TO_MANY_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant';
import { type MetadataOneToManyRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/types/metadata-one-to-many-related-metadata-names.type';
export const getMetadataOneToManyRelatedNames = <T extends AllMetadataName>(
metadataName: T,
): MetadataOneToManyRelatedMetadataNames<T>[] => {
const relations = ALL_METADATA_RELATIONS[metadataName];
return Object.values(relations.oneToMany)
return Object.values(ALL_ONE_TO_MANY_METADATA_RELATIONS[metadataName])
.filter((relation) => relation !== null)
.map(
(relation) => relation.metadataName,
@@ -1,15 +1,11 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { ALL_METADATA_SERIALIZED_RELATION } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-serialized-relation.constant';
export const getMetadataSerializedRelationNames = (
metadataName: AllMetadataName,
): AllMetadataName[] => {
const relations = ALL_METADATA_RELATIONS[metadataName];
if (!('serializedRelations' in relations)) {
return [];
}
return Object.keys(relations.serializedRelations) as AllMetadataName[];
return Object.keys(
ALL_METADATA_SERIALIZED_RELATION[metadataName],
) as AllMetadataName[];
};
@@ -1,25 +1,19 @@
import { t } from '@lingui/core/macro';
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { type RemoveSuffix } from 'twenty-shared/types';
import {
ALL_METADATA_RELATIONS,
type MetadataManyToOneRelationConfiguration,
} from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
type ManyToOneConfig<T extends AllMetadataName> =
(typeof ALL_METADATA_RELATIONS)[T]['manyToOne'];
(typeof ALL_MANY_TO_ONE_METADATA_RELATIONS)[T];
type TargetMetadataNamesForForeignKeys<
T extends AllMetadataName,
@@ -49,9 +43,10 @@ type ResolvedUniversalIdentifiers<
> = {
[K in keyof ManyToOneConfig<T> as ManyToOneConfig<T>[K] extends {
foreignKey: infer FK extends string;
universalForeignKey: infer UFK extends string;
}
? FK extends TProvidedKeys
? `${RemoveSuffix<FK, 'Id'>}UniversalIdentifier`
? UFK
: never
: never]: ManyToOneConfig<T>[K] extends { isNullable: true }
? string | null
@@ -73,15 +68,19 @@ export const resolveEntityRelationUniversalIdentifiers = <
foreignKeyValues: Record<TProvidedKeys, string | null | undefined>;
flatEntityMaps: RequiredFlatEntityMapsForForeignKeys<T, TProvidedKeys>;
}): ResolvedUniversalIdentifiers<T, TProvidedKeys> => {
const relations = ALL_METADATA_RELATIONS[metadataName].manyToOne;
const relationEntries = ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName];
const result: Record<string, string | null> = {};
for (const relation of Object.values(
relations,
) as MetadataManyToOneRelationConfiguration<
T,
ExtractEntityManyToOneEntityRelationProperties<MetadataEntity<T>>
>[]) {
for (const relationPropertyName of Object.keys(relationEntries)) {
const relation = relationEntries[
relationPropertyName as keyof typeof relationEntries
] as {
foreignKey: string;
metadataName: AllMetadataName;
isNullable: boolean;
universalForeignKey: string;
} | null;
if (!isDefined(relation)) {
continue;
}
@@ -90,6 +89,7 @@ export const resolveEntityRelationUniversalIdentifiers = <
foreignKey,
metadataName: targetMetadataName,
isNullable,
universalForeignKey,
} = relation;
if (!Object.prototype.hasOwnProperty.call(foreignKeyValues, foreignKey)) {
@@ -104,14 +104,8 @@ export const resolveEntityRelationUniversalIdentifiers = <
) as keyof RequiredFlatEntityMapsForForeignKeys<T, TProvidedKeys>;
const targetFlatEntityMaps = flatEntityMaps[mapsKey];
// TODO refactor using the new ALL_METADATA_UNIVERSAL_RELATION afterwards
const universalIdentifierKey = foreignKey.replace(
/Id$/,
'UniversalIdentifier',
);
if (isNullable && !isDefined(foreignKeyValue)) {
result[universalIdentifierKey] = null;
result[universalForeignKey] = null;
continue;
}
@@ -126,7 +120,7 @@ export const resolveEntityRelationUniversalIdentifiers = <
);
}
result[universalIdentifierKey] = resolvedUniversalIdentifier;
result[universalForeignKey] = resolvedUniversalIdentifier;
}
return result as ResolvedUniversalIdentifiers<T, TProvidedKeys>;