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
@@ -4,8 +4,8 @@ import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/typ
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
import { type ScalarFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/scalar-flat-entity.type';
import { type AllJsonbPropertiesWithSerializedPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant';
import { type ToUniversalForeignKey } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-universal-metadata-relations.constant';
import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type';
import { type ToUniversalForeignKey } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/to-universal-foreign-key.type';
type HasObjectInUnion<T> = T extends unknown
? T extends object
@@ -0,0 +1,253 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import { type ExtractPropertiesThatEndsWithId } from 'twenty-shared/types';
import { type Relation } from 'typeorm';
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 SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
type ManyToOneRelationConfiguration<
TSourceMetadataName extends AllMetadataName,
TRelationProperty extends ExtractEntityManyToOneEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>,
> =
NonNullable<
MetadataEntity<TSourceMetadataName>[TRelationProperty]
> extends Relation<infer _TTargetEntity extends SyncableEntity>
? {
foreignKey: ExtractPropertiesThatEndsWithId<
MetadataEntity<TSourceMetadataName>,
'id' | 'workspaceId'
>;
}
: null;
type ManyToOneMetadataRelationsProperties = {
[TSourceMetadataName in AllMetadataName]: {
[TRelationProperty in ExtractEntityManyToOneEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>]: ManyToOneRelationConfiguration<TSourceMetadataName, TRelationProperty>;
};
};
export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
agent: {
workspace: null,
application: null,
},
skill: {
workspace: null,
application: null,
},
commandMenuItem: {
workspace: null,
application: null,
availabilityObjectMetadata: {
foreignKey: 'availabilityObjectMetadataId',
},
frontComponent: {
foreignKey: 'frontComponentId',
},
},
navigationMenuItem: {
workspace: null,
userWorkspace: null,
application: null,
targetObjectMetadata: {
foreignKey: 'targetObjectMetadataId',
},
folder: {
foreignKey: 'folderId',
},
view: {
foreignKey: 'viewId',
},
},
fieldMetadata: {
object: {
foreignKey: 'objectMetadataId',
},
workspace: null,
application: null,
relationTargetFieldMetadata: {
foreignKey: 'relationTargetFieldMetadataId',
},
relationTargetObjectMetadata: {
foreignKey: 'relationTargetObjectMetadataId',
},
},
objectMetadata: {
dataSource: null,
workspace: null,
application: null,
},
view: {
objectMetadata: {
foreignKey: 'objectMetadataId',
},
workspace: null,
createdBy: null,
application: null,
calendarFieldMetadata: {
foreignKey: 'calendarFieldMetadataId',
},
kanbanAggregateOperationFieldMetadata: {
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
},
mainGroupByFieldMetadata: {
foreignKey: 'mainGroupByFieldMetadataId',
},
},
viewField: {
fieldMetadata: {
foreignKey: 'fieldMetadataId',
},
view: {
foreignKey: 'viewId',
},
viewFieldGroup: {
foreignKey: 'viewFieldGroupId',
},
workspace: null,
application: null,
},
viewFieldGroup: {
view: {
foreignKey: 'viewId',
},
workspace: null,
application: null,
},
viewFilter: {
fieldMetadata: {
foreignKey: 'fieldMetadataId',
},
view: {
foreignKey: 'viewId',
},
viewFilterGroup: {
foreignKey: 'viewFilterGroupId',
},
workspace: null,
application: null,
},
viewGroup: {
view: {
foreignKey: 'viewId',
},
workspace: null,
application: null,
},
index: {
objectMetadata: {
foreignKey: 'objectMetadataId',
},
workspace: null,
application: null,
},
logicFunction: {
workspace: null,
application: null,
},
role: {
workspace: null,
application: null,
},
roleTarget: {
role: {
foreignKey: 'roleId',
},
apiKey: null,
workspace: null,
application: null,
},
pageLayout: {
workspace: null,
objectMetadata: {
foreignKey: 'objectMetadataId',
},
application: null,
defaultTabToFocusOnMobileAndSidePanel: {
foreignKey: 'defaultTabToFocusOnMobileAndSidePanelId',
},
},
pageLayoutTab: {
workspace: null,
pageLayout: {
foreignKey: 'pageLayoutId',
},
application: null,
},
pageLayoutWidget: {
workspace: null,
pageLayoutTab: {
foreignKey: 'pageLayoutTabId',
},
objectMetadata: {
foreignKey: 'objectMetadataId',
},
application: null,
},
rowLevelPermissionPredicate: {
workspace: null,
role: {
foreignKey: 'roleId',
},
fieldMetadata: {
foreignKey: 'fieldMetadataId',
},
workspaceMemberFieldMetadata: {
foreignKey: 'workspaceMemberFieldMetadataId',
},
objectMetadata: {
foreignKey: 'objectMetadataId',
},
rowLevelPermissionPredicateGroup: {
foreignKey: 'rowLevelPermissionPredicateGroupId',
},
application: null,
},
rowLevelPermissionPredicateGroup: {
objectMetadata: {
foreignKey: 'objectMetadataId',
},
role: {
foreignKey: 'roleId',
},
parentRowLevelPermissionPredicateGroup: {
foreignKey: 'parentRowLevelPermissionPredicateGroupId',
},
workspace: null,
application: null,
},
viewFilterGroup: {
application: null,
parentViewFilterGroup: {
foreignKey: 'parentViewFilterGroupId',
},
view: {
foreignKey: 'viewId',
},
workspace: null,
},
frontComponent: {
workspace: null,
application: null,
},
webhook: {
workspace: null,
application: null,
},
} as const satisfies ManyToOneMetadataRelationsProperties;
// satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
Expect<
AllMetadataName extends keyof typeof ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY
? true
: false
>,
];
@@ -0,0 +1,433 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import { type Relation } from 'typeorm';
import { type ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant';
import { type ALL_ONE_TO_MANY_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant';
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type ToUniversalForeignKey } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/to-universal-foreign-key.type';
type FromRelationPropertyToForeignKey<
TMetadataName extends AllMetadataName,
TRelationProperty extends PropertyKey,
> = TRelationProperty extends keyof (typeof ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY)[TMetadataName]
? (typeof ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY)[TMetadataName][TRelationProperty] extends {
foreignKey: infer FK extends string;
}
? FK
: never
: never;
type ManyToOneRelationValue<
TSourceMetadataName extends AllMetadataName,
TRelationProperty extends ExtractEntityManyToOneEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>,
> =
NonNullable<
MetadataEntity<TSourceMetadataName>[TRelationProperty]
> extends Relation<infer TTargetEntity extends SyncableEntity>
? FromMetadataEntityToMetadataName<TTargetEntity> extends infer TTargetMetadataName extends
AllMetadataName
? FromRelationPropertyToForeignKey<
TSourceMetadataName,
TRelationProperty
> extends infer FK
? {
metadataName: TTargetMetadataName;
foreignKey: FK;
// Should not be nullable, in the best of the world relation should always describe an inverse property
inverseOneToManyProperty:
| keyof (typeof ALL_ONE_TO_MANY_METADATA_RELATIONS)[TTargetMetadataName]
| null;
isNullable: null extends MetadataEntity<TSourceMetadataName>[TRelationProperty]
? true
: false;
universalForeignKey: ToUniversalForeignKey<FK & string>;
}
: never
: never
: null;
type ManyToOneMetadataRelationsProperties = {
[TSourceMetadataName in AllMetadataName]: {
[TRelationProperty in ExtractEntityManyToOneEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>]: ManyToOneRelationValue<TSourceMetadataName, TRelationProperty>;
};
};
export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
agent: {
workspace: null,
application: null,
},
skill: {
workspace: null,
application: null,
},
commandMenuItem: {
workspace: null,
application: null,
availabilityObjectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'availabilityObjectMetadataId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'availabilityObjectMetadataUniversalIdentifier',
},
frontComponent: {
metadataName: 'frontComponent',
foreignKey: 'frontComponentId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'frontComponentUniversalIdentifier',
},
},
navigationMenuItem: {
workspace: null,
userWorkspace: null,
application: null,
targetObjectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'targetObjectMetadataId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'targetObjectMetadataUniversalIdentifier',
},
folder: {
metadataName: 'navigationMenuItem',
foreignKey: 'folderId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'folderUniversalIdentifier',
},
view: {
metadataName: 'view',
foreignKey: 'viewId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'viewUniversalIdentifier',
},
},
fieldMetadata: {
object: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: 'fields',
isNullable: false,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
workspace: null,
application: null,
relationTargetFieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'relationTargetFieldMetadataId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'relationTargetFieldMetadataUniversalIdentifier',
},
relationTargetObjectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'relationTargetObjectMetadataId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'relationTargetObjectMetadataUniversalIdentifier',
},
},
objectMetadata: {
dataSource: null,
workspace: null,
application: null,
},
view: {
objectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: 'views',
isNullable: false,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
workspace: null,
createdBy: null,
application: null,
calendarFieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'calendarFieldMetadataId',
inverseOneToManyProperty: 'calendarViews',
isNullable: true,
universalForeignKey: 'calendarFieldMetadataUniversalIdentifier',
},
kanbanAggregateOperationFieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
inverseOneToManyProperty: 'kanbanAggregateOperationViews',
isNullable: true,
universalForeignKey:
'kanbanAggregateOperationFieldMetadataUniversalIdentifier',
},
mainGroupByFieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'mainGroupByFieldMetadataId',
inverseOneToManyProperty: 'mainGroupByFieldMetadataViews',
isNullable: true,
universalForeignKey: 'mainGroupByFieldMetadataUniversalIdentifier',
},
},
viewField: {
fieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'fieldMetadataId',
inverseOneToManyProperty: 'viewFields',
isNullable: false,
universalForeignKey: 'fieldMetadataUniversalIdentifier',
},
view: {
metadataName: 'view',
foreignKey: 'viewId',
inverseOneToManyProperty: 'viewFields',
isNullable: false,
universalForeignKey: 'viewUniversalIdentifier',
},
viewFieldGroup: {
metadataName: 'viewFieldGroup',
foreignKey: 'viewFieldGroupId',
inverseOneToManyProperty: 'viewFields',
isNullable: true,
universalForeignKey: 'viewFieldGroupUniversalIdentifier',
},
workspace: null,
application: null,
},
viewFieldGroup: {
view: {
metadataName: 'view',
foreignKey: 'viewId',
inverseOneToManyProperty: 'viewFieldGroups',
isNullable: false,
universalForeignKey: 'viewUniversalIdentifier',
},
workspace: null,
application: null,
},
viewFilter: {
fieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'fieldMetadataId',
inverseOneToManyProperty: 'viewFilters',
isNullable: false,
universalForeignKey: 'fieldMetadataUniversalIdentifier',
},
view: {
metadataName: 'view',
foreignKey: 'viewId',
inverseOneToManyProperty: 'viewFilters',
isNullable: false,
universalForeignKey: 'viewUniversalIdentifier',
},
viewFilterGroup: {
metadataName: 'viewFilterGroup',
foreignKey: 'viewFilterGroupId',
inverseOneToManyProperty: 'viewFilters',
isNullable: true,
universalForeignKey: 'viewFilterGroupUniversalIdentifier',
},
workspace: null,
application: null,
},
viewGroup: {
view: {
metadataName: 'view',
foreignKey: 'viewId',
inverseOneToManyProperty: 'viewGroups',
isNullable: false,
universalForeignKey: 'viewUniversalIdentifier',
},
workspace: null,
application: null,
},
index: {
objectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: 'indexMetadatas',
isNullable: false,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
workspace: null,
application: null,
},
logicFunction: {
workspace: null,
application: null,
},
role: {
workspace: null,
application: null,
},
roleTarget: {
role: {
metadataName: 'role',
foreignKey: 'roleId',
inverseOneToManyProperty: 'roleTargets',
isNullable: false,
universalForeignKey: 'roleUniversalIdentifier',
},
apiKey: null,
workspace: null,
application: null,
},
pageLayout: {
workspace: null,
objectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
application: null,
defaultTabToFocusOnMobileAndSidePanel: {
metadataName: 'pageLayoutTab',
foreignKey: 'defaultTabToFocusOnMobileAndSidePanelId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey:
'defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier',
},
},
pageLayoutTab: {
workspace: null,
pageLayout: {
metadataName: 'pageLayout',
foreignKey: 'pageLayoutId',
inverseOneToManyProperty: 'tabs',
isNullable: false,
universalForeignKey: 'pageLayoutUniversalIdentifier',
},
application: null,
},
pageLayoutWidget: {
workspace: null,
pageLayoutTab: {
metadataName: 'pageLayoutTab',
foreignKey: 'pageLayoutTabId',
inverseOneToManyProperty: 'widgets',
isNullable: false,
universalForeignKey: 'pageLayoutTabUniversalIdentifier',
},
objectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
application: null,
},
rowLevelPermissionPredicate: {
workspace: null,
role: {
metadataName: 'role',
foreignKey: 'roleId',
inverseOneToManyProperty: 'rowLevelPermissionPredicates',
isNullable: false,
universalForeignKey: 'roleUniversalIdentifier',
},
fieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'fieldMetadataId',
inverseOneToManyProperty: null,
isNullable: false,
universalForeignKey: 'fieldMetadataUniversalIdentifier',
},
workspaceMemberFieldMetadata: {
metadataName: 'fieldMetadata',
foreignKey: 'workspaceMemberFieldMetadataId',
inverseOneToManyProperty: null,
isNullable: true,
universalForeignKey: 'workspaceMemberFieldMetadataUniversalIdentifier',
},
objectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: null,
isNullable: false,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
rowLevelPermissionPredicateGroup: {
metadataName: 'rowLevelPermissionPredicateGroup',
foreignKey: 'rowLevelPermissionPredicateGroupId',
inverseOneToManyProperty: 'rowLevelPermissionPredicates',
isNullable: true,
universalForeignKey:
'rowLevelPermissionPredicateGroupUniversalIdentifier',
},
application: null,
},
rowLevelPermissionPredicateGroup: {
objectMetadata: {
metadataName: 'objectMetadata',
foreignKey: 'objectMetadataId',
inverseOneToManyProperty: null,
isNullable: false,
universalForeignKey: 'objectMetadataUniversalIdentifier',
},
role: {
metadataName: 'role',
foreignKey: 'roleId',
inverseOneToManyProperty: 'rowLevelPermissionPredicateGroups',
isNullable: false,
universalForeignKey: 'roleUniversalIdentifier',
},
parentRowLevelPermissionPredicateGroup: {
metadataName: 'rowLevelPermissionPredicateGroup',
foreignKey: 'parentRowLevelPermissionPredicateGroupId',
inverseOneToManyProperty: 'childRowLevelPermissionPredicateGroups',
isNullable: true,
universalForeignKey:
'parentRowLevelPermissionPredicateGroupUniversalIdentifier',
},
workspace: null,
application: null,
},
viewFilterGroup: {
application: null,
parentViewFilterGroup: {
metadataName: 'viewFilterGroup',
foreignKey: 'parentViewFilterGroupId',
inverseOneToManyProperty: 'childViewFilterGroups',
isNullable: true,
universalForeignKey: 'parentViewFilterGroupUniversalIdentifier',
},
view: {
metadataName: 'view',
foreignKey: 'viewId',
inverseOneToManyProperty: 'viewFilterGroups',
isNullable: false,
universalForeignKey: 'viewUniversalIdentifier',
},
workspace: null,
},
frontComponent: {
workspace: null,
application: null,
},
webhook: {
workspace: null,
application: null,
},
} as const satisfies ManyToOneMetadataRelationsProperties;
// satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
Expect<
AllMetadataName extends keyof typeof ALL_MANY_TO_ONE_METADATA_RELATIONS
? true
: false
>,
];
@@ -1,537 +0,0 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import { type ExtractPropertiesThatEndsWithId } from 'twenty-shared/types';
import { type Relation } from 'typeorm';
import { type AddSuffixToEntityOneToManyProperties } from 'src/engine/metadata-modules/flat-entity/types/add-suffix-to-entity-one-to-many-properties.type';
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
import { type AllJsonbPropertiesWithSerializedPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant';
export type MetadataManyToOneRelationConfiguration<
TSourceMetadataName extends AllMetadataName,
TRelationProperty extends ExtractEntityManyToOneEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>,
> =
NonNullable<
MetadataEntity<TSourceMetadataName>[TRelationProperty]
> extends Relation<infer TTargetEntity extends SyncableEntity>
? {
metadataName: FromMetadataEntityToMetadataName<TTargetEntity>;
flatEntityForeignKeyAggregator:
| keyof AddSuffixToEntityOneToManyProperties<TTargetEntity, 'ids'>
| null;
foreignKey: ExtractPropertiesThatEndsWithId<
MetadataEntity<TSourceMetadataName>,
'id' | 'workspaceId'
>;
isNullable: null extends MetadataEntity<TSourceMetadataName>[TRelationProperty]
? true
: false;
}
: // Note: In the best of the world should not be nullable, entities should always declare inverside keys
null;
type OneToManyRelationValue<
TSourceMetadataName extends AllMetadataName,
TRelationProperty extends ExtractEntityOneToManyEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>,
> = MetadataEntity<TSourceMetadataName>[TRelationProperty] extends (infer TTargetEntity extends
SyncableEntity)[]
? {
metadataName: FromMetadataEntityToMetadataName<TTargetEntity>;
}
: null;
type MetadataRelationsProperties = {
[TSourceMetadataName in AllMetadataName]: {
manyToOne: {
[TRelationProperty in ExtractEntityManyToOneEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>]: MetadataManyToOneRelationConfiguration<
TSourceMetadataName,
TRelationProperty
>;
};
oneToMany: {
[TRelationProperty in ExtractEntityOneToManyEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>]: OneToManyRelationValue<TSourceMetadataName, TRelationProperty>;
};
} & ([
AllJsonbPropertiesWithSerializedPropertiesForMetadataName<TSourceMetadataName>,
] extends [never]
? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
{}
: {
serializedRelations: Partial<Record<AllMetadataName, true>>;
});
};
export const ALL_METADATA_RELATIONS = {
agent: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {},
},
skill: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {},
},
commandMenuItem: {
manyToOne: {
workspace: null,
application: null,
availabilityObjectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'availabilityObjectMetadataId',
isNullable: true,
},
frontComponent: {
metadataName: 'frontComponent',
flatEntityForeignKeyAggregator: null,
foreignKey: 'frontComponentId',
isNullable: true,
},
},
oneToMany: {},
},
navigationMenuItem: {
manyToOne: {
workspace: null,
userWorkspace: null,
application: null,
targetObjectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'targetObjectMetadataId',
isNullable: true,
},
folder: {
metadataName: 'navigationMenuItem',
flatEntityForeignKeyAggregator: null,
foreignKey: 'folderId',
isNullable: true,
},
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: null,
foreignKey: 'viewId',
isNullable: true,
},
},
oneToMany: {},
},
fieldMetadata: {
manyToOne: {
object: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'fieldIds',
foreignKey: 'objectMetadataId',
isNullable: false,
},
workspace: null,
application: null,
relationTargetFieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'relationTargetFieldMetadataId',
isNullable: true,
},
relationTargetObjectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'relationTargetObjectMetadataId',
isNullable: true,
},
},
oneToMany: {
fieldPermissions: null,
indexFieldMetadatas: null,
viewFields: { metadataName: 'viewField' },
viewFilters: { metadataName: 'viewFilter' },
kanbanAggregateOperationViews: { metadataName: 'view' },
calendarViews: { metadataName: 'view' },
mainGroupByFieldMetadataViews: { metadataName: 'view' },
},
serializedRelations: {
fieldMetadata: true,
},
},
objectMetadata: {
manyToOne: {
dataSource: null,
workspace: null,
application: null,
},
oneToMany: {
fields: { metadataName: 'fieldMetadata' },
indexMetadatas: { metadataName: 'index' },
targetRelationFields: { metadataName: 'fieldMetadata' },
objectPermissions: null,
fieldPermissions: null,
views: { metadataName: 'view' },
},
},
view: {
manyToOne: {
objectMetadata: {
foreignKey: 'objectMetadataId',
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'viewIds',
isNullable: false,
},
workspace: null,
createdBy: null,
application: null,
calendarFieldMetadata: {
foreignKey: 'calendarFieldMetadataId',
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'calendarViewIds',
isNullable: true,
},
kanbanAggregateOperationFieldMetadata: {
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'kanbanAggregateOperationViewIds',
isNullable: true,
},
mainGroupByFieldMetadata: {
foreignKey: 'mainGroupByFieldMetadataId',
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'mainGroupByFieldMetadataViewIds',
isNullable: true,
},
},
oneToMany: {
viewFields: { metadataName: 'viewField' },
viewFilters: { metadataName: 'viewFilter' },
viewFilterGroups: {
metadataName: 'viewFilterGroup',
},
viewGroups: { metadataName: 'viewGroup' },
viewFieldGroups: { metadataName: 'viewFieldGroup' },
// @ts-expect-error TODO migrate viewSort to v2
viewSorts: null,
},
},
viewField: {
manyToOne: {
fieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewFieldIds',
foreignKey: 'fieldMetadataId',
isNullable: false,
},
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFieldIds',
foreignKey: 'viewId',
isNullable: false,
},
viewFieldGroup: {
metadataName: 'viewFieldGroup',
flatEntityForeignKeyAggregator: 'viewFieldIds',
foreignKey: 'viewFieldGroupId',
isNullable: true,
},
workspace: null,
application: null,
},
oneToMany: {},
},
viewFieldGroup: {
manyToOne: {
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFieldGroupIds',
foreignKey: 'viewId',
isNullable: false,
},
workspace: null,
application: null,
},
oneToMany: {
viewFields: { metadataName: 'viewField' },
},
},
viewFilter: {
manyToOne: {
fieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewFilterIds',
foreignKey: 'fieldMetadataId',
isNullable: false,
},
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFilterIds',
foreignKey: 'viewId',
isNullable: false,
},
viewFilterGroup: {
flatEntityForeignKeyAggregator: 'viewFilterIds',
foreignKey: 'viewFilterGroupId',
metadataName: 'viewFilterGroup',
isNullable: true,
},
workspace: null,
application: null,
},
oneToMany: {},
},
viewGroup: {
manyToOne: {
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewGroupIds',
foreignKey: 'viewId',
isNullable: false,
},
workspace: null,
application: null,
},
oneToMany: {},
},
index: {
manyToOne: {
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'indexMetadataIds',
foreignKey: 'objectMetadataId',
isNullable: false,
},
workspace: null,
application: null,
},
oneToMany: {
indexFieldMetadatas: null,
},
},
logicFunction: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {},
},
role: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {
roleTargets: { metadataName: 'roleTarget' },
objectPermissions: null,
permissionFlags: null,
fieldPermissions: null,
rowLevelPermissionPredicates: {
metadataName: 'rowLevelPermissionPredicate',
},
rowLevelPermissionPredicateGroups: {
metadataName: 'rowLevelPermissionPredicateGroup',
},
},
},
roleTarget: {
manyToOne: {
role: {
metadataName: 'role',
flatEntityForeignKeyAggregator: 'roleTargetIds',
foreignKey: 'roleId',
isNullable: false,
},
apiKey: null,
workspace: null,
application: null,
},
oneToMany: {},
},
pageLayout: {
manyToOne: {
workspace: null,
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'objectMetadataId',
isNullable: true,
},
application: null,
defaultTabToFocusOnMobileAndSidePanel: {
metadataName: 'pageLayoutTab',
flatEntityForeignKeyAggregator: null,
foreignKey: 'defaultTabToFocusOnMobileAndSidePanelId',
isNullable: true,
},
},
oneToMany: {
tabs: { metadataName: 'pageLayoutTab' },
},
},
pageLayoutTab: {
manyToOne: {
workspace: null,
pageLayout: {
metadataName: 'pageLayout',
flatEntityForeignKeyAggregator: 'tabIds',
foreignKey: 'pageLayoutId',
isNullable: false,
},
application: null,
},
oneToMany: {
widgets: { metadataName: 'pageLayoutWidget' },
},
},
pageLayoutWidget: {
manyToOne: {
workspace: null,
pageLayoutTab: {
metadataName: 'pageLayoutTab',
flatEntityForeignKeyAggregator: 'widgetIds',
foreignKey: 'pageLayoutTabId',
isNullable: false,
},
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'objectMetadataId',
isNullable: true,
},
application: null,
},
oneToMany: {},
serializedRelations: {
fieldMetadata: true,
},
},
rowLevelPermissionPredicate: {
manyToOne: {
workspace: null,
role: {
metadataName: 'role',
flatEntityForeignKeyAggregator: null,
foreignKey: 'roleId',
isNullable: false,
},
fieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'fieldMetadataId',
isNullable: false,
},
workspaceMemberFieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'workspaceMemberFieldMetadataId',
isNullable: true,
},
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'objectMetadataId',
isNullable: false,
},
rowLevelPermissionPredicateGroup: {
metadataName: 'rowLevelPermissionPredicateGroup',
flatEntityForeignKeyAggregator: null,
foreignKey: 'rowLevelPermissionPredicateGroupId',
isNullable: true,
},
application: null,
},
oneToMany: {},
},
rowLevelPermissionPredicateGroup: {
manyToOne: {
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'objectMetadataId',
isNullable: false,
},
role: {
metadataName: 'role',
foreignKey: 'roleId',
flatEntityForeignKeyAggregator: null,
isNullable: false,
},
parentRowLevelPermissionPredicateGroup: {
metadataName: 'rowLevelPermissionPredicateGroup',
foreignKey: 'parentRowLevelPermissionPredicateGroupId',
flatEntityForeignKeyAggregator:
'childRowLevelPermissionPredicateGroupIds',
isNullable: true,
},
workspace: null,
application: null,
},
oneToMany: {
childRowLevelPermissionPredicateGroups: {
metadataName: 'rowLevelPermissionPredicateGroup',
},
rowLevelPermissionPredicates: {
metadataName: 'rowLevelPermissionPredicate',
},
},
},
viewFilterGroup: {
manyToOne: {
application: null,
parentViewFilterGroup: {
flatEntityForeignKeyAggregator: 'childViewFilterGroupIds',
foreignKey: 'parentViewFilterGroupId',
metadataName: 'viewFilterGroup',
isNullable: true,
},
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFilterGroupIds',
foreignKey: 'viewId',
isNullable: false,
},
workspace: null,
},
oneToMany: {
childViewFilterGroups: {
metadataName: 'viewFilterGroup',
},
viewFilters: {
metadataName: 'viewFilter',
},
},
},
frontComponent: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {},
},
webhook: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {},
},
} as const satisfies MetadataRelationsProperties;
// Note: satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
Expect<
AllMetadataName extends keyof typeof ALL_METADATA_RELATIONS ? true : false
>,
];
@@ -11,7 +11,7 @@ type MetadataRequiredForValidation = {
};
};
// TODO deprecate in favor of ALL_METADATA_RELATIONS
// TODO deprecate in favor of ALL_METADATA_SERIALIZED_RELATION
export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
fieldMetadata: {
objectMetadata: true,
@@ -0,0 +1,53 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import { type AllJsonbPropertiesWithSerializedPropertiesForMetadataName } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/constants/all-jsonb-properties-with-serialized-relation-by-metadata-name.constant';
type MetadataSerializedRelationProperties = {
[TSourceMetadataName in AllMetadataName]: [
AllJsonbPropertiesWithSerializedPropertiesForMetadataName<TSourceMetadataName>,
] extends [never]
? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
{}
: Partial<Record<AllMetadataName, true>>;
};
export const ALL_METADATA_SERIALIZED_RELATION = {
agent: {},
skill: {},
commandMenuItem: {},
navigationMenuItem: {},
fieldMetadata: {
fieldMetadata: true,
},
objectMetadata: {},
view: {},
viewField: {},
viewFieldGroup: {},
viewFilter: {},
viewGroup: {},
index: {},
logicFunction: {},
role: {},
roleTarget: {},
pageLayout: {},
pageLayoutTab: {},
pageLayoutWidget: {
fieldMetadata: true,
},
rowLevelPermissionPredicate: {},
rowLevelPermissionPredicateGroup: {},
viewFilterGroup: {},
frontComponent: {},
webhook: {},
} as const satisfies MetadataSerializedRelationProperties;
// satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
Expect<
AllMetadataName extends keyof typeof ALL_METADATA_SERIALIZED_RELATION
? true
: false
>,
];
@@ -0,0 +1,215 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import { type RemoveSuffix } from 'twenty-shared/types';
import { type ExtractEntityOneToManyEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type';
import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
type OneToManyRelationValue<
TSourceMetadataName extends AllMetadataName,
TRelationProperty extends ExtractEntityOneToManyEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>,
> = MetadataEntity<TSourceMetadataName>[TRelationProperty] extends (infer TTargetEntity extends
SyncableEntity)[]
? TRelationProperty extends string
? {
metadataName: FromMetadataEntityToMetadataName<TTargetEntity>;
flatEntityForeignKeyAggregator: `${RemoveSuffix<TRelationProperty, 's'>}Ids`;
universalFlatEntityForeignKeyAggregator: `${RemoveSuffix<TRelationProperty, 's'>}UniversalIdentifiers`;
}
: never
: null;
type OneToManyMetadataRelationsProperties = {
[TSourceMetadataName in AllMetadataName]: {
[TRelationProperty in ExtractEntityOneToManyEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>]: OneToManyRelationValue<TSourceMetadataName, TRelationProperty>;
};
};
export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
agent: {},
skill: {},
commandMenuItem: {},
navigationMenuItem: {},
fieldMetadata: {
fieldPermissions: null,
indexFieldMetadatas: null,
viewFields: {
metadataName: 'viewField',
flatEntityForeignKeyAggregator: 'viewFieldIds',
universalFlatEntityForeignKeyAggregator: 'viewFieldUniversalIdentifiers',
},
viewFilters: {
metadataName: 'viewFilter',
flatEntityForeignKeyAggregator: 'viewFilterIds',
universalFlatEntityForeignKeyAggregator: 'viewFilterUniversalIdentifiers',
},
kanbanAggregateOperationViews: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'kanbanAggregateOperationViewIds',
universalFlatEntityForeignKeyAggregator:
'kanbanAggregateOperationViewUniversalIdentifiers',
},
calendarViews: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'calendarViewIds',
universalFlatEntityForeignKeyAggregator:
'calendarViewUniversalIdentifiers',
},
mainGroupByFieldMetadataViews: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'mainGroupByFieldMetadataViewIds',
universalFlatEntityForeignKeyAggregator:
'mainGroupByFieldMetadataViewUniversalIdentifiers',
},
},
objectMetadata: {
fields: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'fieldIds',
universalFlatEntityForeignKeyAggregator: 'fieldUniversalIdentifiers',
},
indexMetadatas: {
metadataName: 'index',
flatEntityForeignKeyAggregator: 'indexMetadataIds',
universalFlatEntityForeignKeyAggregator:
'indexMetadataUniversalIdentifiers',
},
objectPermissions: null,
fieldPermissions: null,
views: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewIds',
universalFlatEntityForeignKeyAggregator: 'viewUniversalIdentifiers',
},
},
view: {
viewFields: {
metadataName: 'viewField',
flatEntityForeignKeyAggregator: 'viewFieldIds',
universalFlatEntityForeignKeyAggregator: 'viewFieldUniversalIdentifiers',
},
viewFilters: {
metadataName: 'viewFilter',
flatEntityForeignKeyAggregator: 'viewFilterIds',
universalFlatEntityForeignKeyAggregator: 'viewFilterUniversalIdentifiers',
},
viewFilterGroups: {
metadataName: 'viewFilterGroup',
flatEntityForeignKeyAggregator: 'viewFilterGroupIds',
universalFlatEntityForeignKeyAggregator:
'viewFilterGroupUniversalIdentifiers',
},
viewGroups: {
metadataName: 'viewGroup',
flatEntityForeignKeyAggregator: 'viewGroupIds',
universalFlatEntityForeignKeyAggregator: 'viewGroupUniversalIdentifiers',
},
viewFieldGroups: {
metadataName: 'viewFieldGroup',
flatEntityForeignKeyAggregator: 'viewFieldGroupIds',
universalFlatEntityForeignKeyAggregator:
'viewFieldGroupUniversalIdentifiers',
},
// @ts-expect-error TODO migrate viewSort to v2
viewSorts: null,
},
viewField: {},
viewFieldGroup: {
viewFields: {
metadataName: 'viewField',
flatEntityForeignKeyAggregator: 'viewFieldIds',
universalFlatEntityForeignKeyAggregator: 'viewFieldUniversalIdentifiers',
},
},
viewFilter: {},
viewGroup: {},
index: {
indexFieldMetadatas: null,
},
logicFunction: {},
role: {
roleTargets: {
metadataName: 'roleTarget',
flatEntityForeignKeyAggregator: 'roleTargetIds',
universalFlatEntityForeignKeyAggregator: 'roleTargetUniversalIdentifiers',
},
objectPermissions: null,
permissionFlags: null,
fieldPermissions: null,
rowLevelPermissionPredicates: {
metadataName: 'rowLevelPermissionPredicate',
flatEntityForeignKeyAggregator: 'rowLevelPermissionPredicateIds',
universalFlatEntityForeignKeyAggregator:
'rowLevelPermissionPredicateUniversalIdentifiers',
},
rowLevelPermissionPredicateGroups: {
metadataName: 'rowLevelPermissionPredicateGroup',
flatEntityForeignKeyAggregator: 'rowLevelPermissionPredicateGroupIds',
universalFlatEntityForeignKeyAggregator:
'rowLevelPermissionPredicateGroupUniversalIdentifiers',
},
},
roleTarget: {},
pageLayout: {
tabs: {
metadataName: 'pageLayoutTab',
flatEntityForeignKeyAggregator: 'tabIds',
universalFlatEntityForeignKeyAggregator: 'tabUniversalIdentifiers',
},
},
pageLayoutTab: {
widgets: {
metadataName: 'pageLayoutWidget',
flatEntityForeignKeyAggregator: 'widgetIds',
universalFlatEntityForeignKeyAggregator: 'widgetUniversalIdentifiers',
},
},
pageLayoutWidget: {},
rowLevelPermissionPredicate: {},
rowLevelPermissionPredicateGroup: {
childRowLevelPermissionPredicateGroups: {
metadataName: 'rowLevelPermissionPredicateGroup',
flatEntityForeignKeyAggregator:
'childRowLevelPermissionPredicateGroupIds',
universalFlatEntityForeignKeyAggregator:
'childRowLevelPermissionPredicateGroupUniversalIdentifiers',
},
rowLevelPermissionPredicates: {
metadataName: 'rowLevelPermissionPredicate',
flatEntityForeignKeyAggregator: 'rowLevelPermissionPredicateIds',
universalFlatEntityForeignKeyAggregator:
'rowLevelPermissionPredicateUniversalIdentifiers',
},
},
viewFilterGroup: {
childViewFilterGroups: {
metadataName: 'viewFilterGroup',
flatEntityForeignKeyAggregator: 'childViewFilterGroupIds',
universalFlatEntityForeignKeyAggregator:
'childViewFilterGroupUniversalIdentifiers',
},
viewFilters: {
metadataName: 'viewFilter',
flatEntityForeignKeyAggregator: 'viewFilterIds',
universalFlatEntityForeignKeyAggregator: 'viewFilterUniversalIdentifiers',
},
},
frontComponent: {},
webhook: {},
} as const satisfies OneToManyMetadataRelationsProperties;
// satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
// eslint-disable-next-line unused-imports/no-unused-vars
type Assertions = [
Expect<
AllMetadataName extends keyof typeof ALL_ONE_TO_MANY_METADATA_RELATIONS
? true
: false
>,
];
@@ -6,7 +6,6 @@ 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,
@@ -1,10 +1,10 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { type ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
type ExtractForeignKeys<T> = {
[K in keyof T]: T[K] extends { foreignKey: infer FK } ? FK : never;
}[keyof T];
export type MetadataManyToOneJoinColumn<T extends AllMetadataName> =
ExtractForeignKeys<(typeof ALL_METADATA_RELATIONS)[T]['manyToOne']>;
ExtractForeignKeys<(typeof ALL_MANY_TO_ONE_METADATA_RELATIONS)[T]>;
@@ -1,6 +1,6 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { type ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
type ExtractMetadataNames<T> = {
[K in keyof T]: T[K] extends { metadataName: infer M } ? M : never;
@@ -8,6 +8,6 @@ type ExtractMetadataNames<T> = {
export type MetadataManyToOneRelatedMetadataNames<T extends AllMetadataName> =
Extract<
ExtractMetadataNames<(typeof ALL_METADATA_RELATIONS)[T]['manyToOne']>,
ExtractMetadataNames<(typeof ALL_MANY_TO_ONE_METADATA_RELATIONS)[T]>,
AllMetadataName
>;
@@ -1,6 +1,6 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import { type ALL_ONE_TO_MANY_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant';
type ExtractMetadataNames<T> = {
[K in keyof T]: T[K] extends { metadataName: infer M } ? M : never;
@@ -8,6 +8,6 @@ type ExtractMetadataNames<T> = {
export type MetadataOneToManyRelatedMetadataNames<T extends AllMetadataName> =
Extract<
ExtractMetadataNames<(typeof ALL_METADATA_RELATIONS)[T]['oneToMany']>,
ExtractMetadataNames<(typeof ALL_ONE_TO_MANY_METADATA_RELATIONS)[T]>,
AllMetadataName
>;
@@ -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>;