Centralize metadata relations constant + simplification (#16901)

# Introduction
As we introduced a new grain on relation extraction thanks to low level
`SyncableEntity` and `WorkspaceRelatedEntity` we're able to strictly
typesafe extract metadata entity

The new constant centralizes both many to one and one to many constants
metadata entity constants in a more strictly typesafe way. Remains only
the flatEntityForeignKey aggregator which has to be chosen manually
across all available targeted flat entity ids properties
This commit is contained in:
Paul Rastoin
2026-01-02 16:49:06 +01:00
committed by GitHub
parent 3cab1bf80b
commit 42c9ae1ebc
30 changed files with 569 additions and 409 deletions
@@ -8,9 +8,9 @@ import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/wo
import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity';
import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -41,9 +41,7 @@ export class WorkspaceFlatCronTriggerMapCacheService extends WorkspaceCacheProvi
const flatCronTrigger = {
...removePropertiesFromRecord(
cronTriggerEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.cronTrigger,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.cronTrigger)[],
getMetadataEntityRelationProperties('cronTrigger'),
),
universalIdentifier:
cronTriggerEntity.universalIdentifier ?? cronTriggerEntity.id,
@@ -8,9 +8,9 @@ import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/wo
import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity';
import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -42,9 +42,7 @@ export class WorkspaceFlatDatabaseEventTriggerMapCacheService extends WorkspaceC
const flatDatabaseEventTrigger = {
...removePropertiesFromRecord(
databaseEventTriggerEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.databaseEventTrigger,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.databaseEventTrigger)[],
getMetadataEntityRelationProperties('databaseEventTrigger'),
),
universalIdentifier:
databaseEventTriggerEntity.universalIdentifier ??
@@ -177,7 +177,7 @@ export class FieldMetadataEntity<
cascade: true,
},
)
indexFieldMetadatas: Relation<IndexFieldMetadataEntity>;
indexFieldMetadatas: Relation<IndexFieldMetadataEntity[]>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@@ -1,142 +0,0 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ExtractPropertiesThatEndsWithId } from 'twenty-shared/types';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
type ExtractEntityRelations<TEntity extends MetadataEntity<AllMetadataName>> = {
[K in ExtractPropertiesThatEndsWithId<TEntity, 'id' | 'workspaceId'>]: K;
};
type MetadataRelatedMetadataNames<T extends AllMetadataName> =
keyof ExtractEntityRelations<MetadataEntity<T>>;
type MetadataNameAndRelations = {
[TSourceMetadataName in AllMetadataName]: MetadataRelatedMetadataNames<TSourceMetadataName> extends never
? Record<string, never>
: {
[K in MetadataRelatedMetadataNames<TSourceMetadataName>]?: {
[TTargetMetadataName in AllMetadataName]?: {
metadataName: TTargetMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<TTargetMetadataName>;
};
}[AllMetadataName];
};
};
export const ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY = {
fieldMetadata: {
objectMetadataId: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'fieldMetadataIds',
},
},
objectMetadata: {},
view: {
kanbanAggregateOperationFieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'kanbanAggregateOperationViewIds',
},
calendarFieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'calendarViewIds',
},
mainGroupByFieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'mainGroupByFieldMetadataViewIds',
},
objectMetadataId: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'viewIds',
},
},
viewField: {
viewId: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFieldIds',
},
fieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewFieldIds',
},
},
viewGroup: {
viewId: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewGroupIds',
},
},
index: {
objectMetadataId: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'indexMetadataIds',
},
},
serverlessFunction: {},
cronTrigger: {
serverlessFunctionId: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'cronTriggerIds',
},
},
databaseEventTrigger: {
serverlessFunctionId: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'databaseEventTriggerIds',
},
},
routeTrigger: {
serverlessFunctionId: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'routeTriggerIds',
},
},
viewFilter: {
viewId: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFilterIds',
},
fieldMetadataId: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewFilterIds',
},
viewFilterGroupId: {
metadataName: 'viewFilterGroup',
flatEntityForeignKeyAggregator: 'viewFilterIds',
},
},
role: {},
roleTarget: {
roleId: {
metadataName: 'role',
flatEntityForeignKeyAggregator: 'roleTargetIds',
},
},
agent: {},
skill: {},
pageLayout: {},
pageLayoutWidget: {
pageLayoutTabId: {
metadataName: 'pageLayoutTab',
flatEntityForeignKeyAggregator: 'widgetIds',
},
},
pageLayoutTab: {
pageLayoutId: {
metadataName: 'pageLayout',
flatEntityForeignKeyAggregator: 'tabIds',
},
},
rowLevelPermissionPredicate: {},
rowLevelPermissionPredicateGroup: {},
viewFilterGroup: {
viewId: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFilterGroupIds',
},
parentViewFilterGroupId: {
metadataName: 'viewFilterGroup',
flatEntityForeignKeyAggregator: 'childViewFilterGroupIds',
},
},
} as const satisfies MetadataNameAndRelations;
@@ -1,162 +0,0 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-related-entity-properties.type';
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
export const ALL_METADATA_RELATION_PROPERTIES = {
agent: {
workspace: true,
application: true,
},
skill: {
workspace: true,
application: true,
},
fieldMetadata: {
relationTargetFieldMetadata: true,
relationTargetObjectMetadata: true,
fieldPermissions: true,
indexFieldMetadatas: true,
object: true,
viewFields: true,
viewFilters: true,
kanbanAggregateOperationViews: true,
calendarViews: true,
mainGroupByFieldMetadataViews: true,
workspace: true,
application: true,
},
objectMetadata: {
fields: true,
indexMetadatas: true,
targetRelationFields: true,
dataSource: true,
objectPermissions: true,
fieldPermissions: true,
views: true,
workspace: true,
application: true,
},
view: {
objectMetadata: true,
viewFields: true,
viewFilters: true,
viewFilterGroups: true,
viewGroups: true,
viewSorts: true,
workspace: true,
createdBy: true,
application: true,
calendarFieldMetadata: true,
kanbanAggregateOperationFieldMetadata: true,
mainGroupByFieldMetadata: true,
},
viewField: {
fieldMetadata: true,
view: true,
workspace: true,
application: true,
},
viewFilter: {
fieldMetadata: true,
view: true,
viewFilterGroup: true,
workspace: true,
application: true,
},
viewGroup: {
view: true,
workspace: true,
application: true,
},
index: {
objectMetadata: true,
indexFieldMetadatas: true,
workspace: true,
application: true,
},
serverlessFunction: {
cronTriggers: true,
databaseEventTriggers: true,
routeTriggers: true,
serverlessFunctionLayer: true,
workspace: true,
application: true,
},
cronTrigger: {
serverlessFunction: true,
workspace: true,
application: true,
},
databaseEventTrigger: {
serverlessFunction: true,
workspace: true,
application: true,
},
routeTrigger: {
serverlessFunction: true,
workspace: true,
application: true,
},
role: {
roleTargets: true,
objectPermissions: true,
permissionFlags: true,
fieldPermissions: true,
workspace: true,
application: true,
},
roleTarget: {
role: true,
apiKey: true,
workspace: true,
application: true,
},
pageLayout: {
workspace: true,
objectMetadata: true,
tabs: true,
application: true,
},
pageLayoutTab: {
workspace: true,
pageLayout: true,
widgets: true,
application: true,
},
pageLayoutWidget: {
workspace: true,
pageLayoutTab: true,
objectMetadata: true,
application: true,
},
rowLevelPermissionPredicate: {
workspace: true,
role: true,
fieldMetadata: true,
workspaceMemberFieldMetadata: true,
objectMetadata: true,
rowLevelPermissionPredicateGroup: true,
application: true,
},
rowLevelPermissionPredicateGroup: {
workspace: true,
role: true,
parentRowLevelPermissionPredicateGroup: true,
childRowLevelPermissionPredicateGroups: true,
rowLevelPermissionPredicates: true,
application: true,
},
viewFilterGroup: {
view: true,
viewFilters: true,
parentViewFilterGroup: true,
childViewFilterGroups: true,
workspace: true,
application: true,
},
} as const satisfies {
[TName in AllMetadataName]: {
[P in ExtractEntityRelatedEntityProperties<MetadataEntity<TName>>]: true;
};
};
@@ -0,0 +1,436 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type Expect } from 'twenty-shared/testing';
import {
type ExtractPropertiesThatEndsWithId,
type ExtractPropertiesThatEndsWithIds,
} 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 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 MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { type SyncableEntity } from 'src/engine/workspace-manager/workspace-sync/types/syncable-entity.interface';
type ManyToOneRelationValue<
TSourceMetadataName extends AllMetadataName,
TRelationProperty extends ExtractEntityManyToOneEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>,
> =
NonNullable<
MetadataEntity<TSourceMetadataName>[TRelationProperty]
> extends Relation<infer TTargetEntity extends SyncableEntity>
? {
metadataName: FromMetadataEntityToMetadataName<TTargetEntity>;
flatEntityForeignKeyAggregator: ExtractPropertiesThatEndsWithIds<
MetadataFlatEntity<FromMetadataEntityToMetadataName<TTargetEntity>>
// Note: In the best of the world should not be nullable, entities should always declare inverside keys
> | null;
foreignKey: ExtractPropertiesThatEndsWithId<
MetadataFlatEntity<TSourceMetadataName>,
'id' | 'workspaceId'
>;
}
: null;
type OneToManyRelationValue<
TSourceMetadataName extends AllMetadataName,
TRelationProperty extends ExtractEntityOneToManyEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>,
> =
MetadataEntity<TSourceMetadataName>[TRelationProperty] extends Relation<
(infer TTargetEntity extends SyncableEntity)[]
>
? {
metadataName: FromMetadataEntityToMetadataName<TTargetEntity>;
}
: null;
type MetadataRelationsProperties = {
[TSourceMetadataName in AllMetadataName]: {
manyToOne: {
[TRelationProperty in ExtractEntityManyToOneEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>]: ManyToOneRelationValue<TSourceMetadataName, TRelationProperty>;
};
oneToMany: {
[TRelationProperty in ExtractEntityOneToManyEntityRelationProperties<
MetadataEntity<TSourceMetadataName>
>]: OneToManyRelationValue<TSourceMetadataName, TRelationProperty>;
};
};
};
export const ALL_METADATA_RELATIONS = {
agent: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {},
},
skill: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {},
},
fieldMetadata: {
manyToOne: {
object: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'fieldMetadataIds',
foreignKey: 'objectMetadataId',
},
workspace: null,
application: null,
relationTargetFieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'relationTargetFieldMetadataId',
},
relationTargetObjectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'relationTargetObjectMetadataId',
},
},
oneToMany: {
fieldPermissions: null,
indexFieldMetadatas: null,
viewFields: { metadataName: 'viewField' },
viewFilters: { metadataName: 'viewFilter' },
kanbanAggregateOperationViews: { metadataName: 'view' },
calendarViews: { metadataName: 'view' },
mainGroupByFieldMetadataViews: { metadataName: 'view' },
},
},
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',
},
workspace: null,
createdBy: null,
application: null,
calendarFieldMetadata: {
foreignKey: 'calendarFieldMetadataId',
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'calendarViewIds',
},
kanbanAggregateOperationFieldMetadata: {
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'kanbanAggregateOperationViewIds',
},
mainGroupByFieldMetadata: {
foreignKey: 'mainGroupByFieldMetadataId',
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'mainGroupByFieldMetadataViewIds',
},
},
oneToMany: {
viewFields: { metadataName: 'viewField' },
viewFilters: { metadataName: 'viewFilter' },
viewFilterGroups: {
metadataName: 'viewFilterGroup',
},
viewGroups: { metadataName: 'viewGroup' },
// @ts-expect-error TODO migrate viewSort to v2
viewSorts: null,
},
},
viewField: {
manyToOne: {
fieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewFieldIds',
foreignKey: 'fieldMetadataId',
},
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFieldIds',
foreignKey: 'viewId',
},
workspace: null,
application: null,
},
oneToMany: {},
},
viewFilter: {
manyToOne: {
fieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: 'viewFilterIds',
foreignKey: 'fieldMetadataId',
},
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFilterIds',
foreignKey: 'viewId',
},
viewFilterGroup: {
flatEntityForeignKeyAggregator: 'viewFilterIds',
foreignKey: 'viewFilterGroupId',
metadataName: 'viewFilterGroup',
},
workspace: null,
application: null,
},
oneToMany: {},
},
viewGroup: {
manyToOne: {
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewGroupIds',
foreignKey: 'viewId',
},
workspace: null,
application: null,
},
oneToMany: {},
},
index: {
manyToOne: {
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: 'indexMetadataIds',
foreignKey: 'objectMetadataId',
},
workspace: null,
application: null,
},
oneToMany: {
indexFieldMetadatas: null,
},
},
serverlessFunction: {
manyToOne: {
workspace: null,
application: null,
serverlessFunctionLayer: null,
},
oneToMany: {
cronTriggers: { metadataName: 'cronTrigger' },
databaseEventTriggers: { metadataName: 'databaseEventTrigger' },
routeTriggers: { metadataName: 'routeTrigger' },
},
},
cronTrigger: {
manyToOne: {
serverlessFunction: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'cronTriggerIds',
foreignKey: 'serverlessFunctionId',
},
workspace: null,
application: null,
},
oneToMany: {},
},
databaseEventTrigger: {
manyToOne: {
serverlessFunction: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'databaseEventTriggerIds',
foreignKey: 'serverlessFunctionId',
},
workspace: null,
application: null,
},
oneToMany: {},
},
routeTrigger: {
manyToOne: {
serverlessFunction: {
metadataName: 'serverlessFunction',
flatEntityForeignKeyAggregator: 'routeTriggerIds',
foreignKey: 'serverlessFunctionId',
},
workspace: null,
application: null,
},
oneToMany: {},
},
role: {
manyToOne: {
workspace: null,
application: null,
},
oneToMany: {
roleTargets: { metadataName: 'roleTarget' },
objectPermissions: null,
permissionFlags: null,
fieldPermissions: null,
},
},
roleTarget: {
manyToOne: {
role: {
metadataName: 'role',
flatEntityForeignKeyAggregator: 'roleTargetIds',
foreignKey: 'roleId',
},
apiKey: null,
workspace: null,
application: null,
},
oneToMany: {},
},
pageLayout: {
manyToOne: {
workspace: null,
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'objectMetadataId',
},
application: null,
},
oneToMany: {
tabs: { metadataName: 'pageLayoutTab' },
},
},
pageLayoutTab: {
manyToOne: {
workspace: null,
pageLayout: {
metadataName: 'pageLayout',
flatEntityForeignKeyAggregator: 'tabIds',
foreignKey: 'pageLayoutId',
},
application: null,
},
oneToMany: {
widgets: { metadataName: 'pageLayoutWidget' },
},
},
pageLayoutWidget: {
manyToOne: {
workspace: null,
pageLayoutTab: {
metadataName: 'pageLayoutTab',
flatEntityForeignKeyAggregator: 'widgetIds',
foreignKey: 'pageLayoutTabId',
},
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'objectMetadataId',
},
application: null,
},
oneToMany: {},
},
rowLevelPermissionPredicate: {
manyToOne: {
workspace: null,
role: {
metadataName: 'role',
flatEntityForeignKeyAggregator: null,
foreignKey: 'roleId',
},
fieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'fieldMetadataId',
},
workspaceMemberFieldMetadata: {
metadataName: 'fieldMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'workspaceMemberFieldMetadataId',
},
objectMetadata: {
metadataName: 'objectMetadata',
flatEntityForeignKeyAggregator: null,
foreignKey: 'objectMetadataId',
},
rowLevelPermissionPredicateGroup: {
metadataName: 'rowLevelPermissionPredicateGroup',
flatEntityForeignKeyAggregator: null,
foreignKey: 'rowLevelPermissionPredicateGroupId',
},
application: null,
},
oneToMany: {},
},
rowLevelPermissionPredicateGroup: {
manyToOne: {
workspace: null,
role: {
metadataName: 'role',
foreignKey: 'roleId',
flatEntityForeignKeyAggregator: null,
},
parentRowLevelPermissionPredicateGroup: {
metadataName: 'rowLevelPermissionPredicateGroup',
foreignKey: 'parentRowLevelPermissionPredicateGroupId',
flatEntityForeignKeyAggregator:
'childRowLevelPermissionPredicateGroupIds',
},
application: null,
},
oneToMany: {
childRowLevelPermissionPredicateGroups: {
metadataName: 'rowLevelPermissionPredicateGroup',
},
rowLevelPermissionPredicates: {
metadataName: 'rowLevelPermissionPredicate',
},
},
},
viewFilterGroup: {
manyToOne: {
application: null,
parentViewFilterGroup: {
flatEntityForeignKeyAggregator: 'childViewFilterGroupIds',
foreignKey: 'parentViewFilterGroupId',
metadataName: 'viewFilterGroup',
},
view: {
metadataName: 'view',
flatEntityForeignKeyAggregator: 'viewFilterGroupIds',
foreignKey: 'viewId',
},
workspace: null,
},
oneToMany: {
childViewFilterGroups: {
metadataName: 'viewFilterGroup',
},
viewFilters: {
metadataName: 'viewFilter',
},
},
},
} 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
>,
];
@@ -4,7 +4,7 @@ import { type MetadataManyToOneRelatedMetadataNames } from 'src/engine/metadata-
type MetadataRequiredForValidation = {
[T in AllMetadataName]: Record<
MetadataManyToOneRelatedMetadataNames<T>,
Exclude<MetadataManyToOneRelatedMetadataNames<T>, T>,
true
> & {
[K in Exclude<AllMetadataName, T>]?: true;
@@ -52,7 +52,6 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
},
viewFilterGroup: {
view: true,
viewFilterGroup: true,
},
role: {},
roleTarget: {
@@ -63,11 +62,14 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
role: true,
},
skill: {},
pageLayout: {},
pageLayout: {
objectMetadata: true,
},
pageLayoutTab: {
pageLayout: true,
},
pageLayoutWidget: {
objectMetadata: true,
pageLayoutTab: true,
},
rowLevelPermissionPredicate: {
@@ -1,6 +1,10 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import { type ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-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> =
keyof (typeof ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY)[T];
ExtractForeignKeys<(typeof ALL_METADATA_RELATIONS)[T]['manyToOne']>;
@@ -1,10 +1,13 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
import { type ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
type ExtractMetadataNames<T> = {
[K in keyof T]: T[K] extends { metadataName: infer M } ? M : never;
}[keyof T];
export type MetadataManyToOneRelatedMetadataNames<T extends AllMetadataName> =
Extract<
(typeof ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY)[T][MetadataManyToOneJoinColumn<T>],
{ metadataName: AllMetadataName }
>['metadataName'];
ExtractMetadataNames<(typeof ALL_METADATA_RELATIONS)[T]['manyToOne']>,
AllMetadataName
>;
@@ -1,7 +1,7 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
@@ -36,22 +36,29 @@ export const addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow
flatEntityAndRelatedMapsToMutate[flatEntityMapsKey],
});
const manyToOneRelatedMetadataName = Object.entries(
ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY[metadataName],
) as Array<
[
keyof MetadataFlatEntity<T>,
{
metadataName: AllMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<AllMetadataName>;
},
]
>;
const manyToOneRelations = Object.values(
ALL_METADATA_RELATIONS[metadataName].manyToOne,
) as Array<{
metadataName: AllMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<AllMetadataName>;
foreignKey: keyof MetadataFlatEntity<T>;
} | null>;
for (const relation of manyToOneRelations) {
if (!isDefined(relation)) {
continue;
}
const {
metadataName: relatedMetadataName,
flatEntityForeignKeyAggregator,
foreignKey,
} = relation;
if (!isDefined(flatEntityForeignKeyAggregator)) {
continue;
}
for (const [
foreignKey,
{ metadataName: relatedMetadataName, flatEntityForeignKeyAggregator },
] of manyToOneRelatedMetadataName) {
const relatedFlatEntityMapsKey =
getMetadataFlatEntityMapsKey(relatedMetadataName);
const relatedFlatEntityMetadataMaps = flatEntityAndRelatedMapsToMutate[
@@ -1,7 +1,7 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-many-to-one-relations.constant';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
@@ -38,22 +38,29 @@ export const deleteFlatEntityFromFlatEntityAndRelatedEntityMapsThroughMutationOr
flatEntityAndRelatedMapsToMutate[flatEntityMapsKey],
});
const manyToOneRelatedMetadataName = Object.entries(
ALL_METADATA_RELATED_METADATA_BY_FOREIGN_KEY[metadataName],
) as Array<
[
keyof MetadataFlatEntity<T>,
{
metadataName: AllMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<AllMetadataName>;
},
]
>;
const manyToOneRelations = Object.values(
ALL_METADATA_RELATIONS[metadataName].manyToOne,
) as Array<{
metadataName: AllMetadataName;
flatEntityForeignKeyAggregator: keyof MetadataFlatEntity<AllMetadataName>;
foreignKey: keyof MetadataFlatEntity<T>;
} | null>;
for (const relation of manyToOneRelations) {
if (!isDefined(relation)) {
continue;
}
const {
metadataName: relatedMetadataName,
flatEntityForeignKeyAggregator,
foreignKey,
} = relation;
if (!isDefined(flatEntityForeignKeyAggregator)) {
continue;
}
for (const [
foreignKey,
{ metadataName: relatedMetadataName, flatEntityForeignKeyAggregator },
] of manyToOneRelatedMetadataName) {
const relatedFlatEntityMapsKey =
getMetadataFlatEntityMapsKey(relatedMetadataName);
@@ -0,0 +1,17 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { ALL_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-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),
] as (
| keyof (typeof ALL_METADATA_RELATIONS)[T]['manyToOne']
| keyof (typeof ALL_METADATA_RELATIONS)[T]['oneToMany']
)[];
};
@@ -2,7 +2,7 @@ import { type FieldMetadataType } from 'twenty-shared/types';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export const fromFieldMetadataEntityToFlatFieldMetadata = <
@@ -13,9 +13,7 @@ export const fromFieldMetadataEntityToFlatFieldMetadata = <
): FlatFieldMetadata => {
const fieldMetadataWithoutRelations = removePropertiesFromRecord(
fieldMetadataEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.fieldMetadata,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.fieldMetadata)[],
getMetadataEntityRelationProperties('fieldMetadata'),
);
return {
@@ -1,6 +1,6 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
@@ -9,9 +9,7 @@ export const fromIndexMetadataEntityToFlatIndexMetadata = (
): FlatIndexMetadata => {
const indexMetadataEntityWithoutRelations = removePropertiesFromRecord(
indexMetadataEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.index,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.index)[],
getMetadataEntityRelationProperties('index'),
);
return {
@@ -1,6 +1,6 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -9,9 +9,7 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = (
): FlatObjectMetadata => {
const objectMetadataEntityWithoutRelations = removePropertiesFromRecord(
objectMetadataEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.objectMetadata,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.objectMetadata)[],
getMetadataEntityRelationProperties('objectMetadata'),
);
return {
@@ -1,6 +1,6 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
import { type PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
@@ -9,9 +9,7 @@ export const fromPageLayoutWidgetEntityToFlatPageLayoutWidget = (
): FlatPageLayoutWidget => {
const pageLayoutWidgetEntityWithoutRelations = removePropertiesFromRecord(
pageLayoutWidgetEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.pageLayoutWidget,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.pageLayoutWidget)[],
getMetadataEntityRelationProperties('pageLayoutWidget'),
);
return {
@@ -5,9 +5,9 @@ import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { type FlatSkillMaps } from 'src/engine/metadata-modules/flat-skill/types/flat-skill-maps.type';
import { transformSkillEntityToFlatSkill } from 'src/engine/metadata-modules/flat-skill/utils/transform-skill-entity-to-flat-skill.util';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatSkillMaps } from 'src/engine/metadata-modules/flat-skill/types/flat-skill-maps.type';
import { fromSkillEntityToFlatSkill } from 'src/engine/metadata-modules/flat-skill/utils/from-skill-entity-to-flat-skill.util';
import { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
@@ -31,7 +31,7 @@ export class WorkspaceFlatSkillMapCacheService extends WorkspaceCacheProvider<Fl
const flatSkillMaps = createEmptyFlatEntityMaps();
for (const skillEntity of skills) {
const flatSkill = transformSkillEntityToFlatSkill(skillEntity);
const flatSkill = fromSkillEntityToFlatSkill(skillEntity);
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: flatSkill,
@@ -1,7 +1,7 @@
import { type SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
import { type FlatSkill } from 'src/engine/metadata-modules/flat-skill/types/flat-skill.type';
import { type SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
export const transformSkillEntityToFlatSkill = (
export const fromSkillEntityToFlatSkill = (
skillEntity: SkillEntity,
): FlatSkill => {
return {
@@ -1,6 +1,6 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
@@ -9,9 +9,7 @@ export const fromViewFieldEntityToFlatViewField = (
): FlatViewField => {
const viewFieldEntityWithoutRelations = removePropertiesFromRecord(
viewFieldEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.viewField,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.viewField)[],
getMetadataEntityRelationProperties('viewField'),
);
return {
@@ -1,6 +1,6 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatViewFilterGroup } from 'src/engine/metadata-modules/flat-view-filter-group/types/flat-view-filter-group.type';
import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
@@ -9,9 +9,7 @@ export const fromViewFilterGroupEntityToFlatViewFilterGroup = (
): FlatViewFilterGroup => {
const viewFilterGroupEntityWithoutRelations = removePropertiesFromRecord(
viewFilterGroupEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.viewFilterGroup,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.viewFilterGroup)[],
getMetadataEntityRelationProperties('viewFilterGroup'),
);
return {
@@ -1,6 +1,6 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter.type';
import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
@@ -9,9 +9,7 @@ export const fromViewFilterEntityToFlatViewFilter = (
): FlatViewFilter => {
const viewFilterEntityWithoutRelations = removePropertiesFromRecord(
viewFilterEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.viewFilter,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.viewFilter)[],
getMetadataEntityRelationProperties('viewFilter'),
);
return {
@@ -1,6 +1,6 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
@@ -9,9 +9,7 @@ export const fromViewGroupEntityToFlatViewGroup = (
): FlatViewGroup => {
const viewGroupEntityWithoutRelations = removePropertiesFromRecord(
viewGroupEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.viewGroup,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.viewGroup)[],
getMetadataEntityRelationProperties('viewGroup'),
);
return {
@@ -1,15 +1,13 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
export const fromViewEntityToFlatView = (viewEntity: ViewEntity): FlatView => {
const viewEntityWithoutRelations = removePropertiesFromRecord(
viewEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.view,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.view)[],
getMetadataEntityRelationProperties('view'),
);
return {
@@ -6,9 +6,9 @@ import { Repository } from 'typeorm';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { ALL_METADATA_RELATION_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations-properties.constant';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
@@ -40,9 +40,7 @@ export class WorkspaceFlatRouteTriggerMapCacheService extends WorkspaceCacheProv
for (const routeTriggerEntity of routeTriggers) {
const routeTriggerEntityWithoutRelations = removePropertiesFromRecord(
routeTriggerEntity,
Object.keys(
ALL_METADATA_RELATION_PROPERTIES.routeTrigger,
) as (keyof typeof ALL_METADATA_RELATION_PROPERTIES.routeTrigger)[],
getMetadataEntityRelationProperties('routeTrigger'),
);
const flatRouteTrigger = {
...routeTriggerEntityWithoutRelations,
@@ -72,11 +72,13 @@ export class RowLevelPermissionPredicateGroupEntity
(rowLevelPermissionPredicateGroup) =>
rowLevelPermissionPredicateGroup.parentRowLevelPermissionPredicateGroup,
)
childRowLevelPermissionPredicateGroups: Relation<RowLevelPermissionPredicateGroupEntity>[];
childRowLevelPermissionPredicateGroups: Relation<
RowLevelPermissionPredicateGroupEntity[]
>;
@OneToMany(
() => RowLevelPermissionPredicateEntity,
(predicate) => predicate.rowLevelPermissionPredicateGroup,
)
rowLevelPermissionPredicates: Relation<RowLevelPermissionPredicateEntity>[];
rowLevelPermissionPredicates: Relation<RowLevelPermissionPredicateEntity[]>;
}
@@ -59,7 +59,7 @@ export class ViewFilterGroupEntity extends SyncableEntity {
view: Relation<ViewEntity>;
@OneToMany(() => ViewFilterEntity, (viewFilter) => viewFilter.viewFilterGroup)
viewFilters: Relation<ViewFilterEntity>[];
viewFilters: Relation<ViewFilterEntity[]>;
@ManyToOne(
() => ViewFilterGroupEntity,
@@ -75,5 +75,5 @@ export class ViewFilterGroupEntity extends SyncableEntity {
() => ViewFilterGroupEntity,
(viewFilterGroup) => viewFilterGroup.parentViewFilterGroup,
)
childViewFilterGroups: Relation<ViewFilterGroupEntity>[];
childViewFilterGroups: Relation<ViewFilterGroupEntity[]>;
}